diff --git a/.changeset/internal-helpers-mdx-export.md b/.changeset/internal-helpers-mdx-export.md
new file mode 100644
index 000000000000..91ea4b5e33f7
--- /dev/null
+++ b/.changeset/internal-helpers-mdx-export.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/internal-helpers': minor
+---
+
+Adds an `@astrojs/internal-helpers/mdx` entrypoint with shared helpers used by the Markdown processor packages to render `.mdx` files.
diff --git a/.changeset/markdown-processors-own-mdx.md b/.changeset/markdown-processors-own-mdx.md
new file mode 100644
index 000000000000..6fddb0f0bc47
--- /dev/null
+++ b/.changeset/markdown-processors-own-mdx.md
@@ -0,0 +1,8 @@
+---
+'@astrojs/markdown-remark': minor
+'@astrojs/markdown-satteri': minor
+---
+
+The `unified()` and `satteri()` Markdown processors now render `.mdx` files themselves, through a new optional `createMdxRenderer` hook on the processor. `.mdx` files follow whichever processor you configure, and third-party processors can add their own MDX support by implementing the hook.
+
+`unified()` also gains a `recmaPlugins` option for adding recma (estree/JSX) plugins to the MDX compiler.
diff --git a/.changeset/mdx-extend-markdown-config-processor.md b/.changeset/mdx-extend-markdown-config-processor.md
new file mode 100644
index 000000000000..0a818c8bf41e
--- /dev/null
+++ b/.changeset/mdx-extend-markdown-config-processor.md
@@ -0,0 +1,9 @@
+---
+'@astrojs/mdx': patch
+---
+
+Fixes `.mdx` files ignoring `extendMarkdownConfig: false` for the Markdown processor ([#17030](https://github.com/withastro/astro/issues/17030)).
+
+Previously, `.mdx` files always inherited your site's configured `markdown.processor` (and its plugins), even with `extendMarkdownConfig: false`. Now `extendMarkdownConfig: false` renders `.mdx` with a clean default Sätteri processor, matching the documented behaviour.
+
+The deprecated `remarkPlugins`/`rehypePlugins`/`remarkRehype`/`recmaPlugins` options on `mdx({...})` continue to work: they are wired into the `unified` processor from `@astrojs/markdown-remark`, mirroring how `markdown.remarkPlugins` behaves in core. `recmaPlugins` on `mdx({...})` is now deprecated too — pass it to `unified({ recmaPlugins })` instead.
diff --git a/packages/astro/src/core/viteUtils.ts b/packages/astro/src/core/viteUtils.ts
index 4c9e395533a8..09b04badb121 100644
--- a/packages/astro/src/core/viteUtils.ts
+++ b/packages/astro/src/core/viteUtils.ts
@@ -1,9 +1,10 @@
-import { createRequire } from 'node:module';
import path from 'node:path';
-import { fileURLToPath, pathToFileURL } from 'node:url';
+import { fileURLToPath } from 'node:url';
import { prependForwardSlash, slash } from '../core/path.js';
import type { ModuleLoader } from './module-loader/index.js';
-import { resolveJsToTs, unwrapId, VALID_ID_PREFIX, viteID } from './util.js';
+import { unwrapId, VALID_ID_PREFIX, viteID } from './util.js';
+
+export { resolvePath } from '@astrojs/internal-helpers/mdx';
const isWindows = typeof process !== 'undefined' && process.platform === 'win32';
@@ -14,49 +15,6 @@ export function normalizePath(id: string) {
return path.posix.normalize(isWindows ? slash(id) : id);
}
-/**
- * Resolve island component specifiers to stable paths for hydration metadata.
- *
- * Examples:
- * - `./components/Button.jsx` from `/app/src/pages/index.astro`
- * -> `/app/src/pages/components/Button.tsx` (when `.tsx` exists)
- * - `#components/react/Counter.tsx`
- * -> `/app/src/components/react/Counter.tsx` via package `imports`
- */
-export function resolvePath(specifier: string, importer: string) {
- if (specifier.startsWith('.')) {
- const absoluteSpecifier = path.resolve(path.dirname(importer), specifier);
- return resolveJsToTs(normalizePath(absoluteSpecifier));
- } else if (specifier.startsWith('#')) {
- // Support Node subpath imports (package.json#imports), so this resolves
- // before we hand off to non-runnable dev pipelines.
- //
- // Without this, unresolved values like `/@id/#components/...` can leak
- // into client hydration URLs.
- try {
- // Primary path: CJS-style resolver rooted at the importer.
- const resolved = createRequire(pathToFileURL(importer)).resolve(specifier);
- return resolveJsToTs(normalizePath(resolved));
- } catch {
- try {
- // Fallback: ESM resolver in case environments differ.
- const importerURL = pathToFileURL(importer).toString();
- const resolved = import.meta.resolve(specifier, importerURL);
- const resolvedUrl = new URL(resolved);
- if (resolvedUrl.protocol === 'file:') {
- return resolveJsToTs(normalizePath(fileURLToPath(resolvedUrl)));
- }
- } catch {
- // fall through
- }
- }
- // Keep original behavior for unresolved specifiers (e.g. package ids).
- return specifier;
- } else {
- return specifier;
- }
-}
-
export function rootRelativePath(
root: URL,
idOrUrl: URL | string,
diff --git a/packages/astro/src/vite-plugin-astro/metadata.ts b/packages/astro/src/vite-plugin-astro/metadata.ts
index 3fa0068a5a40..87f8be37f9b2 100644
--- a/packages/astro/src/vite-plugin-astro/metadata.ts
+++ b/packages/astro/src/vite-plugin-astro/metadata.ts
@@ -1,3 +1,4 @@
+import { createDefaultAstroMetadata as createSharedAstroMetadata } from '@astrojs/internal-helpers/mdx';
import type { ModuleInfo } from '../core/module-loader/index.js';
import type { PluginMetadata } from './types.js';
@@ -9,13 +10,5 @@ export function getAstroMetadata(modInfo: ModuleInfo): PluginMetadata['astro'] |
}
export function createDefaultAstroMetadata(): PluginMetadata['astro'] {
- return {
- hydratedComponents: [],
- clientOnlyComponents: [],
- serverComponents: [],
- scripts: [],
- propagation: 'none',
- containsHead: false,
- pageOptions: {},
- };
+ return createSharedAstroMetadata() as PluginMetadata['astro'];
}
diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json
index b31743e7e328..727ffa742332 100644
--- a/packages/integrations/mdx/package.json
+++ b/packages/integrations/mdx/package.json
@@ -35,31 +35,15 @@
},
"dependencies": {
"@astrojs/internal-helpers": "workspace:*",
- "@astrojs/markdown-remark": "workspace:*",
- "@mdx-js/mdx": "^3.1.1",
- "acorn": "^8.16.0",
- "es-module-lexer": "^2.0.0",
- "estree-util-visit": "^2.0.0",
- "hast-util-to-html": "^9.0.5",
- "piccolore": "^0.1.3",
- "rehype-raw": "^7.0.0",
- "remark-gfm": "^4.0.1",
- "remark-smartypants": "^3.0.2",
- "source-map": "^0.7.6",
- "unist-util-visit": "^5.1.0",
- "vfile": "^6.0.3"
+ "@astrojs/markdown-satteri": "workspace:*",
+ "es-module-lexer": "^2.0.0"
},
"peerDependencies": {
- "@astrojs/markdown-satteri": "^0.3.1",
"astro": "^7.0.0"
},
- "peerDependenciesMeta": {
- "@astrojs/markdown-satteri": {
- "optional": true
- }
- },
"devDependencies": {
- "@astrojs/markdown-satteri": "workspace:*",
+ "@astrojs/markdown-remark": "workspace:*",
+ "@mdx-js/mdx": "^3.1.1",
"@shikijs/rehype": "^4.0.2",
"@shikijs/twoslash": "^4.0.2",
"@types/estree": "^1.0.8",
@@ -79,6 +63,7 @@
"satteri": "^0.9.1",
"shiki": "^4.0.2",
"unified": "^11.0.5",
+ "unist-util-visit": "^5.1.0",
"vite": "^8.0.13"
},
"engines": {
diff --git a/packages/integrations/mdx/src/image-constants.ts b/packages/integrations/mdx/src/image-constants.ts
deleted file mode 100644
index 643015ac683d..000000000000
--- a/packages/integrations/mdx/src/image-constants.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-// Tag name we rewrite markdown-derived `
` elements to. Lowercase + hyphenated
-// so MDX routes the tag through the `_components` map.
-export const ASTRO_IMAGE_ELEMENT = 'astro-image';
-// Module-level identifier bound to Astro's `Image` component (from `astro:assets`).
-// Imported by every compiled MDX file that contains a rewritten image; used as the
-// fallback when no `components.img` is provided.
-export const ASTRO_IMAGE_IMPORT = '__AstroImage__';
-// Boolean export set on MDX modules that contain rewritten images. Read by
-// `vite-plugin-mdx-postprocess` to decide whether to wire up the image component.
-export const USES_ASTRO_IMAGE_FLAG = '__usesAstroImage';
diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts
index a054545d204c..9dabf4e61cd9 100644
--- a/packages/integrations/mdx/src/index.ts
+++ b/packages/integrations/mdx/src/index.ts
@@ -3,7 +3,12 @@ import { fileURLToPath } from 'node:url';
import {
type AstroMarkdownOptions,
markdownConfigDefaults,
+ type PluggableList,
+ type RehypePlugins,
+ type RemarkPlugins,
+ type RemarkRehype as RemarkRehypeOptions,
} from '@astrojs/internal-helpers/markdown';
+import { satteri } from '@astrojs/markdown-satteri';
import type {
AstroIntegration,
AstroIntegrationLogger,
@@ -12,15 +17,17 @@ import type {
HookParameters,
} from 'astro';
import type { MarkdownProcessor } from 'astro/markdown';
-import type { Options as RemarkRehypeOptions } from 'remark-rehype';
-import type { PluggableList } from 'unified';
import { getContainerRenderer as getContainerRendererImpl } from './container-renderer.js';
-import { isSatteriProcessor, isUnifiedProcessor } from './processor-guards.js';
-import type { OptimizeOptions } from './rehype-optimize-static.js';
-import { ignoreStringPlugins, safeParseFrontmatter } from './utils.js';
+import { isUnifiedProcessor } from './processor-guards.js';
+import { safeParseFrontmatter } from './utils.js';
import { type VitePluginMdxOptions, vitePluginMdx } from './vite-plugin-mdx.js';
import { vitePluginMdxPostprocess } from './vite-plugin-mdx-postprocess.js';
+/** MDX static-optimization options. Mirror of the pipeline's `OptimizeOptions`. */
+export interface OptimizeOptions {
+ ignoreElementNames?: string[];
+}
+
// `gfm`/`smartypants` are deprecated and stay unset unless the user opts in; the
// MDX pipelines treat an absent value as the default (on), like the `.md` processors.
type SharedMarkdownOptions = Required<
@@ -30,6 +37,9 @@ type SharedMarkdownOptions = Required<
export type MdxOptions = SharedMarkdownOptions & {
extendMarkdownConfig: boolean;
+ /**
+ * @deprecated Pass `recmaPlugins` to `unified({ recmaPlugins })` from `@astrojs/markdown-remark` and set it as `markdown.processor` instead. Will be removed in a future major.
+ */
recmaPlugins: PluggableList;
optimize: boolean | OptimizeOptions;
/**
@@ -59,11 +69,7 @@ export type MdxOptions = SharedMarkdownOptions & {
* @internal
*/
export type ResolvedMdxOptions = SharedMarkdownOptions & {
- recmaPlugins: PluggableList;
optimize: boolean | OptimizeOptions;
- remarkPlugins: PluggableList;
- rehypePlugins: PluggableList;
- remarkRehype: RemarkRehypeOptions;
};
type SetupHookParams = HookParameters<'astro:config:setup'> & {
@@ -126,7 +132,7 @@ export default function mdx(partialMdxOptions: Partial = {}): AstroI
},
});
},
- 'astro:config:done': ({ config, logger }) => {
+ 'astro:config:done': async ({ config, logger }) => {
warnDeprecatedMdxPluginOptions(partialMdxOptions, logger);
// We resolve the final MDX options here so that other integrations have a chance to modify
@@ -138,32 +144,52 @@ export default function mdx(partialMdxOptions: Partial = {}): AstroI
const resolvedMdxOptions = applyDefaultOptions({
options: partialMdxOptions,
- defaults: markdownConfigToMdxOptions(markdownConfig, logger),
+ defaults: markdownConfigToMdxOptions(markdownConfig),
});
- const processor = partialMdxOptions.processor ?? config.markdown.processor;
+ // `extendMarkdownConfig: false` renders `.mdx` with a clean default processor
+ // (Sätteri) instead of inheriting the site's `markdown.processor`. An explicit
+ // `mdx({ processor })` always wins.
+ let processor =
+ partialMdxOptions.processor ??
+ (extendMarkdownConfig ? config.markdown.processor : satteri());
- if (extendMarkdownConfig && isUnifiedProcessor(processor)) {
- // MDX inherits from the processor only when the user did NOT pass that option
- // to `mdx({...})`. Following the historical contract: MDX's value REPLACES the
- // markdown processor's value (no per-key merge).
- if (partialMdxOptions.remarkPlugins === undefined) {
- resolvedMdxOptions.remarkPlugins = ignoreStringPlugins(
- processor.options.remarkPlugins,
- logger,
- );
- }
- if (partialMdxOptions.rehypePlugins === undefined) {
- resolvedMdxOptions.rehypePlugins = ignoreStringPlugins(
- processor.options.rehypePlugins,
- logger,
+ // Deprecated `mdx({ remark/rehypePlugins, remarkRehype })` run on the `unified`
+ // processor. Wire them in by swapping to a `unified()` that carries them (per-key
+ // replacing, inheriting the rest when the active processor is already `unified`),
+ // mirroring how core folds `markdown.{remark,rehype}Plugins` into `unified()`.
+ const hasLegacyMdxPlugins =
+ (partialMdxOptions.remarkPlugins?.length ?? 0) > 0 ||
+ (partialMdxOptions.rehypePlugins?.length ?? 0) > 0 ||
+ (partialMdxOptions.recmaPlugins?.length ?? 0) > 0 ||
+ Object.keys(partialMdxOptions.remarkRehype ?? {}).length > 0;
+ if (hasLegacyMdxPlugins) {
+ let unified: typeof import('@astrojs/markdown-remark').unified;
+ try {
+ ({ unified } = await import('@astrojs/markdown-remark'));
+ } catch {
+ throw new Error(
+ '`remarkPlugins`, `rehypePlugins`, `remarkRehype`, and `recmaPlugins` on `mdx({...})` run on the `unified` processor from `@astrojs/markdown-remark`, which is not installed. Install it with:\n npm install @astrojs/markdown-remark',
);
}
- if (partialMdxOptions.remarkRehype === undefined) {
- resolvedMdxOptions.remarkRehype = { ...processor.options.remarkRehype };
- }
+ const base = isUnifiedProcessor(processor) ? processor.options : undefined;
+ processor = unified({
+ // MDX plugin lists are function-only; widen to the processor's plugin type.
+ remarkPlugins:
+ (partialMdxOptions.remarkPlugins as RemarkPlugins | undefined) ?? base?.remarkPlugins,
+ rehypePlugins:
+ (partialMdxOptions.rehypePlugins as RehypePlugins | undefined) ?? base?.rehypePlugins,
+ remarkRehype: partialMdxOptions.remarkRehype ?? base?.remarkRehype,
+ recmaPlugins: partialMdxOptions.recmaPlugins ?? base?.recmaPlugins,
+ gfm: base?.gfm,
+ smartypants: base?.smartypants,
+ });
+ }
+
+ if (extendMarkdownConfig && isUnifiedProcessor(processor)) {
// `gfm`/`smartypants` from `unified({...})` apply to `.mdx` too, unless
- // `mdx({...})` set its own.
+ // `mdx({...})` set its own. The processor's remark/rehype plugins are read
+ // directly by its own `createMdxRenderer`, so no inheritance is needed here.
if (partialMdxOptions.gfm === undefined && processor.options.gfm !== undefined) {
resolvedMdxOptions.gfm = processor.options.gfm;
}
@@ -174,27 +200,8 @@ export default function mdx(partialMdxOptions: Partial = {}): AstroI
resolvedMdxOptions.smartypants = processor.options.smartypants;
}
}
- if (extendMarkdownConfig && isSatteriProcessor(processor)) {
- // `gfm`/`smartPunctuation` from `satteri({ features: {...} })` apply to `.mdx`
- // too, unless `mdx({...})` set its own. Mirrors the unified branch above.
- const features = processor.options.features;
- // `gfm` can be `boolean | GfmOptions`; only the boolean form is shape-compatible
- // with `mdxOptions.gfm`. Object configs stay on the processor and are applied at
- // the satteri/mdx boundary, like `smartPunctuation` below.
- if (partialMdxOptions.gfm === undefined && typeof features.gfm === 'boolean') {
- resolvedMdxOptions.gfm = features.gfm;
- }
- // `smartPunctuation` can be `boolean | SmartPunctuationOptions`; only the boolean
- // form is shape-compatible with `mdxOptions.smartypants`. Object configs stay on
- // the processor and are applied at the satteri/mdx boundary.
- if (
- partialMdxOptions.smartypants === undefined &&
- typeof features.smartPunctuation === 'boolean'
- ) {
- resolvedMdxOptions.smartypants = features.smartPunctuation;
- }
- }
- // Other third-party processors handle their own pipeline via `createMdxRenderer`.
+ // Sätteri and other processors read their own options inside `createMdxRenderer`;
+ // only `unified` needs its `gfm`/`smartypants` lifted into the shared options above.
// Mutate `mdxOptions` so that `vitePluginMdx` can reference the actual options
Object.assign(vitePluginMdxOptions, {
@@ -221,9 +228,9 @@ function warnDeprecatedMdxPluginOptions(
logger: AstroIntegrationLogger,
): void {
if (didWarnAboutDeprecatedMdxPluginOptions) return;
- const deprecated = (['remarkPlugins', 'rehypePlugins', 'remarkRehype'] as const).filter(
- (key) => options[key] !== undefined,
- );
+ const deprecated = (
+ ['remarkPlugins', 'rehypePlugins', 'remarkRehype', 'recmaPlugins'] as const
+ ).filter((key) => options[key] !== undefined);
if (deprecated.length === 0) return;
didWarnAboutDeprecatedMdxPluginOptions = true;
@@ -237,22 +244,14 @@ function warnDeprecatedMdxPluginOptions(
);
}
-function markdownConfigToMdxOptions(
- markdownConfig: SharedMarkdownOptions,
- _logger: AstroIntegrationLogger,
-): ResolvedMdxOptions {
+function markdownConfigToMdxOptions(markdownConfig: SharedMarkdownOptions): ResolvedMdxOptions {
return {
...markdownConfig,
// Deprecated `markdown.{gfm,smartypants}` may be unset (optional in the schema);
// fall back to the processor defaults so the MDX pipeline still enables them by default.
gfm: markdownConfig.gfm ?? markdownConfigDefaults.gfm,
smartypants: markdownConfig.smartypants ?? markdownConfigDefaults.smartypants,
- recmaPlugins: [],
optimize: false,
- // Plugins come from the processor — merged in astro:config:done.
- remarkPlugins: [],
- rehypePlugins: [],
- remarkRehype: {},
};
}
@@ -268,10 +267,6 @@ function applyDefaultOptions({
shikiConfig: options.shikiConfig ?? defaults.shikiConfig,
gfm: options.gfm ?? defaults.gfm,
smartypants: options.smartypants ?? defaults.smartypants,
- recmaPlugins: options.recmaPlugins ?? defaults.recmaPlugins,
optimize: options.optimize ?? defaults.optimize,
- remarkPlugins: options.remarkPlugins ?? defaults.remarkPlugins,
- rehypePlugins: options.rehypePlugins ?? defaults.rehypePlugins,
- remarkRehype: options.remarkRehype ?? defaults.remarkRehype,
};
}
diff --git a/packages/integrations/mdx/src/processor-guards.ts b/packages/integrations/mdx/src/processor-guards.ts
index 5797873e8562..37b9a981088b 100644
--- a/packages/integrations/mdx/src/processor-guards.ts
+++ b/packages/integrations/mdx/src/processor-guards.ts
@@ -1,14 +1,10 @@
import type { UnifiedResolvedOptions } from '@astrojs/markdown-remark';
-import type { SatteriResolvedOptions } from '@astrojs/markdown-satteri';
import type { MarkdownProcessor } from 'astro/markdown';
-// Name-checks for the built-in processors. Type-only imports keep
-// `@astrojs/markdown-satteri` (an optional peer) out of MDX's runtime graph.
+// Name-check for the built-in `unified` processor. The type-only import keeps
+// `@astrojs/markdown-remark` (a dev-only dependency now that its pipeline is invoked via the
+// processor) out of MDX's runtime graph.
export const isUnifiedProcessor = (p: {
name: string;
}): p is MarkdownProcessor => p.name === 'unified';
-
-export const isSatteriProcessor = (p: {
- name: string;
-}): p is MarkdownProcessor => p.name === 'satteri';
diff --git a/packages/integrations/mdx/src/utils.ts b/packages/integrations/mdx/src/utils.ts
index 2bde89e43a57..ad4f90ffd527 100644
--- a/packages/integrations/mdx/src/utils.ts
+++ b/packages/integrations/mdx/src/utils.ts
@@ -1,10 +1,5 @@
import { parseFrontmatter } from '@astrojs/internal-helpers/frontmatter';
-import type { Options as AcornOpts } from 'acorn';
-import { parse } from 'acorn';
-import type { AstroConfig, AstroIntegrationLogger, SSRError } from 'astro';
-import type { MdxjsEsm } from 'mdast-util-mdx';
-import colors from 'piccolore';
-import type { PluggableList } from 'unified';
+import type { AstroConfig, SSRError } from 'astro';
export function appendForwardSlash(path: string) {
return path.endsWith('/') ? path : path + '/';
@@ -64,45 +59,3 @@ export function safeParseFrontmatter(code: string, id: string) {
}
}
-export function jsToTreeNode(
- jsString: string,
- acornOpts: AcornOpts = {
- ecmaVersion: 'latest',
- sourceType: 'module',
- },
-): MdxjsEsm {
- return {
- type: 'mdxjsEsm',
- value: '',
- data: {
- // @ts-expect-error `parse` return types is incompatible but it should work in runtime
- estree: {
- ...parse(jsString, acornOpts),
- type: 'Program',
- sourceType: 'module',
- },
- },
- };
-}
-
-export function ignoreStringPlugins(plugins: any[], logger: AstroIntegrationLogger): PluggableList {
- let validPlugins: PluggableList = [];
- let hasInvalidPlugin = false;
- for (const plugin of plugins) {
- if (typeof plugin === 'string') {
- logger.warn(`${colors.bold(plugin)} not applied.`);
- hasInvalidPlugin = true;
- } else if (Array.isArray(plugin) && typeof plugin[0] === 'string') {
- logger.warn(`${colors.bold(plugin[0])} not applied.`);
- hasInvalidPlugin = true;
- } else {
- validPlugins.push(plugin);
- }
- }
- if (hasInvalidPlugin) {
- logger.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;
-}
diff --git a/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts b/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts
index c401f25e5abf..375b4da3dba1 100644
--- a/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts
+++ b/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts
@@ -5,7 +5,7 @@ import {
ASTRO_IMAGE_ELEMENT,
ASTRO_IMAGE_IMPORT,
USES_ASTRO_IMAGE_FLAG,
-} from './rehype-images-to-component.js';
+} from '@astrojs/internal-helpers/mdx';
import { type FileInfo, getFileInfo } from './utils.js';
const underscoreFragmentImportRegex = /[\s,{]_Fragment[\s,}]/;
diff --git a/packages/integrations/mdx/src/vite-plugin-mdx.ts b/packages/integrations/mdx/src/vite-plugin-mdx.ts
index 10c7d605390b..17d29d0168ea 100644
--- a/packages/integrations/mdx/src/vite-plugin-mdx.ts
+++ b/packages/integrations/mdx/src/vite-plugin-mdx.ts
@@ -1,9 +1,7 @@
import type { SSRError } from 'astro';
import type { MarkdownProcessor, MdxRenderer } from 'astro/markdown';
-import { VFile } from 'vfile';
import type { Plugin } from 'vite';
import type { ResolvedMdxOptions } from './index.js';
-import { isSatteriProcessor, isUnifiedProcessor } from './processor-guards.js';
import { safeParseFrontmatter } from './utils.js';
export interface VitePluginMdxOptions {
@@ -88,64 +86,26 @@ async function resolveMdxRenderer(
): Promise {
const { processor } = opts;
- // Third-party processors opt into MDX support by implementing createMdxRenderer themselves.
- if (processor.createMdxRenderer) {
- return processor.createMdxRenderer(
- {
- syntaxHighlight: opts.mdxOptions.syntaxHighlight,
- shikiConfig: opts.mdxOptions.shikiConfig,
- gfm: opts.mdxOptions.gfm,
- smartypants: opts.mdxOptions.smartypants,
- },
- { optimize: opts.mdxOptions.optimize, recmaPlugins: opts.mdxOptions.recmaPlugins },
+ // The processor owns its MDX pipeline. The built-in `unified` / `satteri` processors and
+ // any third-party processor supply this; without it, the processor cannot render `.mdx`.
+ if (!processor.createMdxRenderer) {
+ throw new Error(
+ `The markdown processor "${processor.name}" does not provide MDX support. ` +
+ `Implement \`createMdxRenderer\` on the processor to enable MDX rendering.`,
);
}
- if (isSatteriProcessor(processor)) {
- const { createMdxProcessor: createSatteriMdxProcessor } = await import('./satteri/index.js');
- const satteriProcessor = createSatteriMdxProcessor(opts.mdxOptions, processor.options, {
+ return processor.createMdxRenderer(
+ {
+ syntaxHighlight: opts.mdxOptions.syntaxHighlight,
+ shikiConfig: opts.mdxOptions.shikiConfig,
+ gfm: opts.mdxOptions.gfm,
+ smartypants: opts.mdxOptions.smartypants,
+ },
+ {
+ optimize: opts.mdxOptions.optimize,
srcDir: opts.srcDir,
- });
- return {
- async process(content, filePath, frontmatter) {
- const result = await satteriProcessor.process(content, filePath, frontmatter);
- return { code: result.code, map: null, astroMetadata: result.astroMetadata };
- },
- };
- }
-
- if (isUnifiedProcessor(processor)) {
- const { createMdxProcessor } = await import('./plugins.js');
- const { getAstroMetadata } = await import('./rehype-analyze-astro-metadata.js');
- const unifiedProcessor = createMdxProcessor(opts.mdxOptions, { sourcemap });
- return {
- async process(content, filePath, frontmatter) {
- const vfile = new VFile({
- value: content,
- path: filePath,
- data: {
- astro: { frontmatter },
- applyFrontmatterExport: { srcDir: opts.srcDir },
- },
- });
- const compiled = await unifiedProcessor.process(vfile);
- const astroMetadata = getAstroMetadata(vfile);
- if (!astroMetadata) {
- throw new Error(
- 'Internal MDX error: Astro metadata is not set by rehype-analyze-astro-metadata',
- );
- }
- return {
- code: String(compiled.value),
- map: compiled.map ? JSON.stringify(compiled.map) : null,
- astroMetadata,
- };
- },
- };
- }
-
- throw new Error(
- `The markdown processor "${processor.name}" does not provide MDX support. ` +
- `Implement \`createMdxRenderer\` on the processor to enable MDX rendering.`,
+ sourcemap,
+ },
);
}
diff --git a/packages/integrations/mdx/test/mdx-plugins.test.ts b/packages/integrations/mdx/test/mdx-plugins.test.ts
index 3fedac4b09b3..002e501bccba 100644
--- a/packages/integrations/mdx/test/mdx-plugins.test.ts
+++ b/packages/integrations/mdx/test/mdx-plugins.test.ts
@@ -128,7 +128,7 @@ describe('MDX plugins - Astro config integration', () => {
} else {
// smartypants defaults to ON — converts quotes to curly and -- to em dash
assert.equal(
- quote.textContent.includes('\u2014'),
+ quote.textContent.includes('—'),
true,
'Smartypants should be ON when not extending markdown config: -- should become em dash.',
);
diff --git a/packages/integrations/mdx/test/units/utils.test.ts b/packages/integrations/mdx/test/units/utils.test.ts
index 9bf181496b17..ffa774092e20 100644
--- a/packages/integrations/mdx/test/units/utils.test.ts
+++ b/packages/integrations/mdx/test/units/utils.test.ts
@@ -1,13 +1,7 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import type { AstroConfig } from 'astro';
-import {
- appendForwardSlash,
- getFileInfo,
- ignoreStringPlugins,
- jsToTreeNode,
-} from '../../dist/utils.js';
-import { SpyLogger } from '../test-utils.ts';
+import { appendForwardSlash, getFileInfo } from '../../dist/utils.js';
describe('utils', () => {
describe('appendForwardSlash', () => {
@@ -93,90 +87,4 @@ describe('utils', () => {
assert.equal(result.fileUrl, '/other/path/file.mdx');
});
});
-
- describe('jsToTreeNode', () => {
- it('parses a simple export statement', () => {
- const node = jsToTreeNode('export const x = 1;');
- const estree = node.data!.estree!;
- assert.equal(node.type, 'mdxjsEsm');
- assert.equal(estree.type, 'Program');
- assert.equal(estree.sourceType, 'module');
- assert.ok(estree.body.length > 0);
- });
-
- it('parses an import statement', () => {
- const node = jsToTreeNode("import foo from 'bar';");
- assert.equal(node.type, 'mdxjsEsm');
- assert.equal(node.data!.estree!.body[0].type, 'ImportDeclaration');
- });
-
- it('parses a function export', () => {
- const node = jsToTreeNode('export function getHeadings() { return []; }');
- assert.equal(node.type, 'mdxjsEsm');
- const decl = node.data!.estree!.body[0];
- assert.equal(decl.type, 'ExportNamedDeclaration');
- });
-
- it('throws on invalid JS', () => {
- assert.throws(() => jsToTreeNode('this is not valid javascript {{{'), {
- name: 'SyntaxError',
- });
- });
- });
-
- describe('ignoreStringPlugins', () => {
- it('returns function plugins unchanged', () => {
- const plugin1 = () => {};
- const plugin2 = () => {};
- const spyLogger = new SpyLogger();
- const logger = spyLogger.forkIntegrationLogger('test-spy');
- const result = ignoreStringPlugins([plugin1, plugin2], logger);
- assert.equal(result.length, 2);
- assert.equal(result[0], plugin1);
- assert.equal(result[1], plugin2);
- assert.equal(spyLogger.logs.filter((m) => m.level === 'warn').length, 0);
- });
-
- it('filters out string-based plugins', () => {
- const fnPlugin = () => {};
- const spyLogger = new SpyLogger();
- const logger = spyLogger.forkIntegrationLogger('test-spy');
- const result = ignoreStringPlugins(['remark-toc', fnPlugin], logger);
- assert.equal(result.length, 1);
- assert.equal(result[0], fnPlugin);
- });
-
- it('filters out array-based string plugins [string, options]', () => {
- const fnPlugin = () => {};
- const spyLogger = new SpyLogger();
- const logger = spyLogger.forkIntegrationLogger('test-spy');
- const result = ignoreStringPlugins([['remark-toc', {}], fnPlugin], logger);
- assert.equal(result.length, 1);
- assert.equal(result[0], fnPlugin);
- });
-
- it('logs warnings for string plugins', () => {
- const spyLogger = new SpyLogger();
- const logger = spyLogger.forkIntegrationLogger('test-spy');
- ignoreStringPlugins(['remark-toc', ['rehype-highlight', {}]], logger);
- // One warning per string plugin + one summary warning
- assert.equal(spyLogger.logs.filter((m) => m.level === 'warn').length, 3);
- });
-
- it('returns empty array for all string plugins', () => {
- const spyLogger = new SpyLogger();
- const logger = spyLogger.forkIntegrationLogger('test-spy');
- const result = ignoreStringPlugins(['remark-toc'], logger);
- assert.equal(result.length, 0);
- });
-
- it('handles array-based function plugins [function, options]', () => {
- const fnPlugin = () => {};
- const spyLogger = new SpyLogger();
- const logger = spyLogger.forkIntegrationLogger('test-spy');
- const result = ignoreStringPlugins([[fnPlugin, { option: true }]], logger);
- assert.equal(result.length, 1);
- assert.equal(spyLogger.logs.filter((m) => m.level === 'warn').length, 0);
- });
- });
});
diff --git a/packages/internal-helpers/package.json b/packages/internal-helpers/package.json
index 241ffc5edbc0..eb1d875e998e 100644
--- a/packages/internal-helpers/package.json
+++ b/packages/internal-helpers/package.json
@@ -20,6 +20,7 @@
"./request": "./dist/request.js",
"./object": "./dist/object.js",
"./markdown": "./dist/markdown.js",
+ "./mdx": "./dist/mdx.js",
"./frontmatter": "./dist/frontmatter.js",
"./shiki": "./dist/shiki.js"
},
diff --git a/packages/internal-helpers/src/markdown.ts b/packages/internal-helpers/src/markdown.ts
index d0e4ae980ebb..2ccec1574d72 100644
--- a/packages/internal-helpers/src/markdown.ts
+++ b/packages/internal-helpers/src/markdown.ts
@@ -9,7 +9,7 @@ import type {
ThemeRegistration,
ThemeRegistrationRaw,
} from 'shiki';
-import type { PluggableList, Plugin } from 'unified';
+import type { Plugin } from 'unified';
import type { RemotePattern } from './remote.js';
// Processor-agnostic markdown contract types, shared between `astro` and the
@@ -51,6 +51,7 @@ export type RehypePlugin = Plugin<
>;
export type RehypePlugins = (string | [string, any] | RehypePlugin | [RehypePlugin, any])[];
export type RemarkRehype = Record;
+export type { PluggableList } from 'unified';
export interface MarkdownHeading {
depth: number;
@@ -119,8 +120,8 @@ export interface MarkdownProcessor {
createRenderer(shared: AstroMarkdownOptions): Promise;
/**
* Create the runtime renderer for `.mdx` files. Optional — when absent, `@astrojs/mdx`
- * falls back to its built-in handling for the known `unified` / `satteri` processor names.
- * Third-party processors should provide this to enable MDX support.
+ * throws, since the processor cannot render `.mdx`. The built-in `unified` / `satteri`
+ * processors implement this; third-party processors should too to enable MDX support.
*/
createMdxRenderer?(shared: AstroMarkdownOptions, mdx: MdxRendererOptions): Promise;
}
@@ -128,7 +129,10 @@ export interface MarkdownProcessor {
/** Cross-cutting MDX options passed to `createMdxRenderer` regardless of processor. */
export interface MdxRendererOptions {
optimize: boolean | { ignoreElementNames?: string[] };
- recmaPlugins: PluggableList;
+ /** Astro's `srcDir`. Pipelines use it to default layout-less MDX pages to UTF-8. */
+ srcDir: URL;
+ /** Whether Vite has sourcemaps enabled; a pipeline may emit a source map when true. */
+ sourcemap?: boolean;
}
/** Runtime renderer for `.mdx` files returned by `createMdxRenderer`. */
diff --git a/packages/internal-helpers/src/mdx.ts b/packages/internal-helpers/src/mdx.ts
new file mode 100644
index 000000000000..d431ed20203f
--- /dev/null
+++ b/packages/internal-helpers/src/mdx.ts
@@ -0,0 +1,93 @@
+import { existsSync } from 'node:fs';
+import { createRequire } from 'node:module';
+import path from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { slash } from './path.js';
+import type { AstroMetadata } from './markdown.js';
+
+// MDX helpers shared between `@astrojs/mdx` and the markdown processor packages
+// (`@astrojs/markdown-remark`, `@astrojs/markdown-satteri`). They live here so the
+// processor packages can own their MDX pipelines without depending on `astro`.
+
+// Tag name we rewrite markdown-derived `
` elements to. Lowercase + hyphenated
+// so MDX routes the tag through the `_components` map.
+export const ASTRO_IMAGE_ELEMENT = 'astro-image';
+// Module-level identifier bound to Astro's `Image` component (from `astro:assets`).
+// Imported by every compiled MDX file that contains a rewritten image; used as the
+// fallback when no `components.img` is provided.
+export const ASTRO_IMAGE_IMPORT = '__AstroImage__';
+// Boolean export set on MDX modules that contain rewritten images. Read by
+// `vite-plugin-mdx-postprocess` to decide whether to wire up the image component.
+export const USES_ASTRO_IMAGE_FLAG = '__usesAstroImage';
+
+export function createDefaultAstroMetadata(): AstroMetadata {
+ return {
+ hydratedComponents: [],
+ clientOnlyComponents: [],
+ serverComponents: [],
+ scripts: [],
+ propagation: 'none',
+ containsHead: false,
+ pageOptions: {},
+ };
+}
+
+const isWindows = typeof process !== 'undefined' && process.platform === 'win32';
+
+/** Re-implementation of Vite's normalizePath that can be used without Vite. */
+function normalizePath(id: string) {
+ return path.posix.normalize(isWindows ? slash(id) : id);
+}
+
+export function resolveJsToTs(filePath: string) {
+ if (filePath.endsWith('.jsx') && !existsSync(filePath)) {
+ const tryPath = filePath.slice(0, -4) + '.tsx';
+ if (existsSync(tryPath)) {
+ return tryPath;
+ }
+ }
+ return filePath;
+}
+
+/**
+ * Resolve island component specifiers to stable paths for hydration metadata.
+ *
+ * Examples:
+ * - `./components/Button.jsx` from `/app/src/pages/index.astro`
+ * -> `/app/src/pages/components/Button.tsx` (when `.tsx` exists)
+ * - `#components/react/Counter.tsx`
+ * -> `/app/src/components/react/Counter.tsx` via package `imports`
+ */
+export function resolvePath(specifier: string, importer: string) {
+ if (specifier.startsWith('.')) {
+ const absoluteSpecifier = path.resolve(path.dirname(importer), specifier);
+ return resolveJsToTs(normalizePath(absoluteSpecifier));
+ } else if (specifier.startsWith('#')) {
+ // Support Node subpath imports (package.json#imports), so this resolves
+ // before we hand off to non-runnable dev pipelines.
+ //
+ // Without this, unresolved values like `/@id/#components/...` can leak
+ // into client hydration URLs.
+ try {
+ // Primary path: CJS-style resolver rooted at the importer.
+ const resolved = createRequire(pathToFileURL(importer)).resolve(specifier);
+ return resolveJsToTs(normalizePath(resolved));
+ } catch {
+ try {
+ // Fallback: ESM resolver in case environments differ.
+ const importerURL = pathToFileURL(importer).toString();
+ const resolved = import.meta.resolve(specifier, importerURL);
+ const resolvedUrl = new URL(resolved);
+ if (resolvedUrl.protocol === 'file:') {
+ return resolveJsToTs(normalizePath(fileURLToPath(resolvedUrl)));
+ }
+ } catch {
+ // fall through
+ }
+ }
+ // Keep original behavior for unresolved specifiers (e.g. package ids).
+ return specifier;
+ } else {
+ return specifier;
+ }
+}
diff --git a/packages/markdown/remark/package.json b/packages/markdown/remark/package.json
index da6bb6eeee58..b58addaeef1f 100644
--- a/packages/markdown/remark/package.json
+++ b/packages/markdown/remark/package.json
@@ -20,6 +20,10 @@
"#import-plugin": {
"browser": "./dist/import-plugin-browser.js",
"default": "./dist/import-plugin-default.js"
+ },
+ "#mdx-processor": {
+ "browser": "./dist/mdx/create-processor-browser.js",
+ "default": "./dist/mdx/create-processor.js"
}
},
"files": [
@@ -35,8 +39,12 @@
"dependencies": {
"@astrojs/internal-helpers": "workspace:*",
"@astrojs/prism": "workspace:*",
+ "@mdx-js/mdx": "^3.1.1",
+ "acorn": "^8.16.0",
+ "estree-util-visit": "^2.0.0",
"github-slugger": "^2.0.0",
"hast-util-from-html": "^2.0.3",
+ "hast-util-to-html": "^9.0.5",
"hast-util-to-text": "^4.0.2",
"mdast-util-definitions": "^6.0.0",
"rehype-raw": "^7.0.0",
@@ -45,6 +53,7 @@
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"remark-smartypants": "^3.0.2",
+ "source-map": "^0.7.6",
"unified": "^11.0.5",
"unist-util-remove-position": "^5.0.0",
"unist-util-visit": "^5.1.0",
@@ -58,7 +67,9 @@
"@types/unist": "^3.0.3",
"astro-scripts": "workspace:*",
"esbuild": "^0.28.0",
+ "mdast-util-mdx": "^3.0.0",
"mdast-util-mdx-expression": "^2.0.1",
+ "mdast-util-mdx-jsx": "^3.2.0",
"shiki": "^4.0.0"
},
"publishConfig": {
diff --git a/packages/markdown/remark/src/mdx/create-processor-browser.ts b/packages/markdown/remark/src/mdx/create-processor-browser.ts
new file mode 100644
index 000000000000..4d0c27299401
--- /dev/null
+++ b/packages/markdown/remark/src/mdx/create-processor-browser.ts
@@ -0,0 +1,18 @@
+import type {
+ AstroMarkdownOptions,
+ MdxRenderer,
+ MdxRendererOptions,
+} from '@astrojs/internal-helpers/markdown';
+import type { UnifiedResolvedOptions } from '../processor.js';
+
+// Browser counterpart of `create-processor.ts`. MDX compilation relies on Node-only
+// APIs (`@mdx-js/mdx`, `node:fs`, …), so it is unavailable in browser/edge bundles.
+export function createUnifiedMdxProcessor(
+ _shared: AstroMarkdownOptions,
+ _mdx: MdxRendererOptions,
+ _options: UnifiedResolvedOptions,
+): MdxRenderer {
+ throw new Error(
+ 'MDX compilation is not available in the browser build of `@astrojs/markdown-remark`.',
+ );
+}
diff --git a/packages/integrations/mdx/src/plugins.ts b/packages/markdown/remark/src/mdx/create-processor.ts
similarity index 52%
rename from packages/integrations/mdx/src/plugins.ts
rename to packages/markdown/remark/src/mdx/create-processor.ts
index 11afdbe504c7..38da20d9f225 100644
--- a/packages/integrations/mdx/src/plugins.ts
+++ b/packages/markdown/remark/src/mdx/create-processor.ts
@@ -1,32 +1,107 @@
-import {
- rehypeHeadingIds,
- rehypePrism,
- rehypeShiki,
- remarkCollectImages,
-} from '@astrojs/markdown-remark';
+import type {
+ AstroMarkdownOptions,
+ MdxRenderer,
+ MdxRendererOptions,
+ RemarkRehype,
+ ShikiConfig,
+ Smartypants,
+ SyntaxHighlightConfig,
+ SyntaxHighlightConfigType,
+} from '@astrojs/internal-helpers/markdown';
import { createProcessor, nodeTypes } from '@mdx-js/mdx';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import remarkSmartypants from 'remark-smartypants';
import { SourceMapGenerator } from 'source-map';
import type { PluggableList } from 'unified';
-import type { ResolvedMdxOptions } from './index.js';
-import { rehypeAnalyzeAstroMetadata } from './rehype-analyze-astro-metadata.js';
+import { VFile } from 'vfile';
+import { rehypeHeadingIds } from '../rehype-collect-headings.js';
+import { rehypePrism } from '../rehype-prism.js';
+import { rehypeShiki } from '../rehype-shiki.js';
+import { remarkCollectImages } from '../remark-collect-images.js';
+import type { UnifiedResolvedOptions } from '../processor.js';
+import { getAstroMetadata, rehypeAnalyzeAstroMetadata } from './rehype-analyze-astro-metadata.js';
import { rehypeApplyFrontmatterExport } from './rehype-apply-frontmatter-export.js';
-import { rehypeInjectHeadingsExport } from './rehype-collect-headings.js';
+import { rehypeInjectHeadingsExport } from './rehype-inject-headings-export.js';
import { rehypeImageToComponent } from './rehype-images-to-component.js';
import rehypeMetaString from './rehype-meta-string.js';
-import { rehypeOptimizeStatic } from './rehype-optimize-static.js';
+import { type OptimizeOptions, rehypeOptimizeStatic } from './rehype-optimize-static.js';
+import { filterStringPlugins } from './utils.js';
// Skip nonessential plugins during performance benchmark runs
const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK);
+/** Fully-resolved inputs the unified MDX pipeline needs to build its processor. */
+interface UnifiedMdxOptions {
+ syntaxHighlight: SyntaxHighlightConfig | SyntaxHighlightConfigType | false | undefined;
+ shikiConfig: ShikiConfig;
+ gfm: boolean;
+ smartypants: boolean | Smartypants;
+ remarkPlugins: PluggableList;
+ rehypePlugins: PluggableList;
+ remarkRehype: RemarkRehype;
+ recmaPlugins: PluggableList;
+ optimize: boolean | OptimizeOptions;
+}
+
interface MdxProcessorExtraOptions {
sourcemap: boolean;
}
+/**
+ * Build the `MdxRenderer` for the `unified` processor. Called via
+ * `unified().createMdxRenderer` — `options` are the processor's own remark/rehype
+ * plugins, `shared` the cross-cutting markdown options, `mdx` the MDX-only inputs.
+ */
+export function createUnifiedMdxProcessor(
+ shared: AstroMarkdownOptions,
+ mdx: MdxRendererOptions,
+ options: UnifiedResolvedOptions,
+): MdxRenderer {
+ const mdxOptions: UnifiedMdxOptions = {
+ syntaxHighlight: shared.syntaxHighlight,
+ shikiConfig: shared.shikiConfig ?? {},
+ // `shared.gfm`/`smartypants` already encode the resolved precedence
+ // (`mdx({...})` > `unified({...})` > deprecated `markdown.*`), applied by `@astrojs/mdx`.
+ gfm: shared.gfm ?? true,
+ smartypants: shared.smartypants ?? true,
+ remarkPlugins: filterStringPlugins(options.remarkPlugins),
+ rehypePlugins: filterStringPlugins(options.rehypePlugins),
+ remarkRehype: options.remarkRehype,
+ recmaPlugins: options.recmaPlugins,
+ optimize: mdx.optimize,
+ };
+
+ const processor = createMdxProcessor(mdxOptions, { sourcemap: mdx.sourcemap ?? false });
+
+ return {
+ async process(content, filePath, frontmatter) {
+ const vfile = new VFile({
+ value: content,
+ path: filePath,
+ data: {
+ astro: { frontmatter },
+ applyFrontmatterExport: { srcDir: mdx.srcDir },
+ },
+ });
+ const compiled = await processor.process(vfile);
+ const astroMetadata = getAstroMetadata(vfile);
+ if (!astroMetadata) {
+ throw new Error(
+ 'Internal MDX error: Astro metadata is not set by rehype-analyze-astro-metadata',
+ );
+ }
+ return {
+ code: String(compiled.value),
+ map: compiled.map ? JSON.stringify(compiled.map) : null,
+ astroMetadata,
+ };
+ },
+ };
+}
+
export function createMdxProcessor(
- mdxOptions: ResolvedMdxOptions,
+ mdxOptions: UnifiedMdxOptions,
extraOptions: MdxProcessorExtraOptions,
) {
return createProcessor({
@@ -43,7 +118,7 @@ export function createMdxProcessor(
});
}
-function getRemarkPlugins(mdxOptions: ResolvedMdxOptions): PluggableList {
+function getRemarkPlugins(mdxOptions: UnifiedMdxOptions): PluggableList {
let remarkPlugins: PluggableList = [];
if (!isPerformanceBenchmark) {
@@ -62,7 +137,7 @@ function getRemarkPlugins(mdxOptions: ResolvedMdxOptions): PluggableList {
return remarkPlugins;
}
-function getRehypePlugins(mdxOptions: ResolvedMdxOptions): PluggableList {
+function getRehypePlugins(mdxOptions: UnifiedMdxOptions): PluggableList {
let rehypePlugins: PluggableList = [
// ensure `data.meta` is preserved in `properties.metastring` for rehype syntax highlighters
rehypeMetaString,
diff --git a/packages/integrations/mdx/src/rehype-analyze-astro-metadata.ts b/packages/markdown/remark/src/mdx/rehype-analyze-astro-metadata.ts
similarity index 97%
rename from packages/integrations/mdx/src/rehype-analyze-astro-metadata.ts
rename to packages/markdown/remark/src/mdx/rehype-analyze-astro-metadata.ts
index 4985423e3099..f5281ae1a32a 100644
--- a/packages/integrations/mdx/src/rehype-analyze-astro-metadata.ts
+++ b/packages/markdown/remark/src/mdx/rehype-analyze-astro-metadata.ts
@@ -1,6 +1,5 @@
import type { AstroMetadata, RehypePlugin } from '@astrojs/internal-helpers/markdown';
-import { AstroError, AstroErrorData } from 'astro/errors';
-import { resolvePath } from 'astro/markdown';
+import { resolvePath } from '@astrojs/internal-helpers/mdx';
import type { Program } from 'estree';
import type { RootContent } from 'hast';
import type {} from 'mdast-util-mdx';
@@ -46,9 +45,9 @@ export const rehypeAnalyzeAstroMetadata: RehypePlugin = () => {
// Match this component with its import source
const matchedImport = findMatchingImport(tagName, imports);
if (!matchedImport) {
- throw new AstroError(
- AstroErrorData.NoMatchingImport.message(node.name!),
- AstroErrorData.NoMatchingImport.hint,
+ throw new Error(
+ `Could not render \`${node.name}\`. No matching import has been found for \`${node.name}\`.\n` +
+ 'Please make sure the component is properly imported.',
);
}
diff --git a/packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts b/packages/markdown/remark/src/mdx/rehype-apply-frontmatter-export.ts
similarity index 100%
rename from packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts
rename to packages/markdown/remark/src/mdx/rehype-apply-frontmatter-export.ts
diff --git a/packages/integrations/mdx/src/rehype-images-to-component.ts b/packages/markdown/remark/src/mdx/rehype-images-to-component.ts
similarity index 97%
rename from packages/integrations/mdx/src/rehype-images-to-component.ts
rename to packages/markdown/remark/src/mdx/rehype-images-to-component.ts
index ad287422c517..c82d9a691251 100644
--- a/packages/integrations/mdx/src/rehype-images-to-component.ts
+++ b/packages/markdown/remark/src/mdx/rehype-images-to-component.ts
@@ -7,11 +7,9 @@ import {
ASTRO_IMAGE_ELEMENT,
ASTRO_IMAGE_IMPORT,
USES_ASTRO_IMAGE_FLAG,
-} from './image-constants.js';
+} from '@astrojs/internal-helpers/mdx';
import { jsToTreeNode } from './utils.js';
-export { ASTRO_IMAGE_ELEMENT, ASTRO_IMAGE_IMPORT, USES_ASTRO_IMAGE_FLAG };
-
function createArrayAttribute(name: string, values: (string | number)[]): MdxJsxAttribute {
return {
type: 'mdxJsxAttribute',
diff --git a/packages/integrations/mdx/src/rehype-collect-headings.ts b/packages/markdown/remark/src/mdx/rehype-inject-headings-export.ts
similarity index 100%
rename from packages/integrations/mdx/src/rehype-collect-headings.ts
rename to packages/markdown/remark/src/mdx/rehype-inject-headings-export.ts
diff --git a/packages/integrations/mdx/src/rehype-meta-string.ts b/packages/markdown/remark/src/mdx/rehype-meta-string.ts
similarity index 100%
rename from packages/integrations/mdx/src/rehype-meta-string.ts
rename to packages/markdown/remark/src/mdx/rehype-meta-string.ts
diff --git a/packages/integrations/mdx/src/rehype-optimize-static.ts b/packages/markdown/remark/src/mdx/rehype-optimize-static.ts
similarity index 100%
rename from packages/integrations/mdx/src/rehype-optimize-static.ts
rename to packages/markdown/remark/src/mdx/rehype-optimize-static.ts
diff --git a/packages/markdown/remark/src/mdx/utils.ts b/packages/markdown/remark/src/mdx/utils.ts
new file mode 100644
index 000000000000..f39b3d2a5c85
--- /dev/null
+++ b/packages/markdown/remark/src/mdx/utils.ts
@@ -0,0 +1,54 @@
+import type { RehypePlugins, RemarkPlugins } from '@astrojs/internal-helpers/markdown';
+import type { Options as AcornOpts } from 'acorn';
+import { parse } from 'acorn';
+import type { MdxjsEsm } from 'mdast-util-mdx';
+import type { Pluggable, PluggableList } from 'unified';
+
+export function jsToTreeNode(
+ jsString: string,
+ acornOpts: AcornOpts = {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ },
+): MdxjsEsm {
+ return {
+ type: 'mdxjsEsm',
+ value: '',
+ data: {
+ // @ts-expect-error `parse` return types is incompatible but it should work in runtime
+ estree: {
+ ...parse(jsString, acornOpts),
+ type: 'Program',
+ sourceType: 'module',
+ },
+ },
+ };
+}
+
+/**
+ * The MDX compiler cannot resolve string-form plugins the way the `.md` pipeline can,
+ * so drop them (with a warning) instead of letting the compiler throw.
+ */
+export function filterStringPlugins(
+ plugins: RemarkPlugins | RehypePlugins | PluggableList,
+): PluggableList {
+ const validPlugins: PluggableList = [];
+ let hasInvalidPlugin = false;
+ for (const plugin of plugins) {
+ if (typeof plugin === 'string') {
+ console.warn(`[MDX] ${plugin} not applied.`);
+ hasInvalidPlugin = true;
+ } else if (Array.isArray(plugin) && typeof plugin[0] === 'string') {
+ console.warn(`[MDX] ${plugin[0]} not applied.`);
+ hasInvalidPlugin = true;
+ } else {
+ validPlugins.push(plugin as Pluggable);
+ }
+ }
+ if (hasInvalidPlugin) {
+ console.warn(
+ `[MDX] To inherit Markdown plugins in MDX, use explicit imports in your config instead of "strings." See https://docs.astro.build/en/guides/markdown-content/#markdown-plugins`,
+ );
+ }
+ return validPlugins;
+}
diff --git a/packages/markdown/remark/src/processor.ts b/packages/markdown/remark/src/processor.ts
index 19772a60c449..a802eb679308 100644
--- a/packages/markdown/remark/src/processor.ts
+++ b/packages/markdown/remark/src/processor.ts
@@ -1,5 +1,6 @@
import type {
MarkdownProcessor,
+ PluggableList,
RehypePlugins,
RemarkPlugins,
RemarkRehype,
@@ -10,6 +11,8 @@ export interface UnifiedProcessorOptions {
remarkPlugins?: RemarkPlugins;
rehypePlugins?: RehypePlugins;
remarkRehype?: RemarkRehype;
+ /** recma (estree/JSX) plugins for the MDX compiler. Only affect `.mdx` files. */
+ recmaPlugins?: PluggableList;
/** Enable GitHub-Flavored Markdown. Defaults to `true`. */
gfm?: boolean;
/** Enable SmartyPants typography. Defaults to `true`; pass an object to configure it. */
@@ -24,6 +27,7 @@ export interface UnifiedResolvedOptions {
remarkPlugins: RemarkPlugins;
rehypePlugins: RehypePlugins;
remarkRehype: RemarkRehype;
+ recmaPlugins: PluggableList;
gfm?: boolean;
smartypants?: boolean | Smartypants;
}
@@ -52,6 +56,7 @@ export function unified(
remarkPlugins: [...(opts.remarkPlugins ?? [])],
rehypePlugins: [...(opts.rehypePlugins ?? [])],
remarkRehype: { ...opts.remarkRehype },
+ recmaPlugins: [...(opts.recmaPlugins ?? [])],
gfm: opts.gfm,
smartypants: opts.smartypants,
},
@@ -69,6 +74,12 @@ export function unified(
smartypants: processor.options.smartypants ?? shared.smartypants,
});
},
+ async createMdxRenderer(shared, mdx) {
+ // Lazy import via `#mdx-processor` so the Node-only MDX/JSX stack isn't pulled into
+ // `.md`-only projects or browser/edge bundles (the browser condition stubs it out).
+ const { createUnifiedMdxProcessor } = await import('#mdx-processor');
+ return createUnifiedMdxProcessor(shared, mdx, processor.options);
+ },
};
return processor;
}
diff --git a/packages/markdown/remark/src/rehype-collect-headings.ts b/packages/markdown/remark/src/rehype-collect-headings.ts
index 4e65c44f581e..81441a94fdd2 100644
--- a/packages/markdown/remark/src/rehype-collect-headings.ts
+++ b/packages/markdown/remark/src/rehype-collect-headings.ts
@@ -40,8 +40,8 @@ export function rehypeHeadingIds(): ReturnType {
return;
}
}
- if (rawNodeTypes.has(child.type)) {
- if (isMDX || codeTagNames.has(parent.tagName)) {
+ if (rawNodeTypes.has(child.type) && 'value' in child) {
+ if (isMDX || ('tagName' in parent && codeTagNames.has(parent.tagName))) {
let value = child.value;
if (isMdxTextExpression(child) && frontmatter) {
const frontmatterPath = getMdxFrontmatterVariablePath(child);
diff --git a/packages/integrations/mdx/test/units/mdx-compilation.test.ts b/packages/markdown/remark/test/mdx-compilation.test.ts
similarity index 90%
rename from packages/integrations/mdx/test/units/mdx-compilation.test.ts
rename to packages/markdown/remark/test/mdx-compilation.test.ts
index 0117e6fcc03c..4a91f21c2251 100644
--- a/packages/integrations/mdx/test/units/mdx-compilation.test.ts
+++ b/packages/markdown/remark/test/mdx-compilation.test.ts
@@ -1,19 +1,22 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
-import { rehypeHeadingIds } from '@astrojs/markdown-remark';
import { compile as _compile, type CompileOptions, nodeTypes } from '@mdx-js/mdx';
+import type * as estree from 'estree';
import { visit as estreeVisit } from 'estree-util-visit';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import remarkSmartypants from 'remark-smartypants';
+import type * as unified from 'unified';
import { visit } from 'unist-util-visit';
-import { ignoreStringPlugins } from '../../dist/utils.js';
import {
- SpyLogger,
- type RecmaPlugin,
+ rehypeHeadingIds,
type RehypePlugin,
type RemarkPlugin,
-} from '../test-utils.ts';
+ type RemarkPlugins,
+} from '../dist/index.js';
+import { filterStringPlugins } from '../dist/mdx/utils.js';
+
+type RecmaPlugin = unified.Plugin;
/**
* Compile MDX to JSX string output for inspection.
@@ -88,16 +91,16 @@ describe('MDX SmartyPants plugin', () => {
});
// SmartyPants converts straight quotes to curly and -- to em dash
assert.ok(
- code.includes('\u201C') || code.includes('\u201D') || code.includes('\u2014'),
+ code.includes('“') || code.includes('”') || code.includes('—'),
'SmartyPants should convert quotes or dashes to typographic equivalents',
);
});
it('does not convert quotes without SmartyPants', async () => {
const code = await compile('> "Smartypants" is -- awesome');
- // Without SmartyPants, double dashes stay as -- (not converted to em dash \u2014)
+ // Without SmartyPants, double dashes stay as -- (not converted to em dash —)
assert.ok(code.includes('--'), 'Double dashes should remain unconverted');
- assert.ok(!code.includes('\u2014'), 'Em dash should not appear without SmartyPants');
+ assert.ok(!code.includes('—'), 'Em dash should not appear without SmartyPants');
});
});
@@ -259,15 +262,11 @@ describe('MDX heading IDs', () => {
});
describe('MDX string-based plugin filtering', () => {
- it('does not apply string-based remark plugins', async () => {
- // When a string-based plugin is provided, the ignoreStringPlugins
- // function filters it out. We test the filter function directly in utils.test.js.
- // Here we verify that only function plugins affect output.
- const spyLogger = new SpyLogger();
- const logger = spyLogger.forkIntegrationLogger('test-spy');
-
- const plugins = ['remark-toc', () => (tree: unknown) => tree];
- const filtered = ignoreStringPlugins(plugins, logger);
+ it('does not apply string-based plugins', () => {
+ // The MDX compiler cannot resolve string-form plugins, so `filterStringPlugins`
+ // drops them (with a warning) and keeps only function plugins.
+ const fnPlugin = () => (tree: unknown) => tree;
+ const filtered = filterStringPlugins(['remark-toc', fnPlugin] as RemarkPlugins);
assert.equal(filtered.length, 1, 'Should filter out string plugin');
assert.equal(typeof filtered[0], 'function', 'Should keep function plugin');
diff --git a/packages/integrations/mdx/test/units/rehype-optimize-static.test.ts b/packages/markdown/remark/test/mdx-rehype-optimize-static.test.ts
similarity index 96%
rename from packages/integrations/mdx/test/units/rehype-optimize-static.test.ts
rename to packages/markdown/remark/test/mdx-rehype-optimize-static.test.ts
index f93b8d3f3338..02ec671ab52c 100644
--- a/packages/integrations/mdx/test/units/rehype-optimize-static.test.ts
+++ b/packages/markdown/remark/test/mdx-rehype-optimize-static.test.ts
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { compile as _compile, type CompileOptions } from '@mdx-js/mdx';
-import { rehypeOptimizeStatic } from '../../dist/rehype-optimize-static.js';
+import { rehypeOptimizeStatic } from '../dist/mdx/rehype-optimize-static.js';
async function compile(mdxCode: string, options?: Readonly) {
const result = await _compile(mdxCode, {
diff --git a/packages/integrations/mdx/test/units/rehype-plugins.test.ts b/packages/markdown/remark/test/mdx-rehype-plugins.test.ts
similarity index 96%
rename from packages/integrations/mdx/test/units/rehype-plugins.test.ts
rename to packages/markdown/remark/test/mdx-rehype-plugins.test.ts
index 89e65acaa53b..3d28ad7d4aa9 100644
--- a/packages/integrations/mdx/test/units/rehype-plugins.test.ts
+++ b/packages/markdown/remark/test/mdx-rehype-plugins.test.ts
@@ -2,8 +2,8 @@ import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import type * as hast from 'hast';
import { VFile } from 'vfile';
-import { rehypeInjectHeadingsExport } from '../../dist/rehype-collect-headings.js';
-import rehypeMetaString from '../../dist/rehype-meta-string.js';
+import { rehypeInjectHeadingsExport } from '../dist/mdx/rehype-inject-headings-export.js';
+import rehypeMetaString from '../dist/mdx/rehype-meta-string.js';
describe('rehypeMetaString', () => {
function createCodeNode(meta: string | undefined): hast.Element {
diff --git a/packages/markdown/remark/test/mdx-utils.test.ts b/packages/markdown/remark/test/mdx-utils.test.ts
new file mode 100644
index 000000000000..e1a5458cfba8
--- /dev/null
+++ b/packages/markdown/remark/test/mdx-utils.test.ts
@@ -0,0 +1,93 @@
+import assert from 'node:assert/strict';
+import { afterEach, beforeEach, describe, it } from 'node:test';
+import type { RemarkPlugins } from '../dist/index.js';
+import { filterStringPlugins, jsToTreeNode } from '../dist/mdx/utils.js';
+
+describe('mdx utils', () => {
+ describe('jsToTreeNode', () => {
+ it('parses a simple export statement', () => {
+ const node = jsToTreeNode('export const x = 1;');
+ const estree = node.data!.estree!;
+ assert.equal(node.type, 'mdxjsEsm');
+ assert.equal(estree.type, 'Program');
+ assert.equal(estree.sourceType, 'module');
+ assert.ok(estree.body.length > 0);
+ });
+
+ it('parses an import statement', () => {
+ const node = jsToTreeNode("import foo from 'bar';");
+ assert.equal(node.type, 'mdxjsEsm');
+ assert.equal(node.data!.estree!.body[0].type, 'ImportDeclaration');
+ });
+
+ it('parses a function export', () => {
+ const node = jsToTreeNode('export function getHeadings() { return []; }');
+ assert.equal(node.type, 'mdxjsEsm');
+ const decl = node.data!.estree!.body[0];
+ assert.equal(decl.type, 'ExportNamedDeclaration');
+ });
+
+ it('throws on invalid JS', () => {
+ assert.throws(() => jsToTreeNode('this is not valid javascript {{{'), {
+ name: 'SyntaxError',
+ });
+ });
+ });
+
+ describe('filterStringPlugins', () => {
+ let warnings: unknown[][];
+ const originalWarn = console.warn;
+
+ beforeEach(() => {
+ warnings = [];
+ console.warn = (...args: unknown[]) => {
+ warnings.push(args);
+ };
+ });
+ afterEach(() => {
+ console.warn = originalWarn;
+ });
+
+ it('returns function plugins unchanged', () => {
+ const plugin1 = () => {};
+ const plugin2 = () => {};
+ const result = filterStringPlugins([plugin1, plugin2] as RemarkPlugins);
+ assert.equal(result.length, 2);
+ assert.equal(result[0], plugin1);
+ assert.equal(result[1], plugin2);
+ assert.equal(warnings.length, 0);
+ });
+
+ it('filters out string-based plugins', () => {
+ const fnPlugin = () => {};
+ const result = filterStringPlugins(['remark-toc', fnPlugin] as RemarkPlugins);
+ assert.equal(result.length, 1);
+ assert.equal(result[0], fnPlugin);
+ });
+
+ it('filters out array-based string plugins [string, options]', () => {
+ const fnPlugin = () => {};
+ const result = filterStringPlugins([['remark-toc', {}], fnPlugin] as RemarkPlugins);
+ assert.equal(result.length, 1);
+ assert.equal(result[0], fnPlugin);
+ });
+
+ it('logs warnings for string plugins', () => {
+ filterStringPlugins(['remark-toc', ['rehype-highlight', {}]] as RemarkPlugins);
+ // One warning per string plugin + one summary warning
+ assert.equal(warnings.length, 3);
+ });
+
+ it('returns empty array for all string plugins', () => {
+ const result = filterStringPlugins(['remark-toc'] as RemarkPlugins);
+ assert.equal(result.length, 0);
+ });
+
+ it('handles array-based function plugins [function, options]', () => {
+ const fnPlugin = () => {};
+ const result = filterStringPlugins([[fnPlugin, { option: true }]] as RemarkPlugins);
+ assert.equal(result.length, 1);
+ assert.equal(warnings.length, 0);
+ });
+ });
+});
diff --git a/packages/markdown/satteri/package.json b/packages/markdown/satteri/package.json
index 3e4980235426..ac6e289ba7de 100644
--- a/packages/markdown/satteri/package.json
+++ b/packages/markdown/satteri/package.json
@@ -32,6 +32,8 @@
"satteri": "^0.9.1"
},
"devDependencies": {
+ "@types/estree": "^1.0.8",
+ "@types/hast": "^3.0.4",
"astro-scripts": "workspace:*"
},
"publishConfig": {
diff --git a/packages/integrations/mdx/src/satteri/charset.ts b/packages/markdown/satteri/src/mdx/charset.ts
similarity index 100%
rename from packages/integrations/mdx/src/satteri/charset.ts
rename to packages/markdown/satteri/src/mdx/charset.ts
diff --git a/packages/integrations/mdx/src/satteri/index.ts b/packages/markdown/satteri/src/mdx/create-processor.ts
similarity index 74%
rename from packages/integrations/mdx/src/satteri/index.ts
rename to packages/markdown/satteri/src/mdx/create-processor.ts
index 28ab50788fc4..5e49481db74f 100644
--- a/packages/integrations/mdx/src/satteri/index.ts
+++ b/packages/markdown/satteri/src/mdx/create-processor.ts
@@ -1,24 +1,32 @@
import { pathToFileURL } from 'node:url';
import { isFrontmatterValid } from '@astrojs/internal-helpers/frontmatter';
-import type { MarkdownHeading } from '@astrojs/internal-helpers/markdown';
-import type { SatteriAstroData, SatteriResolvedOptions } from '@astrojs/markdown-satteri';
+import type {
+ AstroMarkdownOptions,
+ MarkdownHeading,
+ MdxRendererOptions,
+ MdxRenderResult,
+} from '@astrojs/internal-helpers/markdown';
import {
- satteriCollectImagesPlugin,
- satteriCreateHighlightFn,
- satteriHeadingIdsPlugin,
- satteriHighlightPlugin,
-} from '@astrojs/markdown-satteri';
-import { createDefaultAstroMetadata } from 'astro/markdown';
+ ASTRO_IMAGE_IMPORT,
+ createDefaultAstroMetadata,
+ USES_ASTRO_IMAGE_FLAG,
+} from '@astrojs/internal-helpers/mdx';
import {
mdxToJs,
type HastPluginDefinition,
type MdastPluginDefinition,
type MdxCompileOptions,
} from 'satteri';
-import { ASTRO_IMAGE_IMPORT, USES_ASTRO_IMAGE_FLAG } from '../image-constants.js';
-import type { ResolvedMdxOptions } from '../index.js';
+import type { SatteriResolvedOptions } from '../processor.js';
+import {
+ createCollectImagesPlugin,
+ createHeadingIdsPlugin,
+ createHighlightFn,
+ createHighlightPlugin,
+ type SatteriAstroData,
+} from '../satteri-processor.js';
import { shouldAddCharset } from './charset.js';
-import { type AstroMetadata, createAstroMetadataPlugin } from './hast-astro-metadata.js';
+import { createAstroMetadataPlugin } from './hast-astro-metadata.js';
import { createImageToComponentPlugin, type ImageImportInfo } from './hast-images-to-component.js';
type HighlightFn = (code: string, lang: string, meta?: string) => Promise;
@@ -32,29 +40,18 @@ declare module 'hast' {
}
}
-export interface CompileMdxResult {
- code: string;
- astroMetadata: AstroMetadata;
-}
-
-interface CreateMdxProcessorContext {
- srcDir: URL;
-}
-
-export function createMdxProcessor(
- mdxOptions: ResolvedMdxOptions,
+export function createSatteriMdxProcessor(
+ shared: AstroMarkdownOptions,
+ mdx: MdxRendererOptions,
satteriOptions: SatteriResolvedOptions,
- ctx: CreateMdxProcessorContext,
) {
let highlightFn: HighlightFn | undefined;
let initPromise: Promise | undefined;
function initHighlighter() {
- initPromise = satteriCreateHighlightFn(mdxOptions.syntaxHighlight, mdxOptions.shikiConfig).then(
- (fn) => {
- highlightFn = fn;
- },
- );
+ initPromise = createHighlightFn(shared.syntaxHighlight, shared.shikiConfig).then((fn) => {
+ highlightFn = fn;
+ });
}
return {
@@ -62,7 +59,7 @@ export function createMdxProcessor(
content: string,
filePath: string,
frontmatter: Record,
- ): Promise {
+ ): Promise {
if (!highlightFn && !initPromise) {
initHighlighter();
}
@@ -75,8 +72,8 @@ export function createMdxProcessor(
remoteImagePaths: new Set(),
};
- const collectImages = satteriCollectImagesPlugin();
- const headingIds = satteriHeadingIdsPlugin();
+ const collectImages = createCollectImagesPlugin();
+ const headingIds = createHeadingIdsPlugin();
const astroMeta = createAstroMetadataPlugin(filePath);
const imageImportInfo: ImageImportInfo = {
importedImages: new Map(),
@@ -84,7 +81,7 @@ export function createMdxProcessor(
};
const imageToComponent = createImageToComponentPlugin(imageImportInfo);
- const syntaxHighlight = mdxOptions.syntaxHighlight;
+ const syntaxHighlight = shared.syntaxHighlight;
const excludeLangs =
typeof syntaxHighlight === 'object' ? syntaxHighlight.excludeLangs : undefined;
@@ -98,7 +95,7 @@ export function createMdxProcessor(
if (highlightFn) {
// `mdx: true` wraps the highlighted HTML in a JSX `` node
// rather than a raw HTML node, since the Sätteri pipeline is compiling to JSX.
- hastPlugins.push(satteriHighlightPlugin(highlightFn, excludeLangs, { mdx: true }));
+ hastPlugins.push(createHighlightPlugin(highlightFn, excludeLangs, { mdx: true }));
}
if (satteriOptions.hastPlugins.length) {
hastPlugins.push(...satteriOptions.hastPlugins);
@@ -106,11 +103,9 @@ export function createMdxProcessor(
hastPlugins.push(imageToComponent, headingIds, astroMeta);
let optimizeStatic: MdxCompileOptions['optimizeStatic'];
- if (mdxOptions.optimize) {
+ if (mdx.optimize) {
const ignoreElements =
- typeof mdxOptions.optimize === 'object'
- ? mdxOptions.optimize.ignoreElementNames
- : undefined;
+ typeof mdx.optimize === 'object' ? mdx.optimize.ignoreElementNames : undefined;
optimizeStatic = {
component: 'Fragment',
@@ -123,16 +118,12 @@ export function createMdxProcessor(
mdastPlugins: allMdastPlugins,
hastPlugins,
optimizeStatic,
+ // The processor's own `features` win; the deprecated top-level `markdown.gfm` /
+ // `markdown.smartypants` act as fallback defaults, mirroring the `.md` pipeline.
features: {
+ gfm: (shared.gfm ?? true) !== false,
+ smartPunctuation: (shared.smartypants ?? true) !== false,
...satteriOptions.features,
- // `mdxOptions.gfm`/`smartypants` are always boolean-shaped; skip the override when
- // satteri's feature is an object so granular config isn't clobbered.
- ...(typeof satteriOptions.features.gfm === 'object'
- ? {}
- : { gfm: mdxOptions.gfm !== false }),
- ...(typeof satteriOptions.features.smartPunctuation === 'object'
- ? {}
- : { smartPunctuation: mdxOptions.smartypants !== false }),
},
fileURL: pathToFileURL(filePath),
jsxImportSource: 'astro',
@@ -189,7 +180,7 @@ export default function MDXContent(props) {
children: content,
});
}`;
- } else if (shouldAddCharset(content, filePath, ctx.srcDir)) {
+ } else if (shouldAddCharset(content, filePath, mdx.srcDir)) {
// Default MDX pages without a layout to UTF-8 so users don't have to think about it.
compiled = compiled.replace(/^function MDXContent\(/m, 'function __OriginalMDXContent__(');
compiled += `
@@ -206,6 +197,7 @@ export default function MDXContent(props) {
return {
code: compiled,
+ map: null,
astroMetadata,
};
},
diff --git a/packages/integrations/mdx/src/satteri/hast-astro-metadata.ts b/packages/markdown/satteri/src/mdx/hast-astro-metadata.ts
similarity index 98%
rename from packages/integrations/mdx/src/satteri/hast-astro-metadata.ts
rename to packages/markdown/satteri/src/mdx/hast-astro-metadata.ts
index c9ee8c627207..b1113fb9d676 100644
--- a/packages/integrations/mdx/src/satteri/hast-astro-metadata.ts
+++ b/packages/markdown/satteri/src/mdx/hast-astro-metadata.ts
@@ -1,5 +1,5 @@
import type { AstroMetadata } from '@astrojs/internal-helpers/markdown';
-import { createDefaultAstroMetadata, resolvePath } from 'astro/markdown';
+import { createDefaultAstroMetadata, resolvePath } from '@astrojs/internal-helpers/mdx';
import type { Identifier, Literal } from 'estree';
import {
defineHastPlugin,
diff --git a/packages/integrations/mdx/src/satteri/hast-images-to-component.ts b/packages/markdown/satteri/src/mdx/hast-images-to-component.ts
similarity index 97%
rename from packages/integrations/mdx/src/satteri/hast-images-to-component.ts
rename to packages/markdown/satteri/src/mdx/hast-images-to-component.ts
index a7e7857a088a..b7073db0a19e 100644
--- a/packages/integrations/mdx/src/satteri/hast-images-to-component.ts
+++ b/packages/markdown/satteri/src/mdx/hast-images-to-component.ts
@@ -4,7 +4,7 @@ import {
type HastPluginDefinition,
type MdxJsxAttributeNode,
} from 'satteri';
-import { ASTRO_IMAGE_ELEMENT } from '../image-constants.js';
+import { ASTRO_IMAGE_ELEMENT } from '@astrojs/internal-helpers/mdx';
import { makeJsxAttr, makeJsxExprAttr } from './jsx-utils.js';
export interface ImageImportInfo {
diff --git a/packages/integrations/mdx/src/satteri/jsx-utils.ts b/packages/markdown/satteri/src/mdx/jsx-utils.ts
similarity index 100%
rename from packages/integrations/mdx/src/satteri/jsx-utils.ts
rename to packages/markdown/satteri/src/mdx/jsx-utils.ts
diff --git a/packages/markdown/satteri/src/processor.ts b/packages/markdown/satteri/src/processor.ts
index 517a36c9fa89..ee8665d8f62e 100644
--- a/packages/markdown/satteri/src/processor.ts
+++ b/packages/markdown/satteri/src/processor.ts
@@ -52,6 +52,11 @@ export function satteri(
features: processor.options.features,
});
},
+ async createMdxRenderer(shared, mdx) {
+ // Lazy import so the MDX/JSX compile path isn't pulled in for `.md`-only projects.
+ const { createSatteriMdxProcessor } = await import('./mdx/create-processor.js');
+ return createSatteriMdxProcessor(shared, mdx, processor.options);
+ },
};
return processor;
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b7c70c6fd29d..2ba3c4b89475 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4939,49 +4939,19 @@ importers:
'@astrojs/internal-helpers':
specifier: workspace:*
version: link:../../internal-helpers
- '@astrojs/markdown-remark':
+ '@astrojs/markdown-satteri':
specifier: workspace:*
- version: link:../../markdown/remark
- '@mdx-js/mdx':
- specifier: ^3.1.1
- version: 3.1.1
- acorn:
- specifier: ^8.16.0
- version: 8.16.0
+ version: link:../../markdown/satteri
es-module-lexer:
specifier: ^2.0.0
version: 2.0.0
- estree-util-visit:
- specifier: ^2.0.0
- version: 2.0.0
- hast-util-to-html:
- specifier: ^9.0.5
- version: 9.0.5
- piccolore:
- specifier: ^0.1.3
- version: 0.1.3
- rehype-raw:
- specifier: ^7.0.0
- version: 7.0.0
- remark-gfm:
- specifier: ^4.0.1
- version: 4.0.1
- remark-smartypants:
- specifier: ^3.0.2
- version: 3.0.2
- source-map:
- specifier: ^0.7.6
- version: 0.7.6
- unist-util-visit:
- specifier: ^5.1.0
- version: 5.1.0
- vfile:
- specifier: ^6.0.3
- version: 6.0.3
devDependencies:
- '@astrojs/markdown-satteri':
+ '@astrojs/markdown-remark':
specifier: workspace:*
- version: link:../../markdown/satteri
+ version: link:../../markdown/remark
+ '@mdx-js/mdx':
+ specifier: ^3.1.1
+ version: 3.1.1
'@shikijs/rehype':
specifier: ^4.0.2
version: 4.0.2
@@ -5039,6 +5009,9 @@ importers:
unified:
specifier: ^11.0.5
version: 11.0.5
+ unist-util-visit:
+ specifier: ^5.1.0
+ version: 5.1.0
vite:
specifier: ^8.0.13
version: 8.1.0(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.98.0)(tsx@4.22.3)(yaml@2.9.0)
@@ -6566,12 +6539,24 @@ importers:
'@astrojs/prism':
specifier: workspace:*
version: link:../../astro-prism
+ '@mdx-js/mdx':
+ specifier: ^3.1.1
+ version: 3.1.1
+ acorn:
+ specifier: ^8.16.0
+ version: 8.16.0
+ estree-util-visit:
+ specifier: ^2.0.0
+ version: 2.0.0
github-slugger:
specifier: ^2.0.0
version: 2.0.0
hast-util-from-html:
specifier: ^2.0.3
version: 2.0.3
+ hast-util-to-html:
+ specifier: ^9.0.5
+ version: 9.0.5
hast-util-to-text:
specifier: ^4.0.2
version: 4.0.2
@@ -6596,6 +6581,9 @@ importers:
remark-smartypants:
specifier: ^3.0.2
version: 3.0.2
+ source-map:
+ specifier: ^0.7.6
+ version: 0.7.6
unified:
specifier: ^11.0.5
version: 11.0.5
@@ -6630,9 +6618,15 @@ importers:
esbuild:
specifier: ^0.28.0
version: 0.28.0
+ mdast-util-mdx:
+ specifier: ^3.0.0
+ version: 3.0.0
mdast-util-mdx-expression:
specifier: ^2.0.1
version: 2.0.1
+ mdast-util-mdx-jsx:
+ specifier: ^3.2.0
+ version: 3.2.0
shiki:
specifier: ^4.0.0
version: 4.0.2
@@ -6652,6 +6646,12 @@ importers:
specifier: ^0.9.1
version: 0.9.1
devDependencies:
+ '@types/estree':
+ specifier: ^1.0.8
+ version: 1.0.8
+ '@types/hast':
+ specifier: ^3.0.4
+ version: 3.0.4
astro-scripts:
specifier: workspace:*
version: link:../../../scripts