aboutsummaryrefslogtreecommitdiff
path: root/packages/integrations/mdx/test
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/mdx/test')
-rw-r--r--packages/integrations/mdx/test/css-head-mdx.test.js101
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/package.json10
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/BaseHead.astro11
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/BasicBlock.astro14
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/GenericComponent.astro1
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/HelloWorld.astro11
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/MDXWrapper.astro9
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro3
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro3
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro8
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro6
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/config.ts18
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/blog/_styles.css3
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/blog/using-mdx.mdx6
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/posts/test.mdx5
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/posts/using-component.mdx13
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro18
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/DocumentLayout.astro15
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/One.astro15
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/Three.astro6
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/Two.astro6
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro17
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/componentwithtext.mdx12
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/indexOne.astro10
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/indexThree.astro10
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/indexTwo.astro10
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/noLayoutWithComponent.mdx22
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro18
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro17
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/testOne.mdx15
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/testThree.mdx15
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/testTwo.mdx15
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css3
-rw-r--r--packages/integrations/mdx/test/fixtures/css-head-mdx/src/test.mdx14
-rw-r--r--packages/integrations/mdx/test/fixtures/image-remark-imgattr/astro.config.mjs8
-rw-r--r--packages/integrations/mdx/test/fixtures/image-remark-imgattr/package.json12
-rw-r--r--packages/integrations/mdx/test/fixtures/image-remark-imgattr/remarkPlugin.js22
-rw-r--r--packages/integrations/mdx/test/fixtures/image-remark-imgattr/src/assets/penguin2.jpgbin0 -> 11677 bytes
-rw-r--r--packages/integrations/mdx/test/fixtures/image-remark-imgattr/src/pages/index.mdx1
-rw-r--r--packages/integrations/mdx/test/fixtures/invalid-mdx-component/src/pages/invalid-content.mdx7
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-astro-markdown-remarkRehype/src/pages/index.mdx5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-component/src/components/Test.mdx3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-component/src/components/WithFragment.mdx3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-component/src/pages/glob.astro20
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-component/src/pages/index.astro5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-component/src/pages/w-fragment.astro5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Em.astro7
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-escape/src/components/P.astro1
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Title.astro1
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/html-tag.mdx5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/index.mdx13
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/astro.config.mjs12
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/package.json12
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/layouts/Base.astro17
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/markdown-plugins.mjs27
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/glob.json.js6
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-1.mdx8
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-2.mdx24
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/layouts/Base.astro38
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/glob.json.js6
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/index.mdx10
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/with-headings.mdx7
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/pages.json.js8
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/test-with-frontmatter.mdx45
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/test-with-jsx-expressions.mdx8
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/test.mdx9
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/content/1.mdx5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/pages/[slug].astro34
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/astro.config.ts13
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/package.json10
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/public/favicon.svg9
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/assets/houston in space.webpbin0 -> 3728 bytes
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/assets/houston.webpbin0 -> 3728 bytes
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/components/Component.mdx5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/components/MyImage.astro25
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/content/blog/entry.mdx5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/content/config.ts5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/pages/content-collection.astro19
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/pages/esm-import.astro16
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/pages/houston.pngbin0 -> 160915 bytes
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/pages/index.mdx17
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/pages/no-image.mdx1
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/pages/relative/houston.pngbin0 -> 160915 bytes
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/src/pages/with-components.mdx9
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-images/tsconfig.json11
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-infinite-loop/astro.config.ts6
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-infinite-loop/package.json11
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-infinite-loop/src/components/Test.js3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-infinite-loop/src/pages/doc.mdx6
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-infinite-loop/src/pages/index.astro5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-math/src/pages/mathjax.mdx5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-namespace/astro.config.mjs6
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-namespace/package.json11
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-namespace/src/components/Component.jsx6
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-namespace/src/pages/object.mdx3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-namespace/src/pages/star.mdx3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-optimize/astro.config.mjs37
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-optimize/package.json8
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-optimize/src/components/Blockquote.astro3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-optimize/src/components/Strong.astro3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-optimize/src/pages/_imported.mdx3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-optimize/src/pages/import.astro15
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-optimize/src/pages/index.mdx15
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-page/astro.config.ts5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-page/package.json10
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-page/src/layouts/EncodingLayout.astro1
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-page/src/pages/chinese-encoding-layout-frontmatter.mdx7
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-page/src/pages/chinese-encoding-layout-manual.mdx12
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-page/src/pages/chinese-encoding.mdx3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-page/src/pages/index.mdx3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-page/src/styles.css3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plugins/src/pages/with-plugins.mdx25
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/astro.config.mjs6
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/package.json11
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/components/BrokenComponent.jsx8
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/content/config.js12
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/content/files/file.mdx4
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/pages/broken.astro9
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plus-react/astro.config.mjs6
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plus-react/package.json11
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plus-react/src/components/Component.jsx8
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plus-react/src/pages/index.astro11
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plus-react/src/pages/inline-component.mdx5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-plus-react/src/pages/post.mdx3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-script-style-raw/src/pages/index.mdx13
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-slots/src/components/Slotted.astro4
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-slots/src/components/Test.mdx15
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/glob.astro11
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/index.astro5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-syntax-hightlighting/src/pages/index.mdx9
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/pages.json.js6
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-1.mdx1
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-2.mdx1
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/with-url-override.mdx3
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/astro.config.mjs16
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/package.json8
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/src/pages/frontmatter.json.js5
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/src/pages/vite-env-vars.mdx46
-rw-r--r--packages/integrations/mdx/test/invalid-mdx-component.test.js39
-rw-r--r--packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.js89
-rw-r--r--packages/integrations/mdx/test/mdx-component.test.js195
-rw-r--r--packages/integrations/mdx/test/mdx-escape.test.js33
-rw-r--r--packages/integrations/mdx/test/mdx-frontmatter-injection.test.js60
-rw-r--r--packages/integrations/mdx/test/mdx-frontmatter.test.js79
-rw-r--r--packages/integrations/mdx/test/mdx-get-headings.test.js200
-rw-r--r--packages/integrations/mdx/test/mdx-get-static-paths.test.js34
-rw-r--r--packages/integrations/mdx/test/mdx-images.test.js82
-rw-r--r--packages/integrations/mdx/test/mdx-infinite-loop.test.js31
-rw-r--r--packages/integrations/mdx/test/mdx-math.test.js73
-rw-r--r--packages/integrations/mdx/test/mdx-namespace.test.js84
-rw-r--r--packages/integrations/mdx/test/mdx-optimize.test.js61
-rw-r--r--packages/integrations/mdx/test/mdx-page.test.js109
-rw-r--r--packages/integrations/mdx/test/mdx-plugins.test.js315
-rw-r--r--packages/integrations/mdx/test/mdx-plus-react-errors.test.js33
-rw-r--r--packages/integrations/mdx/test/mdx-plus-react.test.js55
-rw-r--r--packages/integrations/mdx/test/mdx-script-style-raw.test.js75
-rw-r--r--packages/integrations/mdx/test/mdx-slots.test.js125
-rw-r--r--packages/integrations/mdx/test/mdx-syntax-highlighting.test.js146
-rw-r--r--packages/integrations/mdx/test/mdx-url-export.test.js29
-rw-r--r--packages/integrations/mdx/test/mdx-vite-env-vars.test.js65
-rw-r--r--packages/integrations/mdx/test/remark-imgattr.test.js49
-rw-r--r--packages/integrations/mdx/test/units/rehype-optimize-static.test.js89
162 files changed, 3604 insertions, 0 deletions
diff --git a/packages/integrations/mdx/test/css-head-mdx.test.js b/packages/integrations/mdx/test/css-head-mdx.test.js
new file mode 100644
index 000000000..96ee7c900
--- /dev/null
+++ b/packages/integrations/mdx/test/css-head-mdx.test.js
@@ -0,0 +1,101 @@
+import mdx from '@astrojs/mdx';
+
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import * as cheerio from 'cheerio';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+describe('Head injection w/ MDX', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/css-head-mdx/', import.meta.url),
+ integrations: [mdx()],
+ // test suite was authored when inlineStylesheets defaulted to never
+ build: { inlineStylesheets: 'never' },
+ });
+ });
+
+ describe('build', () => {
+ before(async () => {
+ await fixture.build();
+ });
+
+ it('injects content styles into head', async () => {
+ const html = await fixture.readFile('/indexThree/index.html');
+ const { document } = parseHTML(html);
+
+ const links = document.querySelectorAll('head link[rel=stylesheet]');
+ assert.equal(links.length, 1);
+
+ const scripts = document.querySelectorAll('script[type=module]');
+ assert.equal(scripts.length, 1);
+ });
+
+ it('injects into the head for content collections', async () => {
+ const html = await fixture.readFile('/posts/test/index.html');
+ const { document } = parseHTML(html);
+
+ const links = document.querySelectorAll('head link[rel=stylesheet]');
+ assert.equal(links.length, 1);
+ });
+
+ it('injects content from a component using Content#render()', async () => {
+ const html = await fixture.readFile('/DirectContentUsage/index.html');
+ const { document } = parseHTML(html);
+
+ const links = document.querySelectorAll('head link[rel=stylesheet]');
+ assert.equal(links.length, 1);
+
+ const scripts = document.querySelectorAll('script[type=module]');
+ assert.equal(scripts.length, 1);
+ });
+
+ it('Using component using slots.render() API', async () => {
+ const html = await fixture.readFile('/remote/index.html');
+ const { document } = parseHTML(html);
+
+ const links = document.querySelectorAll('head link[rel=stylesheet]');
+ assert.equal(links.length, 1);
+ });
+
+ it('Using component but no layout', async () => {
+ const html = await fixture.readFile('/noLayoutWithComponent/index.html');
+ // Using cheerio here because linkedom doesn't support head tag injection
+ const $ = cheerio.load(html);
+
+ const headLinks = $('head link[rel=stylesheet]');
+ assert.equal(headLinks.length, 1);
+
+ const bodyLinks = $('body link[rel=stylesheet]');
+ assert.equal(bodyLinks.length, 0);
+ });
+
+ it('JSX component rendering Astro children within head buffering phase', async () => {
+ const html = await fixture.readFile('/posts/using-component/index.html');
+ // Using cheerio here because linkedom doesn't support head tag injection
+ const $ = cheerio.load(html);
+
+ const headLinks = $('head link[rel=stylesheet]');
+ assert.equal(headLinks.length, 1);
+
+ const bodyLinks = $('body link[rel=stylesheet]');
+ assert.equal(bodyLinks.length, 0);
+ });
+
+ it('Injection caused by delayed slots', async () => {
+ const html = await fixture.readFile('/componentwithtext/index.html');
+
+ // Using cheerio here because linkedom doesn't support head tag injection
+ const $ = cheerio.load(html);
+
+ const headLinks = $('head link[rel=stylesheet]');
+ assert.equal(headLinks.length, 1);
+
+ const bodyLinks = $('body link[rel=stylesheet]');
+ assert.equal(bodyLinks.length, 0);
+ });
+ });
+});
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/package.json b/packages/integrations/mdx/test/fixtures/css-head-mdx/package.json
new file mode 100644
index 000000000..1b62bf940
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@test/mdx-css-head-mdx",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/mdx": "workspace:*",
+ "astro": "workspace:*",
+ "astro-remote": "0.3.3"
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/BaseHead.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/BaseHead.astro
new file mode 100644
index 000000000..19f517789
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/BaseHead.astro
@@ -0,0 +1,11 @@
+---
+const { title } = Astro.props;
+---
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width" />
+<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+<meta name="generator" content={Astro.generator} />
+<title>{title}</title>
+<style is:global>
+ @import "../styles/global.css";
+</style>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/BasicBlock.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/BasicBlock.astro
new file mode 100644
index 000000000..95660b6b9
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/BasicBlock.astro
@@ -0,0 +1,14 @@
+---
+const { inlineStyle, title, display = 'horizontal' } = Astro.props;
+const lineEnding = display === 'horizontal' ? ', ' : '<br>';
+---
+
+{title && <h2 set:html={title} />}
+<address style={inlineStyle}>
+ <span class="name">some name</span><Fragment set:html={lineEnding} />
+ line 1<Fragment set:html={lineEnding} />
+ line 2<Fragment set:html={lineEnding} />
+ line 3<Fragment set:html={lineEnding} />
+ line 4<Fragment set:html={lineEnding} />
+ line 5
+</address>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/GenericComponent.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/GenericComponent.astro
new file mode 100644
index 000000000..ebcd3ff35
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/GenericComponent.astro
@@ -0,0 +1 @@
+<span>just a generic component</span>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/HelloWorld.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/HelloWorld.astro
new file mode 100644
index 000000000..ee8084b46
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/HelloWorld.astro
@@ -0,0 +1,11 @@
+---
+---
+
+<h3>Hello world!!</h3>
+<slot />
+
+<style>h3 { color: red }</style>
+
+<script>
+console.log('hellooooo')
+</script> \ No newline at end of file
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/MDXWrapper.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/MDXWrapper.astro
new file mode 100644
index 000000000..fbd530e14
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/MDXWrapper.astro
@@ -0,0 +1,9 @@
+---
+import Component from "./GenericComponent.astro";
+---
+
+<div>
+ <slot name="title" />
+ <slot name="intro" class="inline" />
+ <Component />
+</div>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro
new file mode 100644
index 000000000..071e08a12
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/P.astro
@@ -0,0 +1,3 @@
+<p>
+ <slot />
+</p>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro
new file mode 100644
index 000000000..a0bd6e1f1
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/SmallCaps.astro
@@ -0,0 +1,3 @@
+---
+---
+<span style={{fontVariant: "small-caps"}}><slot /></span>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro
new file mode 100644
index 000000000..3ef9d7639
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/UsingMdx.astro
@@ -0,0 +1,8 @@
+---
+import { getEntry, render } from 'astro:content';
+
+const launchWeek = await getEntry('blog', 'using-mdx');
+const { Content } = await render(launchWeek);
+---
+
+<Content />
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro
new file mode 100644
index 000000000..0b8c4445f
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/components/WithHoistedScripts.astro
@@ -0,0 +1,6 @@
+---
+---
+
+<script>
+ console.log('hoisted')
+ </script>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/config.ts b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/config.ts
new file mode 100644
index 000000000..2c8944d51
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/content/config.ts
@@ -0,0 +1,18 @@
+import { defineCollection } from "astro:content";
+import { glob } from "astro/loaders"
+
+const posts = defineCollection({
+ loader: glob({
+ pattern: "*.mdx",
+ base: "src/data/posts",
+ })
+});
+
+const blog = defineCollection({
+ loader: glob({
+ pattern: "*.mdx",
+ base: "src/data/blog",
+ })
+});
+
+export const collections = { posts, blog };
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/blog/_styles.css b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/blog/_styles.css
new file mode 100644
index 000000000..1379b29c0
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/blog/_styles.css
@@ -0,0 +1,3 @@
+body {
+ color: red !important;
+}
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/blog/using-mdx.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/blog/using-mdx.mdx
new file mode 100644
index 000000000..917fc3331
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/blog/using-mdx.mdx
@@ -0,0 +1,6 @@
+import './_styles.css';
+import WithHoistedScripts from '../../components/WithHoistedScripts.astro';
+
+# Using mdx
+
+<WithHoistedScripts />
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/posts/test.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/posts/test.mdx
new file mode 100644
index 000000000..0bb1153ca
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/posts/test.mdx
@@ -0,0 +1,5 @@
+---
+title: Testing
+---
+
+<SmallCaps>A test file</SmallCaps>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/posts/using-component.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/posts/using-component.mdx
new file mode 100644
index 000000000..fa550fb04
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/data/posts/using-component.mdx
@@ -0,0 +1,13 @@
+---
+title: testing
+---
+import MDXWrapper from "../../components/MDXWrapper.astro";
+
+<MDXWrapper>
+ <h1 slot="title">
+ testing
+ </h1>
+ <div slot="intro">
+ Intro
+ </div>
+</MDXWrapper>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro
new file mode 100644
index 000000000..1e4b6a6f8
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/ContentLayout.astro
@@ -0,0 +1,18 @@
+---
+import BaseHead from "../components/BaseHead.astro";
+interface Props {
+ title: string;
+}
+
+const { title } = Astro.props;
+---
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <BaseHead title={title} />
+ </head>
+ <body>
+ <slot />
+ </body>
+</html>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/DocumentLayout.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/DocumentLayout.astro
new file mode 100644
index 000000000..a09a1c218
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/DocumentLayout.astro
@@ -0,0 +1,15 @@
+---
+// Extend the BaseLayout, adding space for a banner at the top of the page
+// after the main heading, then the detail for the actual page
+import ContentLayout from './ContentLayout.astro';
+const { frontmatter } = Astro.props;
+---
+
+<ContentLayout>
+ <div class="content-container">
+ <article id="main-content" class="pad-z5 flow">
+ <h1 set:html={frontmatter.pageHeading ? frontmatter.pageHeading : frontmatter.title} />
+ <slot />
+ </article>
+ </div>
+</ContentLayout>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/One.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/One.astro
new file mode 100644
index 000000000..b9916e106
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/One.astro
@@ -0,0 +1,15 @@
+---
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <title>Astro</title>
+ </head>
+ <body>
+ <slot />
+ </body>
+</html> \ No newline at end of file
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/Three.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/Three.astro
new file mode 100644
index 000000000..3f0fdfa72
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/Three.astro
@@ -0,0 +1,6 @@
+---
+import Two from './Two.astro'
+---
+<Two>
+<slot />
+</Two> \ No newline at end of file
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/Two.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/Two.astro
new file mode 100644
index 000000000..51f0ca18c
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/layouts/Two.astro
@@ -0,0 +1,6 @@
+---
+import One from './One.astro'
+---
+<One>
+<slot />
+</One> \ No newline at end of file
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro
new file mode 100644
index 000000000..cbf4295a7
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/DirectContentUsage.astro
@@ -0,0 +1,17 @@
+---
+import UsingMdx from '../components/UsingMdx.astro'
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <title>Astro</title>
+ </head>
+ <body>
+ <h1>Astro</h1>
+ <UsingMdx />
+ </body>
+</html>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/componentwithtext.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/componentwithtext.mdx
new file mode 100644
index 000000000..2c6f3032f
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/componentwithtext.mdx
@@ -0,0 +1,12 @@
+---
+layout: ../layouts/DocumentLayout.astro
+title: blah blah
+---
+
+import BasicBlock from '../components/BasicBlock.astro';
+
+Some text for a paragraph.
+
+<BasicBlock title="This causes css in wrong place." />
+
+Some other text.
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/indexOne.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/indexOne.astro
new file mode 100644
index 000000000..f24bf4f3c
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/indexOne.astro
@@ -0,0 +1,10 @@
+---
+import One from '../layouts/One.astro'
+
+import { Content } from '../test.mdx'
+---
+
+<One>
+ <h1>Astro</h1>
+ <Content />
+</One>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/indexThree.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/indexThree.astro
new file mode 100644
index 000000000..99be9677c
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/indexThree.astro
@@ -0,0 +1,10 @@
+---
+import Three from '../layouts/Three.astro'
+
+import { Content } from '../test.mdx'
+---
+
+<Three>
+ <h1>Astro</h1>
+ <Content />
+</Three>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/indexTwo.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/indexTwo.astro
new file mode 100644
index 000000000..af07af926
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/indexTwo.astro
@@ -0,0 +1,10 @@
+---
+import Two from '../layouts/Two.astro'
+
+import { Content } from '../test.mdx'
+---
+
+<Two>
+ <h1>Astro</h1>
+ <Content />
+</Two>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/noLayoutWithComponent.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/noLayoutWithComponent.mdx
new file mode 100644
index 000000000..9d799d4db
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/noLayoutWithComponent.mdx
@@ -0,0 +1,22 @@
+---
+title: 'Lorem'
+description: 'Lorem ipsum dolor sit amet'
+pubDate: 'Jul 02 2022'
+---
+
+import MyComponent from '../components/HelloWorld.astro';
+
+
+## Lorem
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+## Lorem 2
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+<MyComponent />
+
+## Lorem 3
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro
new file mode 100644
index 000000000..13a9e56db
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/posts/[post].astro
@@ -0,0 +1,18 @@
+---
+import { getCollection, render } from 'astro:content';
+import SmallCaps from '../../components/SmallCaps.astro';
+import Layout from '../../layouts/ContentLayout.astro';
+
+export async function getStaticPaths() {
+ const entries = await getCollection('posts');
+ return entries.map(entry => {
+ return { params: { post: entry.id }, props: { entry }};
+ });
+}
+
+const { entry } = Astro.props;
+const { Content } = await render(entry);
+---
+<Layout title="">
+ <Content components={{ SmallCaps }} />
+</Layout>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro
new file mode 100644
index 000000000..7c8000642
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/remote.astro
@@ -0,0 +1,17 @@
+---
+import { Markdown } from 'astro-remote'
+import Paragraph from '../components/P.astro';
+import Layout from '../layouts/One.astro';
+import '../styles/global.css'
+---
+
+<Layout title="Welcome to Astro.">
+ <main>
+ <Markdown
+ components={{
+ p: Paragraph,
+ }}>
+ **Removing p component fixes the problem**
+ </Markdown>
+ </main>
+</Layout>
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/testOne.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/testOne.mdx
new file mode 100644
index 000000000..6874b499f
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/testOne.mdx
@@ -0,0 +1,15 @@
+---
+layout: '../layouts/One.astro'
+title: "hello world"
+publishDate: "2023-01-01"
+---
+
+import HelloWorld from '../components/HelloWorld.astro';
+
+# Test
+
+123
+
+<HelloWorld />
+
+456
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/testThree.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/testThree.mdx
new file mode 100644
index 000000000..b0e55eed2
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/testThree.mdx
@@ -0,0 +1,15 @@
+---
+layout: '../layouts/Three.astro'
+title: "hello world"
+publishDate: "2023-01-01"
+---
+
+import HelloWorld from '../components/HelloWorld.astro';
+
+# Test
+
+123
+
+<HelloWorld />
+
+456
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/testTwo.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/testTwo.mdx
new file mode 100644
index 000000000..9a80ed5f0
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/pages/testTwo.mdx
@@ -0,0 +1,15 @@
+---
+layout: '../layouts/Two.astro'
+title: "hello world"
+publishDate: "2023-01-01"
+---
+
+import HelloWorld from '../components/HelloWorld.astro';
+
+# Test
+
+123
+
+<HelloWorld />
+
+456
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css
new file mode 100644
index 000000000..e1450526f
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/styles/global.css
@@ -0,0 +1,3 @@
+html {
+ font-weight: bolder;
+}
diff --git a/packages/integrations/mdx/test/fixtures/css-head-mdx/src/test.mdx b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/test.mdx
new file mode 100644
index 000000000..c8ecc4daa
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/css-head-mdx/src/test.mdx
@@ -0,0 +1,14 @@
+---
+title: "hello world"
+publishDate: "2023-01-01"
+---
+
+import HelloWorld from './components/HelloWorld.astro';
+
+# Test
+
+123
+
+<HelloWorld />
+
+456
diff --git a/packages/integrations/mdx/test/fixtures/image-remark-imgattr/astro.config.mjs b/packages/integrations/mdx/test/fixtures/image-remark-imgattr/astro.config.mjs
new file mode 100644
index 000000000..7cd97f8b1
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/image-remark-imgattr/astro.config.mjs
@@ -0,0 +1,8 @@
+import mdx from '@astrojs/mdx';
+import { defineConfig } from 'astro/config';
+import plugin from "./remarkPlugin"
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [mdx({remarkPlugins:[plugin]})],
+});
diff --git a/packages/integrations/mdx/test/fixtures/image-remark-imgattr/package.json b/packages/integrations/mdx/test/fixtures/image-remark-imgattr/package.json
new file mode 100644
index 000000000..89868f9f4
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/image-remark-imgattr/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@test/image-remark-imgattr",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/mdx": "workspace:*",
+ "astro": "workspace:*"
+ },
+ "scripts": {
+ "dev": "astro dev"
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/image-remark-imgattr/remarkPlugin.js b/packages/integrations/mdx/test/fixtures/image-remark-imgattr/remarkPlugin.js
new file mode 100644
index 000000000..f42d9ec52
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/image-remark-imgattr/remarkPlugin.js
@@ -0,0 +1,22 @@
+export default function plugin() {
+ return transformer;
+
+ function transformer(tree) {
+ function traverse(node) {
+ if (node.type === "image") {
+ node.data = node.data || {};
+ node.data.hProperties = node.data.hProperties || {};
+ node.data.hProperties.id = "test";
+ node.data.hProperties.width = "300";
+ node.data.hProperties.widths = [300,600];
+ node.data.hProperties.sizes = "(min-width: 600px) 600w, 300w";
+ }
+
+ if (node.children) {
+ node.children.forEach(traverse);
+ }
+ }
+
+ traverse(tree);
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/image-remark-imgattr/src/assets/penguin2.jpg b/packages/integrations/mdx/test/fixtures/image-remark-imgattr/src/assets/penguin2.jpg
new file mode 100644
index 000000000..e859ac3c9
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/image-remark-imgattr/src/assets/penguin2.jpg
Binary files differ
diff --git a/packages/integrations/mdx/test/fixtures/image-remark-imgattr/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/image-remark-imgattr/src/pages/index.mdx
new file mode 100644
index 000000000..e415e505d
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/image-remark-imgattr/src/pages/index.mdx
@@ -0,0 +1 @@
+![alt](../assets/penguin2.jpg)
diff --git a/packages/integrations/mdx/test/fixtures/invalid-mdx-component/src/pages/invalid-content.mdx b/packages/integrations/mdx/test/fixtures/invalid-mdx-component/src/pages/invalid-content.mdx
new file mode 100644
index 000000000..9ebaa695b
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/invalid-mdx-component/src/pages/invalid-content.mdx
@@ -0,0 +1,7 @@
+---
+title: Hello, World
+---
+
+# {A.VALID.JAVASCRIPT.EXPRESSION.THAT.RESULTS.IN.A.RUNTIME.ERROR}
+
+Invalid content in the frontmatter
diff --git a/packages/integrations/mdx/test/fixtures/mdx-astro-markdown-remarkRehype/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-astro-markdown-remarkRehype/src/pages/index.mdx
new file mode 100644
index 000000000..8b8f1d189
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-astro-markdown-remarkRehype/src/pages/index.mdx
@@ -0,0 +1,5 @@
+# Hello world
+
+This[^1] should be visible.
+
+[^1]: And there would be a footnote. \ No newline at end of file
diff --git a/packages/integrations/mdx/test/fixtures/mdx-component/src/components/Test.mdx b/packages/integrations/mdx/test/fixtures/mdx-component/src/components/Test.mdx
new file mode 100644
index 000000000..1c6b33184
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-component/src/components/Test.mdx
@@ -0,0 +1,3 @@
+# Hello component!
+
+<div id="foo">bar</div>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-component/src/components/WithFragment.mdx b/packages/integrations/mdx/test/fixtures/mdx-component/src/components/WithFragment.mdx
new file mode 100644
index 000000000..c6058697e
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-component/src/components/WithFragment.mdx
@@ -0,0 +1,3 @@
+# MDX containing `<Fragment />`
+
+<p><Fragment>bar</Fragment></p>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/glob.astro b/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/glob.astro
new file mode 100644
index 000000000..ab5f417b1
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/glob.astro
@@ -0,0 +1,20 @@
+---
+import { parse } from 'node:path';
+const components = await Astro.glob('../components/*.mdx');
+---
+
+<div data-default-export>
+ {components.map(Component => (
+ <div data-file={parse(Component.file).base}>
+ <Component.default />
+ </div>
+ ))}
+</div>
+
+<div data-content-export>
+ {components.map(({ Content, file }) => (
+ <div data-file={parse(file).base}>
+ <Content />
+ </div>
+ ))}
+</div>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/index.astro b/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/index.astro
new file mode 100644
index 000000000..ed5ae98a3
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/index.astro
@@ -0,0 +1,5 @@
+---
+import Test from '../components/Test.mdx';
+---
+
+<Test />
diff --git a/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/w-fragment.astro b/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/w-fragment.astro
new file mode 100644
index 000000000..d394413f0
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/w-fragment.astro
@@ -0,0 +1,5 @@
+---
+import WithFragment from '../components/WithFragment.mdx';
+---
+
+<WithFragment />
diff --git a/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Em.astro b/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Em.astro
new file mode 100644
index 000000000..8166c0586
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Em.astro
@@ -0,0 +1,7 @@
+<em><slot/></em>
+
+<style>
+ em {
+ color: red;
+ }
+</style>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/P.astro b/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/P.astro
new file mode 100644
index 000000000..e29ac6d8f
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/P.astro
@@ -0,0 +1 @@
+<p><slot /></p>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Title.astro b/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Title.astro
new file mode 100644
index 000000000..333ec04a2
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Title.astro
@@ -0,0 +1 @@
+<h1><slot/></h1>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/html-tag.mdx b/packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/html-tag.mdx
new file mode 100644
index 000000000..e668c0dc7
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/html-tag.mdx
@@ -0,0 +1,5 @@
+import P from '../components/P.astro';
+import Em from '../components/Em.astro';
+
+<P>Render <Em>Me</Em></P>
+<P><Em>Me</Em></P>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/index.mdx
new file mode 100644
index 000000000..d1c6cec9d
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/index.mdx
@@ -0,0 +1,13 @@
+import P from '../components/P.astro';
+import Em from '../components/Em.astro';
+import Title from '../components/Title.astro';
+
+export const components = { p: P, em: Em, h1: Title };
+
+# Hello _there_
+
+# _there_
+
+Hello _there_
+
+_there_
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/astro.config.mjs b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/astro.config.mjs
new file mode 100644
index 000000000..00d8e38f5
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/astro.config.mjs
@@ -0,0 +1,12 @@
+import mdx from '@astrojs/mdx';
+import { defineConfig } from 'astro/config';
+import { rehypeReadingTime, remarkDescription, remarkTitle } from './src/markdown-plugins.mjs';
+
+// https://astro.build/config
+export default defineConfig({
+ site: 'https://astro.build/',
+ integrations: [mdx({
+ remarkPlugins: [remarkTitle, remarkDescription],
+ rehypePlugins: [rehypeReadingTime],
+ })],
+});
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/package.json b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/package.json
new file mode 100644
index 000000000..684063ee5
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@test/mdx-frontmatter-injection",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/mdx": "workspace:*",
+ "astro": "workspace:*",
+ "mdast-util-to-string": "^4.0.0",
+ "reading-time": "^1.5.0",
+ "unist-util-visit": "^5.0.0"
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/layouts/Base.astro b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/layouts/Base.astro
new file mode 100644
index 000000000..b3d55f0a8
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/layouts/Base.astro
@@ -0,0 +1,17 @@
+---
+const defaults = { title: 'Frontmatter not passed to layout!' }
+const { frontmatter = defaults, content = defaults } = Astro.props;
+---
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{frontmatter.title}</title>
+</head>
+<body>
+ <slot />
+</body>
+</html>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/markdown-plugins.mjs b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/markdown-plugins.mjs
new file mode 100644
index 000000000..0366bbfe9
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/markdown-plugins.mjs
@@ -0,0 +1,27 @@
+import { toString } from 'mdast-util-to-string';
+import getReadingTime from 'reading-time';
+import { visit } from 'unist-util-visit';
+
+export function rehypeReadingTime() {
+ return function (tree, { data }) {
+ const readingTime = getReadingTime(toString(tree));
+ data.astro.frontmatter.injectedReadingTime = readingTime;
+ };
+}
+
+export function remarkTitle() {
+ return function (tree, { data }) {
+ visit(tree, ['heading'], (node) => {
+ if (node.depth === 1) {
+ data.astro.frontmatter.title = toString(node.children);
+ }
+ });
+ };
+}
+
+export function remarkDescription() {
+ return function (tree, vfile) {
+ const { frontmatter } = vfile.data.astro;
+ frontmatter.description = `Processed by remarkDescription plugin: ${frontmatter.description}`
+ };
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/glob.json.js b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/glob.json.js
new file mode 100644
index 000000000..63248dc56
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/glob.json.js
@@ -0,0 +1,6 @@
+export async function GET() {
+ const docs = await import.meta.glob('./*.mdx', { eager: true });
+ return new Response(
+ JSON.stringify(Object.values(docs).map(doc => doc.frontmatter))
+ );
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-1.mdx b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-1.mdx
new file mode 100644
index 000000000..0d96d95b9
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-1.mdx
@@ -0,0 +1,8 @@
+---
+layout: '../layouts/Base.astro'
+description: Page 1 description
+---
+
+# Page 1
+
+Look at that!
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-2.mdx b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-2.mdx
new file mode 100644
index 000000000..fe6a8286b
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-2.mdx
@@ -0,0 +1,24 @@
+---
+layout: '../layouts/Base.astro'
+description: Page 2 description
+---
+
+# Page 2
+
+## Table of contents
+
+## Section 1
+
+Some text!
+
+### Subsection 1
+
+Some subsection test!
+
+### Subsection 2
+
+Oh cool, more text!
+
+## Section 2
+
+More content
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/layouts/Base.astro b/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/layouts/Base.astro
new file mode 100644
index 000000000..9dde6e980
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/layouts/Base.astro
@@ -0,0 +1,38 @@
+---
+const {
+ content = { title: "content didn't work" },
+ file = "file didn't work",
+ url = "url didn't work",
+ frontmatter = {
+ title: "frontmatter didn't work",
+ file: "file didn't work",
+ url: "url didn't work",
+ },
+ headings = [],
+} = Astro.props;
+---
+
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+</head>
+
+<body>
+ <p data-content-title>{content.title}</p>
+ <p data-frontmatter-title>{frontmatter.title}</p>
+ <p data-frontmatter-file>{frontmatter.file}</p>
+ <p data-frontmatter-url>{frontmatter.url}</p>
+ <p data-file>{frontmatter.file}</p>
+ <p data-url>{frontmatter.url}</p>
+ <p data-layout-rendered>Layout rendered!</p>
+ <ul data-headings>
+ {headings.map(heading => <li>{heading.slug}</li>)}
+ </ul>
+ <slot />
+</body>
+
+</html>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/glob.json.js b/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/glob.json.js
new file mode 100644
index 000000000..d82d9f770
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/glob.json.js
@@ -0,0 +1,6 @@
+export async function GET() {
+ const mdxPages = await import.meta.glob('./*.mdx', { eager: true });
+ return Response.json({
+ titles: Object.values(mdxPages ?? {}).map((v) => v?.frontmatter?.title),
+ });
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/index.mdx
new file mode 100644
index 000000000..e6f9c8f4a
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/index.mdx
@@ -0,0 +1,10 @@
+---
+title: 'Using YAML frontmatter'
+layout: '../layouts/Base.astro'
+illThrowIfIDontExist: "Oh no, that's scary!"
+---
+
+{frontmatter.illThrowIfIDontExist}
+
+> Note: newline intentionally missing from the end of this file.
+> Useful since that can be the source of bugs in our compile step. \ No newline at end of file
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/with-headings.mdx b/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/with-headings.mdx
new file mode 100644
index 000000000..cc4db9582
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/with-headings.mdx
@@ -0,0 +1,7 @@
+---
+layout: '../layouts/Base.astro'
+---
+
+## Section 1
+
+## Section 2
diff --git a/packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/pages.json.js b/packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/pages.json.js
new file mode 100644
index 000000000..e57192c61
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/pages.json.js
@@ -0,0 +1,8 @@
+export async function GET() {
+ const mdxPages = await import.meta.glob('./*.mdx', { eager: true });
+ return Response.json({
+ headingsByPage: Object.fromEntries(
+ Object.entries(mdxPages ?? {}).map(([k, v]) => [k, v?.getHeadings()])
+ ),
+ });
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/test-with-frontmatter.mdx b/packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/test-with-frontmatter.mdx
new file mode 100644
index 000000000..d40537eb8
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/test-with-frontmatter.mdx
@@ -0,0 +1,45 @@
+---
+title: The Frontmatter Title
+keywords: [Keyword 1, Keyword 2, Keyword 3]
+tags:
+ - Tag 1
+ - Tag 2
+ - Tag 3
+items:
+ - value: Item 1
+ - value: Item 2
+ - value: Item 3
+nested_items:
+ nested:
+ - value: Nested Item 1
+ - value: Nested Item 2
+ - value: Nested Item 3
+---
+
+# {frontmatter.title}
+
+This ID should be the frontmatter title.
+
+## frontmatter.title
+
+The ID should not be the frontmatter title.
+
+### {frontmatter.keywords[1]}
+
+The ID should be the frontmatter keyword #2.
+
+### {frontmatter.tags[0]}
+
+The ID should be the frontmatter tag #1.
+
+#### {frontmatter.items[1].value}
+
+The ID should be the frontmatter item #2.
+
+##### {frontmatter.nested_items.nested[2].value}
+
+The ID should be the frontmatter nested item #3.
+
+###### {frontmatter.unknown}
+
+This ID should not reference the frontmatter.
diff --git a/packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/test-with-jsx-expressions.mdx b/packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/test-with-jsx-expressions.mdx
new file mode 100644
index 000000000..2ec7b1686
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/test-with-jsx-expressions.mdx
@@ -0,0 +1,8 @@
+export const h2Title = "Section 1"
+export const h3Title = "Subsection 1"
+
+# Heading test with JSX expressions
+
+## {h2Title}
+
+### {h3Title}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/test.mdx b/packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/test.mdx
new file mode 100644
index 000000000..2bf3677cf
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-get-headings/src/pages/test.mdx
@@ -0,0 +1,9 @@
+# Heading test
+
+## Section 1
+
+### Subsection 1
+
+### Subsection 2
+
+## Section 2
diff --git a/packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/content/1.mdx b/packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/content/1.mdx
new file mode 100644
index 000000000..7ad64edfe
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/content/1.mdx
@@ -0,0 +1,5 @@
+---
+one: hello
+slug: one
+---
+First mdx file
diff --git a/packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/pages/[slug].astro b/packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/pages/[slug].astro
new file mode 100644
index 000000000..fe453a8bf
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/pages/[slug].astro
@@ -0,0 +1,34 @@
+---
+export const getStaticPaths = async () => {
+ const content = await Astro.glob('../content/*.mdx');
+
+ return content
+ .filter((page) => !page.frontmatter.draft) // skip drafts
+ .map(({ default: MdxContent, frontmatter, url, file }) => {
+ return {
+ params: { slug: frontmatter.slug || "index" },
+ props: {
+ MdxContent,
+ file,
+ frontmatter,
+ url
+ }
+ }
+ })
+}
+
+const { MdxContent, frontmatter, url, file } = Astro.props;
+---
+
+<html>
+ <head>
+ <title>Page</title>
+ </head>
+ <body>
+ <MdxContent />
+
+ <div id="one">{frontmatter.one}</div>
+ <div id="url">{url}</div>
+ <div id="file">{file}</div>
+ </body>
+</html>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/astro.config.ts b/packages/integrations/mdx/test/fixtures/mdx-images/astro.config.ts
new file mode 100644
index 000000000..a6326190e
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/astro.config.ts
@@ -0,0 +1,13 @@
+import mdx from '@astrojs/mdx';
+import { testImageService } from '../../../../../astro/test/test-image-service.js';
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ integrations: [mdx()],
+ image: {
+ service: testImageService(),
+ },
+ experimental: {
+ responsiveImages: true,
+ }
+})
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/package.json b/packages/integrations/mdx/test/fixtures/mdx-images/package.json
new file mode 100644
index 000000000..44b4a9fcf
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@test/mdx-images",
+ "private": true,
+ "dependencies": {
+ "@astrojs/mdx": "workspace:*",
+ "astro": "workspace:*",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/public/favicon.svg b/packages/integrations/mdx/test/fixtures/mdx-images/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/assets/houston in space.webp b/packages/integrations/mdx/test/fixtures/mdx-images/src/assets/houston in space.webp
new file mode 100644
index 000000000..3727bc508
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/assets/houston in space.webp
Binary files differ
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/assets/houston.webp b/packages/integrations/mdx/test/fixtures/mdx-images/src/assets/houston.webp
new file mode 100644
index 000000000..3727bc508
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/assets/houston.webp
Binary files differ
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/components/Component.mdx b/packages/integrations/mdx/test/fixtures/mdx-images/src/components/Component.mdx
new file mode 100644
index 000000000..7463939ba
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/components/Component.mdx
@@ -0,0 +1,5 @@
+Optimized image:
+![Houston](../assets/houston.webp)
+
+Public image:
+![Astro logo](/favicon.svg)
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/components/MyImage.astro b/packages/integrations/mdx/test/fixtures/mdx-images/src/components/MyImage.astro
new file mode 100644
index 000000000..3cb699e3a
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/components/MyImage.astro
@@ -0,0 +1,25 @@
+---
+import { Image } from 'astro:assets';
+import type { ImageMetadata } from 'astro';
+
+type Props = {
+ src: string | ImageMetadata;
+ alt: string;
+};
+
+const { src, alt } = Astro.props;
+---
+
+{
+ typeof src === 'string' ? (
+ <img data-my-image src={src} alt={alt} />
+ ) : (
+ <Image data-my-image {src} {alt} />
+ )
+}
+
+<style>
+ [data-my-image] {
+ border: 1px solid red;
+ }
+</style>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/content/blog/entry.mdx b/packages/integrations/mdx/test/fixtures/mdx-images/src/content/blog/entry.mdx
new file mode 100644
index 000000000..58aebcf54
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/content/blog/entry.mdx
@@ -0,0 +1,5 @@
+Optimized image:
+![Houston](../../assets/houston.webp)
+
+Public image:
+![Astro logo](/favicon.svg)
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/content/config.ts b/packages/integrations/mdx/test/fixtures/mdx-images/src/content/config.ts
new file mode 100644
index 000000000..14443e78d
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/content/config.ts
@@ -0,0 +1,5 @@
+import { defineCollection, z } from 'astro:content';
+
+const blog = defineCollection({});
+
+export const collections = { blog };
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/content-collection.astro b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/content-collection.astro
new file mode 100644
index 000000000..68a3fe3ba
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/content-collection.astro
@@ -0,0 +1,19 @@
+---
+import { getEntry, render } from 'astro:content';
+import MyImage from 'src/components/MyImage.astro';
+
+const entry = await getEntry('blog', 'entry');
+const { Content } = await render(entry)
+---
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Renderer</title>
+</head>
+<body>
+ <Content components={{ img: MyImage }} />
+</body>
+</html>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/esm-import.astro b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/esm-import.astro
new file mode 100644
index 000000000..4e4db66a4
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/esm-import.astro
@@ -0,0 +1,16 @@
+---
+import MyImage from 'src/components/MyImage.astro';
+import MDX from '../components/Component.mdx';
+---
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Renderer</title>
+</head>
+<body>
+ <MDX components={{ img: MyImage }} />
+</body>
+</html>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/houston.png b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/houston.png
new file mode 100644
index 000000000..345ed0e9d
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/houston.png
Binary files differ
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/index.mdx
new file mode 100644
index 000000000..de126a6a0
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/index.mdx
@@ -0,0 +1,17 @@
+Image using a relative path:
+![Houston](../assets/houston.webp)
+
+Image using an aliased path:
+![Houston](~/assets/houston.webp)
+
+Image with a title:
+![Houston](~/assets/houston.webp "Houston title")
+
+Image with spaces in the path:
+![Houston](<~/assets/houston in space.webp>)
+
+Image using a relative path with no slashes:
+![Houston](houston.png)
+
+Image using a relative path with nested directory:
+![Houston](relative/houston.png)
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/no-image.mdx b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/no-image.mdx
new file mode 100644
index 000000000..944c593a9
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/no-image.mdx
@@ -0,0 +1 @@
+Nothing to see here.
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/relative/houston.png b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/relative/houston.png
new file mode 100644
index 000000000..345ed0e9d
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/relative/houston.png
Binary files differ
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/with-components.mdx b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/with-components.mdx
new file mode 100644
index 000000000..763256b1c
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/src/pages/with-components.mdx
@@ -0,0 +1,9 @@
+import MyImage from '../components/MyImage.astro';
+
+export const components = { img: MyImage };
+
+Optimized image:
+![Houston](../assets/houston.webp)
+
+Public image:
+![Astro logo](/favicon.svg)
diff --git a/packages/integrations/mdx/test/fixtures/mdx-images/tsconfig.json b/packages/integrations/mdx/test/fixtures/mdx-images/tsconfig.json
new file mode 100644
index 000000000..c193287fc
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-images/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "astro/tsconfigs/base",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "~/assets/*": ["src/assets/*"]
+ },
+ },
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/astro.config.ts b/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/astro.config.ts
new file mode 100644
index 000000000..75a2b5f3a
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/astro.config.ts
@@ -0,0 +1,6 @@
+import mdx from '@astrojs/mdx';
+import preact from '@astrojs/preact';
+
+export default {
+ integrations: [mdx(), preact()]
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/package.json b/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/package.json
new file mode 100644
index 000000000..253691e2d
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@test/mdx-infinite-loop",
+ "type": "module",
+ "private": true,
+ "dependencies": {
+ "@astrojs/mdx": "workspace:*",
+ "@astrojs/preact": "workspace:*",
+ "astro": "workspace:*",
+ "preact": "^10.25.4"
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/src/components/Test.js b/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/src/components/Test.js
new file mode 100644
index 000000000..831fb327c
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/src/components/Test.js
@@ -0,0 +1,3 @@
+export default function () {
+ return 'Hello world'
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/src/pages/doc.mdx b/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/src/pages/doc.mdx
new file mode 100644
index 000000000..8b8dc5e07
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/src/pages/doc.mdx
@@ -0,0 +1,6 @@
+import Test, { Missing } from '../components/Test';
+
+# Hello page!
+
+<Test />
+<Missing />
diff --git a/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/src/pages/index.astro b/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/src/pages/index.astro
new file mode 100644
index 000000000..11f7af385
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-infinite-loop/src/pages/index.astro
@@ -0,0 +1,5 @@
+---
+const files = await Astro.glob('./**/*.mdx')
+---
+
+{files.map((file: any) => <file.Content />)}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-math/src/pages/mathjax.mdx b/packages/integrations/mdx/test/fixtures/mdx-math/src/pages/mathjax.mdx
new file mode 100644
index 000000000..d90b79f2f
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-math/src/pages/mathjax.mdx
@@ -0,0 +1,5 @@
+# Mathjax
+
+$$
+\left(\frac{\sqrt{x+2}}{y^{2}}\right)
+$$
diff --git a/packages/integrations/mdx/test/fixtures/mdx-namespace/astro.config.mjs b/packages/integrations/mdx/test/fixtures/mdx-namespace/astro.config.mjs
new file mode 100644
index 000000000..4671227d3
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-namespace/astro.config.mjs
@@ -0,0 +1,6 @@
+import mdx from '@astrojs/mdx';
+import react from '@astrojs/react';
+
+export default {
+ integrations: [react(), mdx()]
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-namespace/package.json b/packages/integrations/mdx/test/fixtures/mdx-namespace/package.json
new file mode 100644
index 000000000..435bb7f54
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-namespace/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@test/mdx-namespace",
+ "private": true,
+ "dependencies": {
+ "@astrojs/mdx": "workspace:*",
+ "@astrojs/react": "workspace:*",
+ "astro": "workspace:*",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-namespace/src/components/Component.jsx b/packages/integrations/mdx/test/fixtures/mdx-namespace/src/components/Component.jsx
new file mode 100644
index 000000000..19a3d9c19
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-namespace/src/components/Component.jsx
@@ -0,0 +1,6 @@
+const Component = () => {
+ return <p id="component">Hello world</p>;
+};
+export const ns = {
+ Component
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-namespace/src/pages/object.mdx b/packages/integrations/mdx/test/fixtures/mdx-namespace/src/pages/object.mdx
new file mode 100644
index 000000000..6f3990137
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-namespace/src/pages/object.mdx
@@ -0,0 +1,3 @@
+import * as mod from '../components/Component.jsx';
+
+<mod.ns.Component client:load />
diff --git a/packages/integrations/mdx/test/fixtures/mdx-namespace/src/pages/star.mdx b/packages/integrations/mdx/test/fixtures/mdx-namespace/src/pages/star.mdx
new file mode 100644
index 000000000..b3af5422c
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-namespace/src/pages/star.mdx
@@ -0,0 +1,3 @@
+import { ns } from '../components/Component.jsx';
+
+<ns.Component client:load />
diff --git a/packages/integrations/mdx/test/fixtures/mdx-optimize/astro.config.mjs b/packages/integrations/mdx/test/fixtures/mdx-optimize/astro.config.mjs
new file mode 100644
index 000000000..a3626c3a3
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-optimize/astro.config.mjs
@@ -0,0 +1,37 @@
+import mdx from '@astrojs/mdx';
+
+export default {
+ integrations: [
+ mdx({
+ optimize: {
+ ignoreElementNames: ['strong'],
+ },
+ }),
+ ],
+ markdown: {
+ rehypePlugins: [
+ () => {
+ return (tree) => {
+ tree.children.push({
+ type: 'root',
+ children: [
+ {
+ type: 'element',
+ tagName: 'p',
+ properties: {
+ id: 'injected-root-hast',
+ },
+ children: [
+ {
+ type: 'text',
+ value: 'Injected root hast from rehype plugin',
+ },
+ ],
+ },
+ ],
+ });
+ };
+ },
+ ],
+ },
+};
diff --git a/packages/integrations/mdx/test/fixtures/mdx-optimize/package.json b/packages/integrations/mdx/test/fixtures/mdx-optimize/package.json
new file mode 100644
index 000000000..69120477e
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-optimize/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@test/mdx-optimize",
+ "private": true,
+ "dependencies": {
+ "@astrojs/mdx": "workspace:*",
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-optimize/src/components/Blockquote.astro b/packages/integrations/mdx/test/fixtures/mdx-optimize/src/components/Blockquote.astro
new file mode 100644
index 000000000..aa55e82b1
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-optimize/src/components/Blockquote.astro
@@ -0,0 +1,3 @@
+<blockquote {...Astro.props} class="custom-blockquote">
+ <slot />
+</blockquote>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-optimize/src/components/Strong.astro b/packages/integrations/mdx/test/fixtures/mdx-optimize/src/components/Strong.astro
new file mode 100644
index 000000000..3c0b39ffc
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-optimize/src/components/Strong.astro
@@ -0,0 +1,3 @@
+<strong {...Astro.props} class="custom-strong">
+ <slot />
+</strong>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-optimize/src/pages/_imported.mdx b/packages/integrations/mdx/test/fixtures/mdx-optimize/src/pages/_imported.mdx
new file mode 100644
index 000000000..efe520341
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-optimize/src/pages/_imported.mdx
@@ -0,0 +1,3 @@
+I once heard a very **inspirational** quote:
+
+> I like pancakes
diff --git a/packages/integrations/mdx/test/fixtures/mdx-optimize/src/pages/import.astro b/packages/integrations/mdx/test/fixtures/mdx-optimize/src/pages/import.astro
new file mode 100644
index 000000000..9bfa7cebf
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-optimize/src/pages/import.astro
@@ -0,0 +1,15 @@
+---
+import Strong from '../components/Strong.astro'
+import { Content, components } from './index.mdx'
+---
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Import MDX component</title>
+ </head>
+ <body>
+ <h1>Astro page</h1>
+ <Content components={{ ...components, strong: Strong }} />
+ </body>
+</html>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-optimize/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-optimize/src/pages/index.mdx
new file mode 100644
index 000000000..a3545bd8c
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-optimize/src/pages/index.mdx
@@ -0,0 +1,15 @@
+import Blockquote from '../components/Blockquote.astro'
+
+export const components = {
+ blockquote: Blockquote
+}
+
+# MDX page
+
+I once heard a very inspirational quote:
+
+> I like pancakes
+
+```js
+const pancakes = 'yummy'
+```
diff --git a/packages/integrations/mdx/test/fixtures/mdx-page/astro.config.ts b/packages/integrations/mdx/test/fixtures/mdx-page/astro.config.ts
new file mode 100644
index 000000000..f1d5e8bd7
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-page/astro.config.ts
@@ -0,0 +1,5 @@
+import mdx from '@astrojs/mdx';
+
+export default {
+ integrations: [mdx()]
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-page/package.json b/packages/integrations/mdx/test/fixtures/mdx-page/package.json
new file mode 100644
index 000000000..0429b4ee4
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-page/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@test/mdx-page",
+ "private": true,
+ "dependencies": {
+ "@astrojs/mdx": "workspace:*",
+ "astro": "workspace:*",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-page/src/layouts/EncodingLayout.astro b/packages/integrations/mdx/test/fixtures/mdx-page/src/layouts/EncodingLayout.astro
new file mode 100644
index 000000000..13e0e91ed
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-page/src/layouts/EncodingLayout.astro
@@ -0,0 +1 @@
+<slot></slot>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-page/src/pages/chinese-encoding-layout-frontmatter.mdx b/packages/integrations/mdx/test/fixtures/mdx-page/src/pages/chinese-encoding-layout-frontmatter.mdx
new file mode 100644
index 000000000..471827de0
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-page/src/pages/chinese-encoding-layout-frontmatter.mdx
@@ -0,0 +1,7 @@
+---
+layout: ../layouts/EncodingLayout.astro
+---
+
+# 我的第一篇博客文章
+
+发表于:2022-07-01
diff --git a/packages/integrations/mdx/test/fixtures/mdx-page/src/pages/chinese-encoding-layout-manual.mdx b/packages/integrations/mdx/test/fixtures/mdx-page/src/pages/chinese-encoding-layout-manual.mdx
new file mode 100644
index 000000000..1c8c78630
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-page/src/pages/chinese-encoding-layout-manual.mdx
@@ -0,0 +1,12 @@
+import EncodingLayout from '../layouts/EncodingLayout.astro'
+
+{/* Ensure random stuff preceding the wrapper layout is ignored when detecting a wrapper layout */}
+export const foo = {}
+
+<EncodingLayout>
+
+# 我的第一篇博客文章
+
+发表于:2022-07-01
+
+</EncodingLayout>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-page/src/pages/chinese-encoding.mdx b/packages/integrations/mdx/test/fixtures/mdx-page/src/pages/chinese-encoding.mdx
new file mode 100644
index 000000000..572b3c370
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-page/src/pages/chinese-encoding.mdx
@@ -0,0 +1,3 @@
+# 我的第一篇博客文章
+
+发表于:2022-07-01
diff --git a/packages/integrations/mdx/test/fixtures/mdx-page/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-page/src/pages/index.mdx
new file mode 100644
index 000000000..2c9af4d03
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-page/src/pages/index.mdx
@@ -0,0 +1,3 @@
+import '../styles.css'
+
+# Hello page!
diff --git a/packages/integrations/mdx/test/fixtures/mdx-page/src/styles.css b/packages/integrations/mdx/test/fixtures/mdx-page/src/styles.css
new file mode 100644
index 000000000..7d5c79e1e
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-page/src/styles.css
@@ -0,0 +1,3 @@
+p {
+ color: red;
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plugins/src/pages/with-plugins.mdx b/packages/integrations/mdx/test/fixtures/mdx-plugins/src/pages/with-plugins.mdx
new file mode 100644
index 000000000..8699f4a22
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plugins/src/pages/with-plugins.mdx
@@ -0,0 +1,25 @@
+export let recmaPluginWorking = false
+
+# TOC test
+
+## Table of contents
+
+## Section 1
+
+Some text!
+
+### Subsection 1
+
+Some subsection test!
+
+### Subsection 2
+
+Oh cool, more text!
+
+## Section 2
+
+And section 2, with a hyperlink to check GFM is preserved: https://handle-me-gfm.com
+
+<div data-recma-plugin-works={recmaPluginWorking}></div>
+
+> "Smartypants" is -- awesome
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/astro.config.mjs b/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/astro.config.mjs
new file mode 100644
index 000000000..2905fe476
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/astro.config.mjs
@@ -0,0 +1,6 @@
+import mdx from '@astrojs/mdx';
+import react from '@astrojs/react';
+
+export default {
+ integrations: [mdx(), react()],
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/package.json b/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/package.json
new file mode 100644
index 000000000..b76dc19a6
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@test/mdx-plus-react-errors",
+ "private": true,
+ "dependencies": {
+ "@astrojs/mdx": "workspace:*",
+ "@astrojs/react": "workspace:*",
+ "astro": "workspace:*",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/components/BrokenComponent.jsx b/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/components/BrokenComponent.jsx
new file mode 100644
index 000000000..f9091c825
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/components/BrokenComponent.jsx
@@ -0,0 +1,8 @@
+import { useState } from "react";
+
+export default function BrokenComponent() {
+ useState(0);
+ a;
+
+ return <p>Whoops!</p>;
+};
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/content/config.js b/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/content/config.js
new file mode 100644
index 000000000..6250d13c8
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/content/config.js
@@ -0,0 +1,12 @@
+import { z, defineCollection } from "astro:content";
+
+const filesSchema = () => {
+ return z.object({});
+};
+
+const filesCollection = defineCollection({
+ type: "content",
+ schema: filesSchema(),
+});
+
+export const collections = { files: filesCollection, };
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/content/files/file.mdx b/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/content/files/file.mdx
new file mode 100644
index 000000000..9c536e9ff
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/content/files/file.mdx
@@ -0,0 +1,4 @@
+
+import BrokenComponent from '../../components/BrokenComponent'
+
+<BrokenComponent {...props} />
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/pages/broken.astro b/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/pages/broken.astro
new file mode 100644
index 000000000..bee4c85b5
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plus-react-errors/src/pages/broken.astro
@@ -0,0 +1,9 @@
+---
+import { getCollection } from "astro:content";
+const files = await getCollection("files");
+
+const { Content } = await files[0].render();
+---
+
+<Content />
+
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plus-react/astro.config.mjs b/packages/integrations/mdx/test/fixtures/mdx-plus-react/astro.config.mjs
new file mode 100644
index 000000000..fd017ed8e
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plus-react/astro.config.mjs
@@ -0,0 +1,6 @@
+import mdx from '@astrojs/mdx';
+import react from '@astrojs/react';
+
+export default {
+ integrations: [mdx(), react()]
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plus-react/package.json b/packages/integrations/mdx/test/fixtures/mdx-plus-react/package.json
new file mode 100644
index 000000000..a177efaff
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plus-react/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@test/mdx-plus-react",
+ "private": true,
+ "dependencies": {
+ "@astrojs/mdx": "workspace:*",
+ "@astrojs/react": "workspace:*",
+ "astro": "workspace:*",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plus-react/src/components/Component.jsx b/packages/integrations/mdx/test/fixtures/mdx-plus-react/src/components/Component.jsx
new file mode 100644
index 000000000..70c336314
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plus-react/src/components/Component.jsx
@@ -0,0 +1,8 @@
+import { useState } from "react";
+
+const Component = () => {
+ const [name] = useState('world');
+ return <p>Hello {name}</p>;
+};
+
+export default Component;
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plus-react/src/pages/index.astro b/packages/integrations/mdx/test/fixtures/mdx-plus-react/src/pages/index.astro
new file mode 100644
index 000000000..2486e7834
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plus-react/src/pages/index.astro
@@ -0,0 +1,11 @@
+---
+import Component from "../components/Component.jsx";
+---
+<html>
+ <head>
+ <title>Testing</title>
+ </head>
+ <body>
+ <Component />
+ </body>
+</html>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plus-react/src/pages/inline-component.mdx b/packages/integrations/mdx/test/fixtures/mdx-plus-react/src/pages/inline-component.mdx
new file mode 100644
index 000000000..4071a2c70
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plus-react/src/pages/inline-component.mdx
@@ -0,0 +1,5 @@
+export const Comp = () => <span>Comp</span>
+
+# Inline component
+
+This is an inline component: <Comp />
diff --git a/packages/integrations/mdx/test/fixtures/mdx-plus-react/src/pages/post.mdx b/packages/integrations/mdx/test/fixtures/mdx-plus-react/src/pages/post.mdx
new file mode 100644
index 000000000..734f33e82
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-plus-react/src/pages/post.mdx
@@ -0,0 +1,3 @@
+# Testing
+
+This works!
diff --git a/packages/integrations/mdx/test/fixtures/mdx-script-style-raw/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-script-style-raw/src/pages/index.mdx
new file mode 100644
index 000000000..a603c6626
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-script-style-raw/src/pages/index.mdx
@@ -0,0 +1,13 @@
+# Script style raw
+
+<script id="test-script">
+{`console.log('raw script')`}
+</script>
+
+<style id="test-style">
+{`
+h1[id="script-style-raw"] {
+ color: red;
+}
+`}
+</style>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-slots/src/components/Slotted.astro b/packages/integrations/mdx/test/fixtures/mdx-slots/src/components/Slotted.astro
new file mode 100644
index 000000000..99453b685
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-slots/src/components/Slotted.astro
@@ -0,0 +1,4 @@
+<div class="slotted">
+ <div data-default-slot><slot /></div>
+ <div data-named-slot><slot name="named" /></div>
+</div>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-slots/src/components/Test.mdx b/packages/integrations/mdx/test/fixtures/mdx-slots/src/components/Test.mdx
new file mode 100644
index 000000000..8e901aa1a
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-slots/src/components/Test.mdx
@@ -0,0 +1,15 @@
+import Slotted from './Slotted.astro'
+
+# Hello slotted component!
+
+<Slotted>
+
+Default content.
+
+<Fragment slot="named">
+
+Content for named slot.
+
+</Fragment>
+
+</Slotted>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/glob.astro b/packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/glob.astro
new file mode 100644
index 000000000..ae857fe27
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/glob.astro
@@ -0,0 +1,11 @@
+---
+const components = await Astro.glob('../components/*.mdx');
+---
+
+<div data-default-export>
+ {components.map(Component => <Component.default />)}
+</div>
+
+<div data-content-export>
+ {components.map(({ Content }) => <Content />)}
+</div>
diff --git a/packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/index.astro b/packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/index.astro
new file mode 100644
index 000000000..ed5ae98a3
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/index.astro
@@ -0,0 +1,5 @@
+---
+import Test from '../components/Test.mdx';
+---
+
+<Test />
diff --git a/packages/integrations/mdx/test/fixtures/mdx-syntax-hightlighting/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-syntax-hightlighting/src/pages/index.mdx
new file mode 100644
index 000000000..866387e57
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-syntax-hightlighting/src/pages/index.mdx
@@ -0,0 +1,9 @@
+# Syntax highlighting
+
+```astro {2}
+---
+const handlesAstroSyntax = true
+---
+
+<h1>{handlesAstroSyntax}</h1>
+```
diff --git a/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/pages.json.js b/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/pages.json.js
new file mode 100644
index 000000000..02cff64a8
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/pages.json.js
@@ -0,0 +1,6 @@
+export async function GET() {
+ const mdxPages = await import.meta.glob('./*.mdx', { eager: true });
+ return Response.json({
+ urls: Object.values(mdxPages ?? {}).map((v) => v?.url),
+ });
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-1.mdx b/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-1.mdx
new file mode 100644
index 000000000..c9b984787
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-1.mdx
@@ -0,0 +1 @@
+# I'm a page with a url of "/test-1!"
diff --git a/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-2.mdx b/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-2.mdx
new file mode 100644
index 000000000..360f72fc3
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-2.mdx
@@ -0,0 +1 @@
+# I'm a page with a url of "/test-2!"
diff --git a/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/with-url-override.mdx b/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/with-url-override.mdx
new file mode 100644
index 000000000..e9961928b
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/with-url-override.mdx
@@ -0,0 +1,3 @@
+export const url = '/AH!'
+
+# I'm a test with a url override!
diff --git a/packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/astro.config.mjs b/packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/astro.config.mjs
new file mode 100644
index 000000000..717d379c0
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/astro.config.mjs
@@ -0,0 +1,16 @@
+import mdx from '@astrojs/mdx';
+
+export default {
+ site: 'https://mdx-is-neat.com/',
+ markdown: {
+ syntaxHighlight: false,
+ },
+ integrations: [mdx()],
+ vite: {
+ build: {
+ // Enabling sourcemap may crash the build when using `import.meta.env.UNKNOWN_VAR`
+ // https://github.com/withastro/astro/issues/9012
+ sourcemap: true,
+ },
+ },
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/package.json b/packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/package.json
new file mode 100644
index 000000000..b7c45b3e3
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@test/mdx-env-variables",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*",
+ "@astrojs/mdx": "workspace:*"
+ }
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/src/pages/frontmatter.json.js b/packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/src/pages/frontmatter.json.js
new file mode 100644
index 000000000..152285b9c
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/src/pages/frontmatter.json.js
@@ -0,0 +1,5 @@
+import { frontmatter } from './vite-env-vars.mdx';
+
+export function GET() {
+ return Response.json(frontmatter);
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/src/pages/vite-env-vars.mdx b/packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/src/pages/vite-env-vars.mdx
new file mode 100644
index 000000000..7fb9960ce
--- /dev/null
+++ b/packages/integrations/mdx/test/fixtures/mdx-vite-env-vars/src/pages/vite-env-vars.mdx
@@ -0,0 +1,46 @@
+---
+title: Let's talk about my import.meta.env.SITE
+---
+
+export const modeWorks =
+ import.meta.env.MODE === 'production' ? 'MODE works' : 'MODE does not work!';
+
+export const unknownVar = import.meta.env.UNKNOWN_VAR;
+
+# About my import.meta.env.SITE
+
+My `import.meta.env.SITE` is so cool, I can put env variables in code!
+
+```js
+const site = import.meta.env.SITE;
+```
+
+## But I can use import.meta.env properly too
+
+<div data-env-site>
+
+I can compute my site, for example: {new URL('/blog/cool-post', import.meta.env.SITE)}
+
+</div>
+
+<div data-env-variable-exports>
+
+I can also use `import.meta.env` in variable exports: {modeWorks}
+
+</div>
+
+<div data-env-variable-exports-unknown>
+
+I can also use `import.meta.env.UNKNOWN_VAR` through exports: "{unknownVar}"
+
+</div>
+
+I can also use vars as HTML attributes:
+
+<div
+ data-env-dump
+ data-env-prod={import.meta.env.PROD}
+ data-env-dev={import.meta.env.DEV}
+ data-env-base-url={import.meta.env.BASE_URL}
+ data-env-mode={import.meta.env.MODE}
+></div>
diff --git a/packages/integrations/mdx/test/invalid-mdx-component.test.js b/packages/integrations/mdx/test/invalid-mdx-component.test.js
new file mode 100644
index 000000000..b8152e89c
--- /dev/null
+++ b/packages/integrations/mdx/test/invalid-mdx-component.test.js
@@ -0,0 +1,39 @@
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+import mdx from '../dist/index.js';
+
+const FIXTURE_ROOT = new URL('./fixtures/invalid-mdx-component/', import.meta.url);
+
+describe('MDX component with runtime error', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ integrations: [mdx()],
+ });
+ });
+
+ describe('build', () => {
+ /** @type {Error | null} */
+ let error;
+
+ before(async () => {
+ error = null;
+ try {
+ await fixture.build();
+ } catch (e) {
+ error = e;
+ }
+ });
+
+ it('Throws the right error', async () => {
+ assert.ok(error);
+ assert.match(
+ error?.hint,
+ /This issue often occurs when your MDX component encounters runtime errors/,
+ );
+ });
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.js b/packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.js
new file mode 100644
index 000000000..07197e0e5
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.js
@@ -0,0 +1,89 @@
+import mdx from '@astrojs/mdx';
+
+import * as assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+describe('MDX with Astro Markdown remark-rehype config', () => {
+ it('Renders footnotes with values from the default configuration', async () => {
+ const fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-astro-markdown-remarkRehype/', import.meta.url),
+ integrations: [mdx()],
+ markdown: {
+ remarkRehype: {
+ footnoteLabel: 'Catatan kaki',
+ footnoteBackLabel: 'Kembali ke konten',
+ },
+ },
+ });
+
+ await fixture.build();
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ assert.equal(document.querySelector('#footnote-label').textContent, 'Catatan kaki');
+ assert.equal(
+ document.querySelector('.data-footnote-backref').getAttribute('aria-label'),
+ 'Kembali ke konten',
+ );
+ });
+
+ it('Renders footnotes with values from custom configuration extending the default', async () => {
+ const fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-astro-markdown-remarkRehype/', import.meta.url),
+ integrations: [
+ mdx({
+ remarkRehype: {
+ footnoteLabel: 'Catatan kaki',
+ footnoteBackLabel: 'Kembali ke konten',
+ },
+ }),
+ ],
+ markdown: {
+ remarkRehype: {
+ footnoteBackLabel: 'Replace me',
+ },
+ },
+ });
+
+ await fixture.build();
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ assert.equal(document.querySelector('#footnote-label').textContent, 'Catatan kaki');
+ assert.equal(
+ document.querySelector('.data-footnote-backref').getAttribute('aria-label'),
+ 'Kembali ke konten',
+ );
+ });
+
+ it('Renders footnotes with values from custom configuration without extending the default', async () => {
+ const fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-astro-markdown-remarkRehype/', import.meta.url),
+ integrations: [
+ mdx({
+ extendPlugins: 'astroDefaults',
+ remarkRehype: {
+ footnoteLabel: 'Catatan kaki',
+ },
+ }),
+ ],
+ markdown: {
+ remarkRehype: {
+ footnoteBackLabel: 'Kembali ke konten',
+ },
+ },
+ });
+
+ await fixture.build();
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ assert.equal(document.querySelector('#footnote-label').textContent, 'Catatan kaki');
+ assert.equal(
+ document.querySelector('.data-footnote-backref').getAttribute('aria-label'),
+ 'Back to reference 1',
+ );
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-component.test.js b/packages/integrations/mdx/test/mdx-component.test.js
new file mode 100644
index 000000000..1983730e7
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-component.test.js
@@ -0,0 +1,195 @@
+import mdx from '@astrojs/mdx';
+
+import * as assert from 'node:assert/strict';
+import { after, before, describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+describe('MDX Component', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-component/', import.meta.url),
+ integrations: [mdx()],
+ });
+ });
+
+ describe('build', () => {
+ before(async () => {
+ await fixture.build();
+ });
+
+ it('supports top-level imports', async () => {
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('h1');
+ const foo = document.querySelector('#foo');
+
+ assert.equal(h1.textContent, 'Hello component!');
+ assert.equal(foo.textContent, 'bar');
+ });
+
+ it('supports glob imports - <Component.default />', async () => {
+ const html = await fixture.readFile('/glob/index.html');
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('[data-default-export] h1');
+ const foo = document.querySelector('[data-default-export] #foo');
+
+ assert.equal(h1.textContent, 'Hello component!');
+ assert.equal(foo.textContent, 'bar');
+ });
+
+ it('supports glob imports - <Content />', async () => {
+ const html = await fixture.readFile('/glob/index.html');
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('[data-content-export] h1');
+ const foo = document.querySelector('[data-content-export] #foo');
+
+ assert.equal(h1.textContent, 'Hello component!');
+ assert.equal(foo.textContent, 'bar');
+ });
+
+ describe('with <Fragment>', () => {
+ it('supports top-level imports', async () => {
+ const html = await fixture.readFile('/w-fragment/index.html');
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('h1');
+ const p = document.querySelector('p');
+
+ assert.equal(h1.textContent, 'MDX containing <Fragment />');
+ assert.equal(p.textContent, 'bar');
+ });
+
+ it('supports glob imports - <Component.default />', async () => {
+ const html = await fixture.readFile('/glob/index.html');
+ const { document } = parseHTML(html);
+
+ const h = document.querySelector('[data-default-export] [data-file="WithFragment.mdx"] h1');
+ const p = document.querySelector('[data-default-export] [data-file="WithFragment.mdx"] p');
+
+ assert.equal(h.textContent, 'MDX containing <Fragment />');
+ assert.equal(p.textContent, 'bar');
+ });
+
+ it('supports glob imports - <Content />', async () => {
+ const html = await fixture.readFile('/glob/index.html');
+ const { document } = parseHTML(html);
+
+ const h = document.querySelector('[data-content-export] [data-file="WithFragment.mdx"] h1');
+ const p = document.querySelector('[data-content-export] [data-file="WithFragment.mdx"] p');
+
+ assert.equal(h.textContent, 'MDX containing <Fragment />');
+ assert.equal(p.textContent, 'bar');
+ });
+ });
+ });
+
+ describe('dev', () => {
+ let devServer;
+
+ before(async () => {
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ it('supports top-level imports', async () => {
+ const res = await fixture.fetch('/');
+
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('h1');
+ const foo = document.querySelector('#foo');
+
+ assert.equal(h1.textContent, 'Hello component!');
+ assert.equal(foo.textContent, 'bar');
+ });
+
+ it('supports glob imports - <Component.default />', async () => {
+ const res = await fixture.fetch('/glob');
+
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('[data-default-export] h1');
+ const foo = document.querySelector('[data-default-export] #foo');
+
+ assert.equal(h1.textContent, 'Hello component!');
+ assert.equal(foo.textContent, 'bar');
+ });
+
+ it('supports glob imports - <Content />', async () => {
+ const res = await fixture.fetch('/glob');
+
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('[data-content-export] h1');
+ const foo = document.querySelector('[data-content-export] #foo');
+
+ assert.equal(h1.textContent, 'Hello component!');
+ assert.equal(foo.textContent, 'bar');
+ });
+
+ describe('with <Fragment>', () => {
+ it('supports top-level imports', async () => {
+ const res = await fixture.fetch('/w-fragment');
+
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('h1');
+ const p = document.querySelector('p');
+
+ assert.equal(h1.textContent, 'MDX containing <Fragment />');
+ assert.equal(p.textContent, 'bar');
+ });
+
+ it('supports glob imports - <Component.default />', async () => {
+ const res = await fixture.fetch('/glob');
+
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const h = document.querySelector('[data-default-export] [data-file="WithFragment.mdx"] h1');
+ const p = document.querySelector('[data-default-export] [data-file="WithFragment.mdx"] p');
+
+ assert.equal(h.textContent, 'MDX containing <Fragment />');
+ assert.equal(p.textContent, 'bar');
+ });
+
+ it('supports glob imports - <Content />', async () => {
+ const res = await fixture.fetch('/glob');
+
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const h = document.querySelector('[data-content-export] [data-file="WithFragment.mdx"] h1');
+ const p = document.querySelector('[data-content-export] [data-file="WithFragment.mdx"] p');
+
+ assert.equal(h.textContent, 'MDX containing <Fragment />');
+ assert.equal(p.textContent, 'bar');
+ });
+ });
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-escape.test.js b/packages/integrations/mdx/test/mdx-escape.test.js
new file mode 100644
index 000000000..0d1200fe7
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-escape.test.js
@@ -0,0 +1,33 @@
+import mdx from '@astrojs/mdx';
+
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+const FIXTURE_ROOT = new URL('./fixtures/mdx-escape/', import.meta.url);
+
+describe('MDX frontmatter', () => {
+ let fixture;
+ before(async () => {
+ fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ integrations: [mdx()],
+ });
+ await fixture.build();
+ });
+
+ it('does not have unescaped HTML at top-level', async () => {
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ assert.equal(document.body.textContent.includes('<em'), false);
+ });
+
+ it('does not have unescaped HTML inside html tags', async () => {
+ const html = await fixture.readFile('/html-tag/index.html');
+ const { document } = parseHTML(html);
+
+ assert.equal(document.body.textContent.includes('<em'), false);
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-frontmatter-injection.test.js b/packages/integrations/mdx/test/mdx-frontmatter-injection.test.js
new file mode 100644
index 000000000..5c8b69d07
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-frontmatter-injection.test.js
@@ -0,0 +1,60 @@
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+const FIXTURE_ROOT = new URL('./fixtures/mdx-frontmatter-injection/', import.meta.url);
+
+describe('MDX frontmatter injection', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ });
+ await fixture.build();
+ });
+
+ it('remark supports custom vfile data - get title', async () => {
+ const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json'));
+ const titles = frontmatterByPage.map((frontmatter = {}) => frontmatter.title);
+ assert.equal(titles.includes('Page 1'), true);
+ assert.equal(titles.includes('Page 2'), true);
+ });
+
+ it('rehype supports custom vfile data - reading time', async () => {
+ const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json'));
+ const readingTimes = frontmatterByPage.map(
+ (frontmatter = {}) => frontmatter.injectedReadingTime,
+ );
+ assert.equal(readingTimes.length > 0, true);
+ for (let readingTime of readingTimes) {
+ assert.notEqual(readingTime, null);
+ assert.match(readingTime.text, /^\d+ min read/);
+ }
+ });
+
+ it('allow user frontmatter mutation', async () => {
+ const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json'));
+ const descriptions = frontmatterByPage.map((frontmatter = {}) => frontmatter.description);
+ assert.equal(
+ descriptions.includes('Processed by remarkDescription plugin: Page 1 description'),
+ true,
+ );
+ assert.equal(
+ descriptions.includes('Processed by remarkDescription plugin: Page 2 description'),
+ true,
+ );
+ });
+
+ it('passes injected frontmatter to layouts', async () => {
+ const html1 = await fixture.readFile('/page-1/index.html');
+ const html2 = await fixture.readFile('/page-2/index.html');
+
+ const title1 = parseHTML(html1).document.querySelector('title');
+ const title2 = parseHTML(html2).document.querySelector('title');
+
+ assert.equal(title1.innerHTML, 'Page 1');
+ assert.equal(title2.innerHTML, 'Page 2');
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-frontmatter.test.js b/packages/integrations/mdx/test/mdx-frontmatter.test.js
new file mode 100644
index 000000000..df36a2439
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-frontmatter.test.js
@@ -0,0 +1,79 @@
+import mdx from '@astrojs/mdx';
+
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+const FIXTURE_ROOT = new URL('./fixtures/mdx-frontmatter/', import.meta.url);
+
+describe('MDX frontmatter', () => {
+ let fixture;
+ before(async () => {
+ fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ integrations: [mdx()],
+ });
+ await fixture.build();
+ });
+ it('builds when "frontmatter.property" is in JSX expression', async () => {
+ assert.equal(true, true);
+ });
+
+ it('extracts frontmatter to "frontmatter" export', async () => {
+ const { titles } = JSON.parse(await fixture.readFile('/glob.json'));
+ assert.equal(titles.includes('Using YAML frontmatter'), true);
+ });
+
+ it('renders layout from "layout" frontmatter property', async () => {
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const layoutParagraph = document.querySelector('[data-layout-rendered]');
+
+ assert.notEqual(layoutParagraph, null);
+ });
+
+ it('passes frontmatter to layout via "content" and "frontmatter" props', async () => {
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const contentTitle = document.querySelector('[data-content-title]');
+ const frontmatterTitle = document.querySelector('[data-frontmatter-title]');
+
+ assert.equal(contentTitle.textContent, 'Using YAML frontmatter');
+ assert.equal(frontmatterTitle.textContent, 'Using YAML frontmatter');
+ });
+
+ it('passes headings to layout via "headings" prop', async () => {
+ const html = await fixture.readFile('/with-headings/index.html');
+ const { document } = parseHTML(html);
+
+ const headingSlugs = [...document.querySelectorAll('[data-headings] > li')].map(
+ (el) => el.textContent,
+ );
+
+ assert.equal(headingSlugs.length > 0, true);
+ assert.equal(headingSlugs.includes('section-1'), true);
+ assert.equal(headingSlugs.includes('section-2'), true);
+ });
+
+ it('passes "file" and "url" to layout', async () => {
+ const html = await fixture.readFile('/with-headings/index.html');
+ const { document } = parseHTML(html);
+
+ const frontmatterFile = document.querySelector('[data-frontmatter-file]')?.textContent;
+ const frontmatterUrl = document.querySelector('[data-frontmatter-url]')?.textContent;
+ const file = document.querySelector('[data-file]')?.textContent;
+ const url = document.querySelector('[data-url]')?.textContent;
+
+ assert.equal(
+ frontmatterFile?.endsWith('with-headings.mdx'),
+ true,
+ '"file" prop does not end with correct path or is undefined',
+ );
+ assert.equal(frontmatterUrl, '/with-headings');
+ assert.equal(file, frontmatterFile);
+ assert.equal(url, frontmatterUrl);
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-get-headings.test.js b/packages/integrations/mdx/test/mdx-get-headings.test.js
new file mode 100644
index 000000000..4de8b0a6e
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-get-headings.test.js
@@ -0,0 +1,200 @@
+import { rehypeHeadingIds } from '@astrojs/markdown-remark';
+import mdx from '@astrojs/mdx';
+import { visit } from 'unist-util-visit';
+
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+describe('MDX getHeadings', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-get-headings/', import.meta.url),
+ integrations: [mdx()],
+ });
+
+ await fixture.build();
+ });
+
+ it('adds anchor IDs to headings', async () => {
+ const html = await fixture.readFile('/test/index.html');
+ const { document } = parseHTML(html);
+
+ const h2Ids = document.querySelectorAll('h2').map((el) => el?.id);
+ const h3Ids = document.querySelectorAll('h3').map((el) => el?.id);
+ assert.equal(document.querySelector('h1').id, 'heading-test');
+ assert.equal(h2Ids.includes('section-1'), true);
+ assert.equal(h2Ids.includes('section-2'), true);
+ assert.equal(h3Ids.includes('subsection-1'), true);
+ assert.equal(h3Ids.includes('subsection-2'), true);
+ });
+
+ it('generates correct getHeadings() export', async () => {
+ const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json'));
+ // TODO: make this a snapshot test :)
+ assert.equal(
+ JSON.stringify(headingsByPage['./test.mdx']),
+ JSON.stringify([
+ { depth: 1, slug: 'heading-test', text: 'Heading test' },
+ { depth: 2, slug: 'section-1', text: 'Section 1' },
+ { depth: 3, slug: 'subsection-1', text: 'Subsection 1' },
+ { depth: 3, slug: 'subsection-2', text: 'Subsection 2' },
+ { depth: 2, slug: 'section-2', text: 'Section 2' },
+ ]),
+ );
+ });
+
+ it('generates correct getHeadings() export for JSX expressions', async () => {
+ const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json'));
+ assert.equal(
+ JSON.stringify(headingsByPage['./test-with-jsx-expressions.mdx']),
+ JSON.stringify([
+ {
+ depth: 1,
+ slug: 'heading-test-with-jsx-expressions',
+ text: 'Heading test with JSX expressions',
+ },
+ { depth: 2, slug: 'h2title', text: 'h2Title' },
+ { depth: 3, slug: 'h3title', text: 'h3Title' },
+ ]),
+ );
+ });
+});
+
+describe('MDX heading IDs can be customized by user plugins', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-get-headings/', import.meta.url),
+ integrations: [mdx()],
+ markdown: {
+ rehypePlugins: [
+ () => (tree) => {
+ let count = 0;
+ visit(tree, 'element', (node) => {
+ if (!/^h\d$/.test(node.tagName)) return;
+ if (!node.properties?.id) {
+ node.properties = { ...node.properties, id: String(count++) };
+ }
+ });
+ },
+ ],
+ },
+ });
+
+ await fixture.build();
+ });
+
+ it('adds user-specified IDs to HTML output', async () => {
+ const html = await fixture.readFile('/test/index.html');
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('h1');
+ assert.equal(h1?.textContent, 'Heading test');
+ assert.equal(h1?.getAttribute('id'), '0');
+
+ const headingIDs = document.querySelectorAll('h1,h2,h3').map((el) => el.id);
+ assert.equal(
+ JSON.stringify(headingIDs),
+ JSON.stringify(Array.from({ length: headingIDs.length }, (_, idx) => String(idx))),
+ );
+ });
+
+ it('generates correct getHeadings() export', async () => {
+ const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json'));
+ assert.equal(
+ JSON.stringify(headingsByPage['./test.mdx']),
+ JSON.stringify([
+ { depth: 1, slug: '0', text: 'Heading test' },
+ { depth: 2, slug: '1', text: 'Section 1' },
+ { depth: 3, slug: '2', text: 'Subsection 1' },
+ { depth: 3, slug: '3', text: 'Subsection 2' },
+ { depth: 2, slug: '4', text: 'Section 2' },
+ ]),
+ );
+ });
+});
+
+describe('MDX heading IDs can be injected before user plugins', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-get-headings/', import.meta.url),
+ integrations: [
+ mdx({
+ rehypePlugins: [
+ rehypeHeadingIds,
+ () => (tree) => {
+ visit(tree, 'element', (node) => {
+ if (!/^h\d$/.test(node.tagName)) return;
+ if (node.properties?.id) {
+ node.children.push({ type: 'text', value: ' ' + node.properties.id });
+ }
+ });
+ },
+ ],
+ }),
+ ],
+ });
+
+ await fixture.build();
+ });
+
+ it('adds user-specified IDs to HTML output', async () => {
+ const html = await fixture.readFile('/test/index.html');
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('h1');
+ assert.equal(h1?.textContent, 'Heading test heading-test');
+ assert.equal(h1?.id, 'heading-test');
+ });
+});
+
+describe('MDX headings with frontmatter', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-get-headings/', import.meta.url),
+ integrations: [mdx()],
+ });
+
+ await fixture.build();
+ });
+
+ it('adds anchor IDs to headings', async () => {
+ const html = await fixture.readFile('/test-with-frontmatter/index.html');
+ const { document } = parseHTML(html);
+
+ const h3Ids = document.querySelectorAll('h3').map((el) => el?.id);
+
+ assert.equal(document.querySelector('h1').id, 'the-frontmatter-title');
+ assert.equal(document.querySelector('h2').id, 'frontmattertitle');
+ assert.equal(h3Ids.includes('keyword-2'), true);
+ assert.equal(h3Ids.includes('tag-1'), true);
+ assert.equal(document.querySelector('h4').id, 'item-2');
+ assert.equal(document.querySelector('h5').id, 'nested-item-3');
+ assert.equal(document.querySelector('h6').id, 'frontmatterunknown');
+ });
+
+ it('generates correct getHeadings() export', async () => {
+ const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json'));
+ assert.equal(
+ JSON.stringify(headingsByPage['./test-with-frontmatter.mdx']),
+ JSON.stringify([
+ { depth: 1, slug: 'the-frontmatter-title', text: 'The Frontmatter Title' },
+ { depth: 2, slug: 'frontmattertitle', text: 'frontmatter.title' },
+ { depth: 3, slug: 'keyword-2', text: 'Keyword 2' },
+ { depth: 3, slug: 'tag-1', text: 'Tag 1' },
+ { depth: 4, slug: 'item-2', text: 'Item 2' },
+ { depth: 5, slug: 'nested-item-3', text: 'Nested Item 3' },
+ { depth: 6, slug: 'frontmatterunknown', text: 'frontmatter.unknown' },
+ ]),
+ );
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-get-static-paths.test.js b/packages/integrations/mdx/test/mdx-get-static-paths.test.js
new file mode 100644
index 000000000..96f0dc9a4
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-get-static-paths.test.js
@@ -0,0 +1,34 @@
+import mdx from '@astrojs/mdx';
+
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import * as cheerio from 'cheerio';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+const FIXTURE_ROOT = new URL('./fixtures/mdx-get-static-paths', import.meta.url);
+
+describe('getStaticPaths', () => {
+ /** @type {import('astro/test/test-utils').Fixture} */
+ let fixture;
+ before(async () => {
+ fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ integrations: [mdx()],
+ });
+ await fixture.build();
+ });
+
+ it('Provides file and url', async () => {
+ const html = await fixture.readFile('/one/index.html');
+
+ const $ = cheerio.load(html);
+ assert.equal($('p').text(), 'First mdx file');
+ assert.equal($('#one').text(), 'hello', 'Frontmatter included');
+ assert.equal($('#url').text(), 'src/content/1.mdx', 'url is included');
+ assert.equal(
+ $('#file').text().includes('fixtures/mdx-get-static-paths/src/content/1.mdx'),
+ true,
+ 'file is included',
+ );
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-images.test.js b/packages/integrations/mdx/test/mdx-images.test.js
new file mode 100644
index 000000000..543b9021e
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-images.test.js
@@ -0,0 +1,82 @@
+import * as assert from 'node:assert/strict';
+import { after, before, describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+const imageTestRoutes = ['with-components', 'esm-import', 'content-collection'];
+
+describe('MDX Page', () => {
+ let devServer;
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-images/', import.meta.url),
+ });
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ describe('Optimized images in MDX', () => {
+ it('works', async () => {
+ const res = await fixture.fetch('/');
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const imgs = document.getElementsByTagName('img');
+ assert.equal(imgs.length, 6);
+ // Image using a relative path
+ assert.equal(imgs.item(0).src.startsWith('/_image'), true);
+ // Image using an aliased path
+ assert.equal(imgs.item(1).src.startsWith('/_image'), true);
+ // Image with title
+ assert.equal(imgs.item(2).title, 'Houston title');
+ // Image with spaces in the path
+ assert.equal(imgs.item(3).src.startsWith('/_image'), true);
+ // Image using a relative path with no slashes
+ assert.equal(imgs.item(4).src.startsWith('/_image'), true);
+ // Image using a relative path with nested directory
+ assert.equal(imgs.item(5).src.startsWith('/_image'), true);
+ });
+
+ for (const route of imageTestRoutes) {
+ it(`supports img component - ${route}`, async () => {
+ const res = await fixture.fetch(`/${route}`);
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const imgs = document.getElementsByTagName('img');
+ assert.equal(imgs.length, 2);
+
+ const assetsImg = imgs.item(0);
+ assert.equal(assetsImg.src.startsWith('/_image'), true);
+ assert.equal(assetsImg.hasAttribute('data-my-image'), true);
+
+ const publicImg = imgs.item(1);
+ assert.equal(publicImg.src, '/favicon.svg');
+ assert.equal(publicImg.hasAttribute('data-my-image'), true);
+ });
+ }
+ });
+
+ describe('build', () => {
+ before(async () => {
+ await fixture.build();
+ });
+ it('includes responsive styles', async () => {
+ const code = await fixture.readFile('/index.html');
+ assert.ok(code.includes('[data-astro-image]'));
+ });
+ it("doesn't include styles on pages without images", async () => {
+ const code = await fixture.readFile('/no-image/index.html');
+ assert.ok(!code.includes('[data-astro-image]'));
+ });
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-infinite-loop.test.js b/packages/integrations/mdx/test/mdx-infinite-loop.test.js
new file mode 100644
index 000000000..ed98d5c6d
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-infinite-loop.test.js
@@ -0,0 +1,31 @@
+import mdx from '@astrojs/mdx';
+
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+describe('MDX Infinite Loop', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-infinite-loop/', import.meta.url),
+ integrations: [mdx()],
+ });
+ });
+
+ describe('build', () => {
+ let err;
+ before(async () => {
+ try {
+ await fixture.build();
+ } catch (e) {
+ err = e;
+ }
+ });
+
+ it('does not hang forever if an error is thrown', async () => {
+ assert.equal(!!err, true);
+ });
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-math.test.js b/packages/integrations/mdx/test/mdx-math.test.js
new file mode 100644
index 000000000..ff54a1042
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-math.test.js
@@ -0,0 +1,73 @@
+import * as assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import mdx from '@astrojs/mdx';
+import { parseHTML } from 'linkedom';
+import rehypeMathjaxSvg from 'rehype-mathjax';
+import rehypeMathjaxChtml from 'rehype-mathjax/chtml';
+import remarkMath from 'remark-math';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+const FIXTURE_ROOT = new URL('./fixtures/mdx-math/', import.meta.url);
+
+describe('MDX math', () => {
+ describe('mathjax', () => {
+ it('works with svg', async () => {
+ const fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ markdown: {
+ remarkPlugins: [remarkMath],
+ rehypePlugins: [rehypeMathjaxSvg],
+ },
+ integrations: [mdx()],
+ });
+ await fixture.build();
+
+ const html = await fixture.readFile('/mathjax/index.html');
+ const { document } = parseHTML(html);
+
+ const mjxContainer = document.querySelector('mjx-container[jax="SVG"]');
+ assert.notEqual(mjxContainer, null);
+
+ const mjxStyle = document.querySelectorAll('style')[0].innerHTML;
+ assert.equal(
+ mjxStyle.includes('mjx-container[jax="SVG"]'),
+ true,
+ 'style should not be html-escaped',
+ );
+ });
+
+ it('works with chtml', async () => {
+ const fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ markdown: {
+ remarkPlugins: [remarkMath],
+ rehypePlugins: [
+ [
+ rehypeMathjaxChtml,
+ {
+ chtml: {
+ fontURL: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/output/chtml/fonts/woff-v2',
+ },
+ },
+ ],
+ ],
+ },
+ integrations: [mdx()],
+ });
+ await fixture.build();
+
+ const html = await fixture.readFile('/mathjax/index.html');
+ const { document } = parseHTML(html);
+
+ const mjxContainer = document.querySelector('mjx-container[jax="CHTML"]');
+ assert.notEqual(mjxContainer, null);
+
+ const mjxStyle = document.querySelectorAll('style')[0].innerHTML;
+ assert.equal(
+ mjxStyle.includes('mjx-container[jax="CHTML"]'),
+ true,
+ 'style should not be html-escaped',
+ );
+ });
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-namespace.test.js b/packages/integrations/mdx/test/mdx-namespace.test.js
new file mode 100644
index 000000000..13137e40e
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-namespace.test.js
@@ -0,0 +1,84 @@
+import * as assert from 'node:assert/strict';
+import { after, before, describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+describe('MDX Namespace', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-namespace/', import.meta.url),
+ });
+ });
+
+ describe('build', () => {
+ before(async () => {
+ await fixture.build();
+ });
+
+ it('works for object', async () => {
+ const html = await fixture.readFile('/object/index.html');
+ const { document } = parseHTML(html);
+
+ const island = document.querySelector('astro-island');
+ const component = document.querySelector('#component');
+
+ assert.notEqual(island, undefined);
+ assert.equal(component.textContent, 'Hello world');
+ });
+
+ it('works for star', async () => {
+ const html = await fixture.readFile('/star/index.html');
+ const { document } = parseHTML(html);
+
+ const island = document.querySelector('astro-island');
+ const component = document.querySelector('#component');
+
+ assert.notEqual(island, undefined);
+ assert.equal(component.textContent, 'Hello world');
+ });
+ });
+
+ describe('dev', () => {
+ let devServer;
+
+ before(async () => {
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ it('works for object', async () => {
+ const res = await fixture.fetch('/object');
+
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const island = document.querySelector('astro-island');
+ const component = document.querySelector('#component');
+
+ assert.notEqual(island, undefined);
+ assert.equal(component.textContent, 'Hello world');
+ });
+
+ it('works for star', async () => {
+ const res = await fixture.fetch('/star');
+
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const island = document.querySelector('astro-island');
+ const component = document.querySelector('#component');
+
+ assert.notEqual(island, undefined);
+ assert.equal(component.textContent, 'Hello world');
+ });
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-optimize.test.js b/packages/integrations/mdx/test/mdx-optimize.test.js
new file mode 100644
index 000000000..c45ecda15
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-optimize.test.js
@@ -0,0 +1,61 @@
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+const FIXTURE_ROOT = new URL('./fixtures/mdx-optimize/', import.meta.url);
+
+describe('MDX optimize', () => {
+ let fixture;
+ before(async () => {
+ fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ });
+ await fixture.build();
+ });
+
+ it('renders an MDX page fine', async () => {
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ assert.equal(document.querySelector('h1').textContent.includes('MDX page'), true);
+ assert.equal(
+ document.querySelector('p').textContent.includes('I once heard a very inspirational quote:'),
+ true,
+ );
+
+ const blockquote = document.querySelector('blockquote.custom-blockquote');
+ assert.notEqual(blockquote, null);
+ assert.equal(blockquote.textContent.includes('I like pancakes'), true);
+
+ const code = document.querySelector('pre.astro-code');
+ assert.notEqual(code, null);
+ assert.equal(code.textContent.includes(`const pancakes = 'yummy'`), true);
+ });
+
+ it('renders an Astro page that imports MDX fine', async () => {
+ const html = await fixture.readFile('/import/index.html');
+ const { document } = parseHTML(html);
+
+ assert.equal(document.querySelector('h1').textContent.includes('Astro page'), true);
+ assert.equal(
+ document.querySelector('p').textContent.includes('I once heard a very inspirational quote:'),
+ true,
+ );
+
+ const blockquote = document.querySelector('blockquote.custom-blockquote');
+ assert.notEqual(blockquote, null);
+ assert.equal(blockquote.textContent.includes('I like pancakes'), true);
+ });
+
+ it('renders MDX with rehype plugin that incorrectly injects root hast node', async () => {
+ const html = await fixture.readFile('/import/index.html');
+ const { document } = parseHTML(html);
+
+ assert.doesNotMatch(html, /set:html=/);
+ assert.equal(
+ document.getElementById('injected-root-hast').textContent,
+ 'Injected root hast from rehype plugin',
+ );
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-page.test.js b/packages/integrations/mdx/test/mdx-page.test.js
new file mode 100644
index 000000000..b58781efc
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-page.test.js
@@ -0,0 +1,109 @@
+import * as assert from 'node:assert/strict';
+import { after, before, describe, it } from 'node:test';
+import * as cheerio from 'cheerio';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+describe('MDX Page', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-page/', import.meta.url),
+ // test suite was authored when inlineStylesheets defaulted to never
+ build: { inlineStylesheets: 'never' },
+ });
+ });
+
+ describe('build', () => {
+ before(async () => {
+ await fixture.build();
+ });
+
+ it('works', async () => {
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('h1');
+
+ assert.equal(h1.textContent, 'Hello page!');
+ });
+
+ it('injects style imports when layout is not applied', async () => {
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const stylesheet = document.querySelector('link[rel="stylesheet"]');
+
+ assert.notEqual(stylesheet, null);
+ });
+
+ it('Renders MDX in utf-8 by default', async () => {
+ const html = await fixture.readFile('/chinese-encoding/index.html');
+ const $ = cheerio.load(html);
+ assert.equal($('h1').text(), '我的第一篇博客文章');
+ assert.match(html, /<meta charset="utf-8"/);
+ });
+
+ it('Renders MDX with layout frontmatter without utf-8 by default', async () => {
+ const html = await fixture.readFile('/chinese-encoding-layout-frontmatter/index.html');
+ assert.doesNotMatch(html, /<meta charset="utf-8"/);
+ });
+
+ it('Renders MDX with layout manual import without utf-8 by default', async () => {
+ const html = await fixture.readFile('/chinese-encoding-layout-manual/index.html');
+ assert.doesNotMatch(html, /<meta charset="utf-8"/);
+ });
+ });
+
+ describe('dev', () => {
+ let devServer;
+
+ before(async () => {
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ it('works', async () => {
+ const res = await fixture.fetch('/');
+
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('h1');
+
+ assert.equal(h1.textContent, 'Hello page!');
+ });
+
+ it('Renders MDX in utf-8 by default', async () => {
+ const res = await fixture.fetch('/chinese-encoding/');
+ assert.equal(res.status, 200);
+ const html = await res.text();
+ const $ = cheerio.load(html);
+ assert.equal($('h1').text(), '我的第一篇博客文章');
+ assert.doesNotMatch(res.headers.get('content-type'), /charset=utf-8/);
+ assert.match(html, /<meta charset="utf-8"/);
+ });
+
+ it('Renders MDX with layout frontmatter without utf-8 by default', async () => {
+ const res = await fixture.fetch('/chinese-encoding-layout-frontmatter/');
+ assert.equal(res.status, 200);
+ const html = await res.text();
+ assert.doesNotMatch(res.headers.get('content-type'), /charset=utf-8/);
+ assert.doesNotMatch(html, /<meta charset="utf-8"/);
+ });
+
+ it('Renders MDX with layout manual import without utf-8 by default', async () => {
+ const res = await fixture.fetch('/chinese-encoding-layout-manual/');
+ assert.equal(res.status, 200);
+ const html = await res.text();
+ assert.doesNotMatch(res.headers.get('content-type'), /charset=utf-8/);
+ assert.doesNotMatch(html, /<meta charset="utf-8"/);
+ });
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-plugins.test.js b/packages/integrations/mdx/test/mdx-plugins.test.js
new file mode 100644
index 000000000..68ce1e22e
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-plugins.test.js
@@ -0,0 +1,315 @@
+import mdx from '@astrojs/mdx';
+
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { visit as estreeVisit } from 'estree-util-visit';
+import { parseHTML } from 'linkedom';
+import remarkToc from 'remark-toc';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+const FIXTURE_ROOT = new URL('./fixtures/mdx-plugins/', import.meta.url);
+const FILE = '/with-plugins/index.html';
+
+describe('MDX plugins', () => {
+ it('supports custom remark plugins - TOC', async () => {
+ const fixture = await buildFixture({
+ integrations: [
+ mdx({
+ remarkPlugins: [remarkToc],
+ }),
+ ],
+ });
+
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ assert.notEqual(selectTocLink(document), null);
+ });
+
+ it('Applies GFM by default', async () => {
+ const fixture = await buildFixture({
+ integrations: [mdx()],
+ });
+
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ assert.notEqual(selectGfmLink(document), null);
+ });
+
+ it('Applies SmartyPants by default', async () => {
+ const fixture = await buildFixture({
+ integrations: [mdx()],
+ });
+
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ const quote = selectSmartypantsQuote(document);
+ assert.notEqual(quote, null);
+ assert.equal(quote.textContent.includes('“Smartypants” is — awesome'), true);
+ });
+
+ it('supports custom rehype plugins', async () => {
+ const fixture = await buildFixture({
+ integrations: [
+ mdx({
+ rehypePlugins: [rehypeExamplePlugin],
+ }),
+ ],
+ });
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ assert.notEqual(selectRehypeExample(document), null);
+ });
+
+ it('supports custom rehype plugins from integrations', async () => {
+ const fixture = await buildFixture({
+ integrations: [
+ mdx(),
+ {
+ name: 'test',
+ hooks: {
+ 'astro:config:setup': ({ updateConfig }) => {
+ updateConfig({
+ markdown: {
+ rehypePlugins: [rehypeExamplePlugin],
+ },
+ });
+ },
+ },
+ },
+ ],
+ });
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ assert.notEqual(selectRehypeExample(document), null);
+ });
+
+ it('supports custom rehype plugins with namespaced attributes', async () => {
+ const fixture = await buildFixture({
+ integrations: [
+ mdx({
+ rehypePlugins: [rehypeSvgPlugin],
+ }),
+ ],
+ });
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ assert.notEqual(selectRehypeSvg(document), null);
+ });
+
+ it('extends markdown config by default', async () => {
+ const fixture = await buildFixture({
+ markdown: {
+ remarkPlugins: [remarkExamplePlugin],
+ rehypePlugins: [rehypeExamplePlugin],
+ },
+ integrations: [mdx()],
+ });
+
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ assert.notEqual(selectRemarkExample(document), null);
+ assert.notEqual(selectRehypeExample(document), null);
+ });
+
+ it('ignores string-based plugins in markdown config', async () => {
+ const fixture = await buildFixture({
+ markdown: {
+ remarkPlugins: [['remark-toc', {}]],
+ },
+ integrations: [mdx()],
+ });
+
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ assert.equal(selectTocLink(document), null);
+ });
+
+ for (const extendMarkdownConfig of [true, false]) {
+ describe(`extendMarkdownConfig = ${extendMarkdownConfig}`, () => {
+ let fixture;
+ before(async () => {
+ fixture = await buildFixture({
+ markdown: {
+ remarkPlugins: [remarkToc],
+ gfm: false,
+ smartypants: false,
+ },
+ integrations: [
+ mdx({
+ extendMarkdownConfig,
+ remarkPlugins: [remarkExamplePlugin],
+ rehypePlugins: [rehypeExamplePlugin],
+ }),
+ ],
+ });
+ });
+
+ it('Handles MDX plugins', async () => {
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ assert.notEqual(selectRemarkExample(document, 'MDX remark plugins not applied.'), null);
+ assert.notEqual(selectRehypeExample(document, 'MDX rehype plugins not applied.'), null);
+ });
+
+ it('Handles Markdown plugins', async () => {
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ assert.equal(
+ selectTocLink(
+ document,
+ '`remarkToc` plugin applied unexpectedly. Should override Markdown config.',
+ ),
+ null,
+ );
+ });
+
+ it('Handles gfm', async () => {
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ if (extendMarkdownConfig === true) {
+ assert.equal(selectGfmLink(document), null, 'Does not respect `markdown.gfm` option.');
+ } else {
+ assert.notEqual(selectGfmLink(document), null, 'Respects `markdown.gfm` unexpectedly.');
+ }
+ });
+
+ it('Handles smartypants', async () => {
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ const quote = selectSmartypantsQuote(document);
+
+ if (extendMarkdownConfig === true) {
+ assert.equal(
+ quote.textContent.includes('"Smartypants" is -- awesome'),
+ true,
+ 'Does not respect `markdown.smartypants` option.',
+ );
+ } else {
+ assert.equal(
+ quote.textContent.includes('“Smartypants” is — awesome'),
+ true,
+ 'Respects `markdown.smartypants` unexpectedly.',
+ );
+ }
+ });
+ });
+ }
+
+ it('supports custom recma plugins', async () => {
+ const fixture = await buildFixture({
+ integrations: [
+ mdx({
+ recmaPlugins: [recmaExamplePlugin],
+ }),
+ ],
+ });
+
+ const html = await fixture.readFile(FILE);
+ const { document } = parseHTML(html);
+
+ assert.notEqual(selectRecmaExample(document), null);
+ });
+});
+
+async function buildFixture(config) {
+ const fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ ...config,
+ });
+ await fixture.build();
+ return fixture;
+}
+
+function remarkExamplePlugin() {
+ return (tree) => {
+ tree.children.push({
+ type: 'html',
+ value: '<div data-remark-plugin-works="true"></div>',
+ });
+ };
+}
+
+function rehypeExamplePlugin() {
+ return (tree) => {
+ tree.children.push({
+ type: 'element',
+ tagName: 'div',
+ properties: { 'data-rehype-plugin-works': 'true' },
+ });
+ };
+}
+
+function rehypeSvgPlugin() {
+ return (tree) => {
+ tree.children.push({
+ type: 'element',
+ tagName: 'svg',
+ properties: { xmlns: 'http://www.w3.org/2000/svg' },
+ children: [
+ {
+ type: 'element',
+ tagName: 'use',
+ properties: { xLinkHref: '#icon' },
+ },
+ ],
+ });
+ };
+}
+
+function recmaExamplePlugin() {
+ return (tree) => {
+ estreeVisit(tree, (node) => {
+ if (
+ node.type === 'VariableDeclarator' &&
+ node.id.name === 'recmaPluginWorking' &&
+ node.init?.type === 'Literal'
+ ) {
+ node.init = {
+ ...(node.init ?? {}),
+ value: true,
+ raw: 'true',
+ };
+ }
+ });
+ };
+}
+
+function selectTocLink(document) {
+ return document.querySelector('ul a[href="#section-1"]');
+}
+
+function selectGfmLink(document) {
+ return document.querySelector('a[href="https://handle-me-gfm.com"]');
+}
+
+function selectSmartypantsQuote(document) {
+ return document.querySelector('blockquote');
+}
+
+function selectRemarkExample(document) {
+ return document.querySelector('div[data-remark-plugin-works]');
+}
+
+function selectRehypeExample(document) {
+ return document.querySelector('div[data-rehype-plugin-works]');
+}
+
+function selectRehypeSvg(document) {
+ return document.querySelector('svg > use[xlink\\:href]');
+}
+
+function selectRecmaExample(document) {
+ return document.querySelector('div[data-recma-plugin-works]');
+}
diff --git a/packages/integrations/mdx/test/mdx-plus-react-errors.test.js b/packages/integrations/mdx/test/mdx-plus-react-errors.test.js
new file mode 100644
index 000000000..9d87fa8a0
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-plus-react-errors.test.js
@@ -0,0 +1,33 @@
+import * as assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+function hookError() {
+ const error = console.error;
+ const errors = [];
+ console.error = function (...args) {
+ errors.push(args);
+ };
+ return () => {
+ console.error = error;
+ return errors;
+ };
+}
+
+describe('MDX and React with build errors', () => {
+ let fixture;
+ let unhook;
+
+ it('shows correct error messages on build error', async () => {
+ try {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-plus-react-errors/', import.meta.url),
+ });
+ unhook = hookError();
+ await fixture.build();
+ } catch (err) {
+ assert.equal(err.message, 'a is not defined');
+ }
+ unhook();
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-plus-react.test.js b/packages/integrations/mdx/test/mdx-plus-react.test.js
new file mode 100644
index 000000000..0aa3ed459
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-plus-react.test.js
@@ -0,0 +1,55 @@
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+function hookError() {
+ const error = console.error;
+ const errors = [];
+ console.error = function (...args) {
+ errors.push(args);
+ };
+ return () => {
+ console.error = error;
+ return errors;
+ };
+}
+
+describe('MDX and React', () => {
+ let fixture;
+ let unhook;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-plus-react/', import.meta.url),
+ });
+ unhook = hookError();
+ await fixture.build();
+ });
+
+ it('can be used in the same project', async () => {
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const p = document.querySelector('p');
+
+ assert.equal(p.textContent, 'Hello world');
+ });
+
+ it('mdx renders fine', async () => {
+ const html = await fixture.readFile('/post/index.html');
+ const { document } = parseHTML(html);
+ const h = document.querySelector('#testing');
+ assert.equal(h.textContent, 'Testing');
+ });
+
+ it('does not get a invalid hook call warning', () => {
+ const errors = unhook();
+ assert.equal(errors.length === 0, true);
+ });
+
+ it('renders inline mdx component', async () => {
+ const html = await fixture.readFile('/inline-component/index.html');
+ assert.match(html, /This is an inline component: <span>Comp<\/span>/);
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-script-style-raw.test.js b/packages/integrations/mdx/test/mdx-script-style-raw.test.js
new file mode 100644
index 000000000..dc4f337bf
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-script-style-raw.test.js
@@ -0,0 +1,75 @@
+import * as assert from 'node:assert/strict';
+import { after, before, describe, it } from 'node:test';
+import mdx from '@astrojs/mdx';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+const FIXTURE_ROOT = new URL('./fixtures/mdx-script-style-raw/', import.meta.url);
+
+describe('MDX script style raw', () => {
+ describe('dev', () => {
+ let fixture;
+ let devServer;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ integrations: [mdx()],
+ });
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ it('works with with raw script and style strings', async () => {
+ const res = await fixture.fetch('/index.html');
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const scriptContent = document.getElementById('test-script').innerHTML;
+ assert.equal(
+ scriptContent.includes("console.log('raw script')"),
+ true,
+ 'script should not be html-escaped',
+ );
+
+ const styleContent = document.getElementById('test-style').innerHTML;
+ assert.equal(
+ styleContent.includes('h1[id="script-style-raw"]'),
+ true,
+ 'style should not be html-escaped',
+ );
+ });
+ });
+
+ describe('build', () => {
+ it('works with with raw script and style strings', async () => {
+ const fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ integrations: [mdx()],
+ });
+ await fixture.build();
+
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const scriptContent = document.getElementById('test-script').innerHTML;
+ assert.equal(
+ scriptContent.includes("console.log('raw script')"),
+ true,
+ 'script should not be html-escaped',
+ );
+
+ const styleContent = document.getElementById('test-style').innerHTML;
+ assert.equal(
+ styleContent.includes('h1[id="script-style-raw"]'),
+ true,
+ 'style should not be html-escaped',
+ );
+ });
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-slots.test.js b/packages/integrations/mdx/test/mdx-slots.test.js
new file mode 100644
index 000000000..e8dc68e67
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-slots.test.js
@@ -0,0 +1,125 @@
+import mdx from '@astrojs/mdx';
+
+import * as assert from 'node:assert/strict';
+import { after, before, describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+describe('MDX slots', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-slots/', import.meta.url),
+ integrations: [mdx()],
+ });
+ });
+
+ describe('build', () => {
+ before(async () => {
+ await fixture.build();
+ });
+
+ it('supports top-level imports', async () => {
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('h1');
+ const defaultSlot = document.querySelector('[data-default-slot]');
+ const namedSlot = document.querySelector('[data-named-slot]');
+
+ assert.equal(h1.textContent, 'Hello slotted component!');
+ assert.equal(defaultSlot.textContent, 'Default content.');
+ assert.equal(namedSlot.textContent, 'Content for named slot.');
+ });
+
+ it('supports glob imports - <Component.default />', async () => {
+ const html = await fixture.readFile('/glob/index.html');
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('[data-default-export] h1');
+ const defaultSlot = document.querySelector('[data-default-export] [data-default-slot]');
+ const namedSlot = document.querySelector('[data-default-export] [data-named-slot]');
+
+ assert.equal(h1.textContent, 'Hello slotted component!');
+ assert.equal(defaultSlot.textContent, 'Default content.');
+ assert.equal(namedSlot.textContent, 'Content for named slot.');
+ });
+
+ it('supports glob imports - <Content />', async () => {
+ const html = await fixture.readFile('/glob/index.html');
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('[data-content-export] h1');
+ const defaultSlot = document.querySelector('[data-content-export] [data-default-slot]');
+ const namedSlot = document.querySelector('[data-content-export] [data-named-slot]');
+
+ assert.equal(h1.textContent, 'Hello slotted component!');
+ assert.equal(defaultSlot.textContent, 'Default content.');
+ assert.equal(namedSlot.textContent, 'Content for named slot.');
+ });
+ });
+
+ describe('dev', () => {
+ let devServer;
+
+ before(async () => {
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ it('supports top-level imports', async () => {
+ const res = await fixture.fetch('/');
+
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('h1');
+ const defaultSlot = document.querySelector('[data-default-slot]');
+ const namedSlot = document.querySelector('[data-named-slot]');
+
+ assert.equal(h1.textContent, 'Hello slotted component!');
+ assert.equal(defaultSlot.textContent, 'Default content.');
+ assert.equal(namedSlot.textContent, 'Content for named slot.');
+ });
+
+ it('supports glob imports - <Component.default />', async () => {
+ const res = await fixture.fetch('/glob');
+
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('[data-default-export] h1');
+ const defaultSlot = document.querySelector('[data-default-export] [data-default-slot]');
+ const namedSlot = document.querySelector('[data-default-export] [data-named-slot]');
+
+ assert.equal(h1.textContent, 'Hello slotted component!');
+ assert.equal(defaultSlot.textContent, 'Default content.');
+ assert.equal(namedSlot.textContent, 'Content for named slot.');
+ });
+
+ it('supports glob imports - <Content />', async () => {
+ const res = await fixture.fetch('/glob');
+
+ assert.equal(res.status, 200);
+
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ const h1 = document.querySelector('[data-content-export] h1');
+ const defaultSlot = document.querySelector('[data-content-export] [data-default-slot]');
+ const namedSlot = document.querySelector('[data-content-export] [data-named-slot]');
+
+ assert.equal(h1.textContent, 'Hello slotted component!');
+ assert.equal(defaultSlot.textContent, 'Default content.');
+ assert.equal(namedSlot.textContent, 'Content for named slot.');
+ });
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js b/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js
new file mode 100644
index 000000000..662c1a0cd
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js
@@ -0,0 +1,146 @@
+import mdx from '@astrojs/mdx';
+
+import * as assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import rehypePrettyCode from 'rehype-pretty-code';
+import shikiTwoslash from 'remark-shiki-twoslash';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+const FIXTURE_ROOT = new URL('./fixtures/mdx-syntax-hightlighting/', import.meta.url);
+
+describe('MDX syntax highlighting', () => {
+ describe('shiki', () => {
+ it('works', async () => {
+ const fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ markdown: {
+ syntaxHighlight: 'shiki',
+ },
+ integrations: [mdx()],
+ });
+ await fixture.build();
+
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const shikiCodeBlock = document.querySelector('pre.astro-code');
+ assert.notEqual(shikiCodeBlock, null);
+ assert.equal(shikiCodeBlock.getAttribute('style').includes('background-color:#24292e'), true);
+ });
+
+ it('respects markdown.shikiConfig.theme', async () => {
+ const fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ markdown: {
+ syntaxHighlight: 'shiki',
+ shikiConfig: {
+ theme: 'dracula',
+ },
+ },
+ integrations: [mdx()],
+ });
+ await fixture.build();
+
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const shikiCodeBlock = document.querySelector('pre.astro-code');
+ assert.notEqual(shikiCodeBlock, null);
+ assert.equal(shikiCodeBlock.getAttribute('style').includes('background-color:#282A36'), true);
+ });
+ });
+
+ describe('prism', () => {
+ it('works', async () => {
+ const fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ markdown: {
+ syntaxHighlight: 'prism',
+ },
+ integrations: [mdx()],
+ });
+ await fixture.build();
+
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const prismCodeBlock = document.querySelector('pre.language-astro');
+ assert.notEqual(prismCodeBlock, null);
+ });
+
+ for (const extendMarkdownConfig of [true, false]) {
+ it(`respects syntaxHighlight when extendMarkdownConfig = ${extendMarkdownConfig}`, async () => {
+ const fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ markdown: {
+ syntaxHighlight: 'shiki',
+ },
+ integrations: [
+ mdx({
+ extendMarkdownConfig,
+ syntaxHighlight: 'prism',
+ }),
+ ],
+ });
+ await fixture.build();
+
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const shikiCodeBlock = document.querySelector('pre.astro-code');
+ assert.equal(shikiCodeBlock, null, 'Markdown config syntaxHighlight used unexpectedly');
+ const prismCodeBlock = document.querySelector('pre.language-astro');
+ assert.notEqual(prismCodeBlock, null);
+ });
+ }
+ });
+
+ it('supports custom highlighter - shiki-twoslash', async () => {
+ const fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ markdown: {
+ syntaxHighlight: false,
+ },
+ integrations: [
+ mdx({
+ remarkPlugins: [shikiTwoslash.default ?? shikiTwoslash],
+ }),
+ ],
+ });
+ await fixture.build();
+
+ const html = await fixture.readFile('/index.html');
+ const { document } = parseHTML(html);
+
+ const twoslashCodeBlock = document.querySelector('pre.shiki');
+ assert.notEqual(twoslashCodeBlock, null);
+ });
+
+ it('supports custom highlighter - rehype-pretty-code', async () => {
+ const fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ markdown: {
+ syntaxHighlight: false,
+ },
+ integrations: [
+ mdx({
+ rehypePlugins: [
+ [
+ rehypePrettyCode,
+ {
+ onVisitHighlightedLine(node) {
+ node.properties.style = 'background-color:#000000';
+ },
+ },
+ ],
+ ],
+ }),
+ ],
+ });
+ await fixture.build();
+
+ const html = await fixture.readFile('/index.html');
+ assert.equal(html.includes('style="background-color:#000000"'), true);
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-url-export.test.js b/packages/integrations/mdx/test/mdx-url-export.test.js
new file mode 100644
index 000000000..db7288bff
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-url-export.test.js
@@ -0,0 +1,29 @@
+import mdx from '@astrojs/mdx';
+
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+describe('MDX url export', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-url-export/', import.meta.url),
+ integrations: [mdx()],
+ });
+
+ await fixture.build();
+ });
+
+ it('generates correct urls in glob result', async () => {
+ const { urls } = JSON.parse(await fixture.readFile('/pages.json'));
+ assert.equal(urls.includes('/test-1'), true);
+ assert.equal(urls.includes('/test-2'), true);
+ });
+
+ it('respects "export url" overrides in glob result', async () => {
+ const { urls } = JSON.parse(await fixture.readFile('/pages.json'));
+ assert.equal(urls.includes('/AH!'), true);
+ });
+});
diff --git a/packages/integrations/mdx/test/mdx-vite-env-vars.test.js b/packages/integrations/mdx/test/mdx-vite-env-vars.test.js
new file mode 100644
index 000000000..213386ceb
--- /dev/null
+++ b/packages/integrations/mdx/test/mdx-vite-env-vars.test.js
@@ -0,0 +1,65 @@
+import * as assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { parseHTML } from 'linkedom';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+describe('MDX - Vite env vars', () => {
+ let fixture;
+ before(async () => {
+ fixture = await loadFixture({
+ root: new URL('./fixtures/mdx-vite-env-vars/', import.meta.url),
+ });
+ await fixture.build();
+ });
+
+ it('Avoids transforming `import.meta.env` outside JSX expressions', async () => {
+ const html = await fixture.readFile('/vite-env-vars/index.html');
+ const { document } = parseHTML(html);
+
+ assert.equal(document.querySelector('h1')?.innerHTML.includes('import.meta.env.SITE'), true);
+ assert.equal(document.querySelector('code')?.innerHTML.includes('import.meta.env.SITE'), true);
+ assert.equal(document.querySelector('pre')?.innerHTML.includes('import.meta.env.SITE'), true);
+ });
+ it('Allows referencing `import.meta.env` in frontmatter', async () => {
+ const { title = '' } = JSON.parse(await fixture.readFile('/frontmatter.json'));
+ assert.equal(title.includes('import.meta.env.SITE'), true);
+ });
+ it('Transforms `import.meta.env` in {JSX expressions}', async () => {
+ const html = await fixture.readFile('/vite-env-vars/index.html');
+ const { document } = parseHTML(html);
+
+ assert.equal(
+ document
+ .querySelector('[data-env-site]')
+ ?.innerHTML.includes('https://mdx-is-neat.com/blog/cool-post'),
+ true,
+ );
+ });
+ it('Transforms `import.meta.env` in variable exports', async () => {
+ const html = await fixture.readFile('/vite-env-vars/index.html');
+ const { document } = parseHTML(html);
+
+ assert.equal(
+ document.querySelector('[data-env-variable-exports]')?.innerHTML.includes('MODE works'),
+ true,
+ );
+ assert.equal(
+ document
+ .querySelector('[data-env-variable-exports-unknown]')
+ ?.innerHTML.includes('exports: ""'),
+ true,
+ );
+ });
+ it('Transforms `import.meta.env` in HTML attributes', async () => {
+ const html = await fixture.readFile('/vite-env-vars/index.html');
+ const { document } = parseHTML(html);
+
+ const dataAttrDump = document.querySelector('[data-env-dump]');
+ assert.notEqual(dataAttrDump, null);
+
+ assert.equal(dataAttrDump.getAttribute('data-env-prod'), 'true');
+ assert.equal(dataAttrDump.getAttribute('data-env-dev'), 'false');
+ assert.equal(dataAttrDump.getAttribute('data-env-base-url'), '/');
+ assert.equal(dataAttrDump.getAttribute('data-env-mode'), 'production');
+ });
+});
diff --git a/packages/integrations/mdx/test/remark-imgattr.test.js b/packages/integrations/mdx/test/remark-imgattr.test.js
new file mode 100644
index 000000000..ebd9207b2
--- /dev/null
+++ b/packages/integrations/mdx/test/remark-imgattr.test.js
@@ -0,0 +1,49 @@
+import * as assert from 'node:assert/strict';
+import { after, before, describe, it } from 'node:test';
+import * as cheerio from 'cheerio';
+import { loadFixture } from '../../../astro/test/test-utils.js';
+
+const FIXTURE_ROOT = new URL('./fixtures/image-remark-imgattr/', import.meta.url);
+
+describe('Testing remark plugins for image processing', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+
+ describe('start dev server', () => {
+ /** @type {import('./test-utils').DevServer} */
+ let devServer;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: FIXTURE_ROOT,
+ });
+
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ describe('Test image attributes can be added by remark plugins', () => {
+ let $;
+ before(async () => {
+ let res = await fixture.fetch('/');
+ let html = await res.text();
+ $ = cheerio.load(html);
+ });
+
+ it('<img> has correct attributes', async () => {
+ let $img = $('img');
+ assert.equal($img.attr('id'), 'test');
+ assert.equal($img.attr('sizes'), '(min-width: 600px) 600w, 300w');
+ assert.ok($img.attr('srcset'));
+ });
+
+ it('<img> was processed properly', async () => {
+ let $img = $('img');
+ assert.equal(new URL($img.attr('src'), 'http://example.com').searchParams.get('w'), '300');
+ });
+ });
+ });
+});
diff --git a/packages/integrations/mdx/test/units/rehype-optimize-static.test.js b/packages/integrations/mdx/test/units/rehype-optimize-static.test.js
new file mode 100644
index 000000000..6121975a4
--- /dev/null
+++ b/packages/integrations/mdx/test/units/rehype-optimize-static.test.js
@@ -0,0 +1,89 @@
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import { compile as _compile } from '@mdx-js/mdx';
+import { rehypeOptimizeStatic } from '../../dist/rehype-optimize-static.js';
+
+/**
+ * @param {string} mdxCode
+ * @param {Readonly<import('@mdx-js/mdx').CompileOptions>} options
+ */
+async function compile(mdxCode, options) {
+ const result = await _compile(mdxCode, {
+ jsx: true,
+ rehypePlugins: [rehypeOptimizeStatic],
+ ...options,
+ });
+ const code = result.toString();
+ // Capture the returned JSX code for testing
+ const jsx = /return (.+);\n\}\nexport default function MDXContent/s.exec(code)?.[1];
+ if (jsx == null) throw new Error('Could not find JSX code in compiled MDX');
+ return dedent(jsx);
+}
+
+function dedent(str) {
+ const lines = str.split('\n');
+ if (lines.length <= 1) return str;
+ // Get last line indent, and dedent this amount for the other lines
+ const lastLineIndent = lines[lines.length - 1].match(/^\s*/)[0].length;
+ return lines.map((line, i) => (i === 0 ? line : line.slice(lastLineIndent))).join('\n');
+}
+
+describe('rehype-optimize-static', () => {
+ it('works', async () => {
+ const jsx = await compile(`# hello`);
+ assert.equal(
+ jsx,
+ `\
+<_components.h1 {...{
+ "set:html": "hello"
+}} />`,
+ );
+ });
+
+ it('groups sibling nodes as a single Fragment', async () => {
+ const jsx = await compile(`\
+# hello
+
+foo bar
+`);
+ assert.equal(
+ jsx,
+ `\
+<Fragment set:html="<h1>hello</h1>
+<p>foo bar</p>" />`,
+ );
+ });
+
+ it('skips optimization of components', async () => {
+ const jsx = await compile(`\
+import Comp from './Comp.jsx';
+
+# hello
+
+This is a <Comp />
+`);
+ assert.equal(
+ jsx,
+ `\
+<><Fragment set:html="<h1>hello</h1>
+" /><_components.p>{"This is a "}<Comp /></_components.p></>`,
+ );
+ });
+
+ it('optimizes explicit html elements', async () => {
+ const jsx = await compile(`\
+# hello
+
+foo <strong>bar</strong> baz
+
+<strong>qux</strong>
+`);
+ assert.equal(
+ jsx,
+ `\
+<Fragment set:html="<h1>hello</h1>
+<p>foo <strong>bar</strong> baz</p>
+<strong>qux</strong>" />`,
+ );
+ });
+});