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