diff --git a/integration-tests/e2e/mdx-imports/mdx-imports.test.ts b/integration-tests/e2e/mdx-imports/mdx-imports.test.ts index 1e4d136a..b6a5b35d 100644 --- a/integration-tests/e2e/mdx-imports/mdx-imports.test.ts +++ b/integration-tests/e2e/mdx-imports/mdx-imports.test.ts @@ -43,6 +43,14 @@ test.describe('MDX imports', () => { expect(html).toContain('Delightful docs. Mintlify drop in replacement as a Vite plugin') }) + test('renders nested imports inside imported .md snippets', async ({ request }) => { + const response = await request.get('/') + expect(response.status()).toBe(200) + const html = await response.text() + expect(html).toContain('Outer imported markdown body.') + expect(html).toContain('Nested imported markdown works in the browser.') + }) + test('renders named import from /components/', async ({ request }) => { const response = await request.get('/') expect(response.status()).toBe(200) diff --git a/integration-tests/fixtures/mdx-imports/index.mdx b/integration-tests/fixtures/mdx-imports/index.mdx index 258a2dff..0d1300c2 100644 --- a/integration-tests/fixtures/mdx-imports/index.mdx +++ b/integration-tests/fixtures/mdx-imports/index.mdx @@ -7,6 +7,7 @@ import { CustomBadge } from '/components/custom-badge' import Alert from '/snippets/alert' import MarkdownSnippet from '/snippets/plain-markdown.md' import RootReadme from '../../../README.md' +import NestedMarkdown from '/snippets/nested-outer.md' @@ -27,3 +28,5 @@ This page tests MDX import resolution. + + diff --git a/integration-tests/fixtures/mdx-imports/snippets/nested-inner.md b/integration-tests/fixtures/mdx-imports/snippets/nested-inner.md new file mode 100644 index 00000000..1ea2afca --- /dev/null +++ b/integration-tests/fixtures/mdx-imports/snippets/nested-inner.md @@ -0,0 +1 @@ +Nested imported markdown works in the browser. diff --git a/integration-tests/fixtures/mdx-imports/snippets/nested-outer.md b/integration-tests/fixtures/mdx-imports/snippets/nested-outer.md new file mode 100644 index 00000000..27242a03 --- /dev/null +++ b/integration-tests/fixtures/mdx-imports/snippets/nested-outer.md @@ -0,0 +1,5 @@ +import Inner from './nested-inner.md' + +Outer imported markdown body. + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0aeb54ad..4736704a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,7 +31,7 @@ importers: version: 6.9.0 spiceflow: specifier: 1.20.0-rsc.1 - version: 1.20.0-rsc.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(zod@4.4.1) + version: 1.20.0-rsc.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(zod@4.4.1) string-dedent: specifier: ^3.0.2 version: 3.0.2 @@ -118,7 +118,7 @@ importers: devDependencies: '@cloudflare/vite-plugin': specifier: ^1.32.2 - version: 1.32.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260410.1)(wrangler@4.82.2) + version: 1.32.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260410.1)(wrangler@4.82.2(@cloudflare/workers-types@4.20260409.1)) '@cloudflare/workers-types': specifier: ^4.20260408.0 version: 4.20260409.1 @@ -176,7 +176,7 @@ importers: version: 0.8.2 rollup-plugin-visualizer: specifier: ^7.0.1 - version: 7.0.1(rolldown@1.0.0-rc.15) + version: 7.0.1(rolldown@1.0.0-rc.17) vite: dependencies: @@ -277,8 +277,8 @@ importers: specifier: ^3.1.1 version: 3.1.1 safe-mdx: - specifier: ^1.8.0 - version: 1.8.0(react@19.2.5) + specifier: github:remorses/safe-mdx#recursive-mdx-modules + version: https://codeload.github.com/remorses/safe-mdx/tar.gz/6626a58fe17a89154adbc67960449dd3197cbc85(react@19.2.5) string-dedent: specifier: ^3.0.2 version: 3.0.2 @@ -406,7 +406,7 @@ importers: devDependencies: '@cloudflare/vite-plugin': specifier: ^1.32.2 - version: 1.32.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260410.1)(wrangler@4.82.2) + version: 1.32.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260410.1)(wrangler@4.82.2(@cloudflare/workers-types@4.20260409.1)) '@types/node': specifier: ^25.6.0 version: 25.6.0 @@ -535,8 +535,8 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.2': - resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} hasBin: true @@ -722,9 +722,15 @@ packages: '@drizzle-team/brocli@0.11.0': resolution: {integrity: sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} @@ -1445,6 +1451,12 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@noble/ciphers@2.2.0': resolution: {integrity: sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==} engines: {node: '>= 20.19.0'} @@ -1468,6 +1480,9 @@ packages: '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@playwright/test@1.59.1': resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} engines: {node: '>=18'} @@ -2178,30 +2193,60 @@ packages: cpu: [arm64] os: [android] + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-rc.15': resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2209,6 +2254,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2216,6 +2268,13 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2223,6 +2282,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2230,6 +2296,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2237,6 +2310,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2244,32 +2324,65 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} engines: {node: '>=14.0.0'} cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/pluginutils@1.0.0-rc.15': resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} + '@rolldown/pluginutils@1.0.0-rc.5': resolution: {integrity: sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw==} @@ -3990,6 +4103,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanostores@1.3.0: resolution: {integrity: sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA==} engines: {node: ^20.0.0 || >=22.0.0} @@ -4102,6 +4220,10 @@ packages: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} + postcss@8.5.13: + resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.9: resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} @@ -4249,6 +4371,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup-plugin-visualizer@7.0.1: resolution: {integrity: sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==} engines: {node: '>=22'} @@ -4278,8 +4405,9 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-mdx@1.8.0: - resolution: {integrity: sha512-LreURjpkNuLYVZT59ygJQaxER82VQLG/iBOPJ1y+/r9dNlYQizXZxMu0H0bdB6DOf12KGFhOiaHXRILyYEKbpw==} + safe-mdx@https://codeload.github.com/remorses/safe-mdx/tar.gz/6626a58fe17a89154adbc67960449dd3197cbc85: + resolution: {tarball: https://codeload.github.com/remorses/safe-mdx/tar.gz/6626a58fe17a89154adbc67960449dd3197cbc85} + version: 1.9.0 peerDependencies: react: '*' @@ -4565,6 +4693,49 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@8.0.10: + resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vite@8.0.8: resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4952,7 +5123,7 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} - '@babel/parser@7.29.2': + '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 @@ -5064,7 +5235,7 @@ snapshots: optionalDependencies: workerd: 1.20260410.1 - '@cloudflare/vite-plugin@1.32.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260410.1)(wrangler@4.82.2)': + '@cloudflare/vite-plugin@1.32.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(workerd@1.20260410.1)(wrangler@4.82.2(@cloudflare/workers-types@4.20260409.1))': dependencies: '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260410.1) miniflare: 4.20260410.0 @@ -5100,12 +5271,23 @@ snapshots: '@drizzle-team/brocli@0.11.0': {} + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.9.2': dependencies: '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 @@ -5556,6 +5738,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + '@noble/ciphers@2.2.0': {} '@noble/hashes@2.2.0': {} @@ -5568,6 +5757,8 @@ snapshots: '@oxc-project/types@0.124.0': {} + '@oxc-project/types@0.127.0': {} + '@playwright/test@1.59.1': dependencies: playwright: 1.59.1 @@ -6334,39 +6525,75 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-rc.15': optional: true + '@rolldown/binding-android-arm64@1.0.0-rc.17': + optional: true + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': optional: true + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + optional: true + '@rolldown/binding-darwin-x64@1.0.0-rc.15': optional: true + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + optional: true + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': optional: true + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': dependencies: '@emnapi/core': 1.9.2 @@ -6374,14 +6601,29 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + optional: true + '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rolldown/pluginutils@1.0.0-rc.17': {} + '@rolldown/pluginutils@1.0.0-rc.5': {} '@rolldown/pluginutils@1.0.0-rc.7': {} @@ -6752,6 +6994,21 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + '@vitejs/plugin-rsc@0.5.21(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.5 + es-module-lexer: 2.1.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + periscopic: 4.0.3 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + srvx: 0.11.15 + strip-literal: 3.1.0 + turbo-stream: 3.2.0 + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vitefu: 1.1.3(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitejs/plugin-rsc@0.5.21(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.5 @@ -8282,6 +8539,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@3.3.12: {} + nanostores@1.3.0: {} napi-build-utils@2.0.0: @@ -8402,6 +8661,12 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss@8.5.13: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.9: dependencies: nanoid: 3.3.11 @@ -8660,14 +8925,35 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 - rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.15): + rolldown@1.0.0-rc.17: + dependencies: + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + + rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.17): dependencies: open: 11.0.0 picomatch: 4.0.4 source-map: 0.7.6 yargs: 18.0.0 optionalDependencies: - rolldown: 1.0.0-rc.15 + rolldown: 1.0.0-rc.17 rou3@0.7.12: {} @@ -8684,9 +8970,9 @@ snapshots: safe-buffer@5.2.1: {} - safe-mdx@1.8.0(react@19.2.5): + safe-mdx@https://codeload.github.com/remorses/safe-mdx/tar.gz/6626a58fe17a89154adbc67960449dd3197cbc85(react@19.2.5): dependencies: - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.3 '@babel/types': 7.29.0 '@standard-schema/spec': 1.1.0 collapse-white-space: 2.1.0 @@ -8783,6 +9069,25 @@ snapshots: - react-server-dom-webpack - vite + spiceflow@1.20.0-rsc.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(zod@4.4.1): + dependencies: + '@vitejs/plugin-rsc': 0.5.21(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + errore: 0.14.1 + eventsource-parser: 3.0.8 + history: 5.3.0 + isbot: 4.4.0 + its-fine: 2.0.0(@types/react@19.2.14)(react@19.2.5) + nf3: 0.3.16 + openapi-types: 12.1.3 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + superjson: 2.2.6 + zod: 4.4.1 + transitivePeerDependencies: + - '@types/react' + - react-server-dom-webpack + - vite + sprintf-js@1.1.3: {} sql.js@1.14.1: {} @@ -9032,6 +9337,21 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.13 + rolldown: 1.0.0-rc.17 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.6.0 + esbuild: 0.27.4 + fsevents: 2.3.3 + jiti: 2.6.1 + tsx: 4.21.0 + yaml: 2.8.3 + vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -9047,6 +9367,10 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 + vitefu@1.1.3(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): + optionalDependencies: + vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vitefu@1.1.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): optionalDependencies: vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8ace6fc4..f98c5e7f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,5 @@ packages: - ./* + +onlyBuiltDependencies: + - safe-mdx diff --git a/vite/package.json b/vite/package.json index 6012c32c..a0f5a43f 100644 --- a/vite/package.json +++ b/vite/package.json @@ -91,7 +91,7 @@ "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", "remark-mdx": "^3.1.1", - "safe-mdx": "^1.8.0", + "safe-mdx": "github:remorses/safe-mdx#recursive-mdx-modules", "string-dedent": "^3.0.2", "tailwindcss": "^4.2.2", "takumi-js": "1.0.0-rc.15", diff --git a/vite/src/app-factory.tsx b/vite/src/app-factory.tsx index c7943f92..ac274dad 100644 --- a/vite/src/app-factory.tsx +++ b/vite/src/app-factory.tsx @@ -16,7 +16,7 @@ import type { ApiApp as WebsiteApiApp } from 'website/src/api.ts' import { Spiceflow, type AnySpiceflow, redirect } from 'spiceflow' import { createSpiceflowFetch } from 'spiceflow/client' import { Head, ProgressBar } from 'spiceflow/react' -import { mdxParse, resolveModules, type EagerModules } from 'safe-mdx/parse' +import { mdxParse, resolveModules, type EagerModules, type MdxModuleFile } from 'safe-mdx/parse' import { parse as parseCookies } from 'cookie' import type { Root } from 'mdast' import { @@ -45,7 +45,7 @@ import { zipSync, strToU8 } from 'fflate' import { buildSections, isAboveNode } from './lib/mdx-sections.ts' import { computeSidebarWidthFromAsideNodes } from './lib/sidebar-widths.ts' import { visit } from 'unist-util-visit' -import { RenderNodes } from './lib/mdx-components-map.tsx' +import { RenderImportedMdx, RenderNodes } from './lib/mdx-components-map.tsx' import { decodeGeneratedLogoText, type GeneratedLogoTheme, @@ -161,6 +161,7 @@ type HolocronProviders = { /** Lazy glob of importable files (snippets, components, colocated pages). * Used by resolveModules() to resolve MDX import statements at render time. */ getModules?(): Record Promise>> + getImportedMdxFiles?(): Record /** Pages directory relative to root with ./ prefix and trailing slash. * E.g. './pages/' or './' when pagesDir is the project root. */ pagesDirPrefix?: string @@ -641,7 +642,19 @@ export async function createHolocronApp(providers: HolocronProviders): Promise) { + return + }, + } + }, + }) } return renderMdxPage({ site, slug, pageMdx, loaderData, bannerJsx, ogImageUrl, modules, pagesDirPrefix: providers.pagesDirPrefix, preParsedMdast }) diff --git a/vite/src/app.tsx b/vite/src/app.tsx index 7dfabcb0..75336849 100644 --- a/vite/src/app.tsx +++ b/vite/src/app.tsx @@ -8,7 +8,7 @@ import { createHolocronApp, type HolocronApp } from './app-factory.tsx' import { base, getConfig } from 'virtual:holocron-config' import { getNavigationData } from 'virtual:holocron-navigation' import { getMdxSlugs, getMdxSource, getPageIconRefs } from 'virtual:holocron-mdx' -import { getModules, pagesDirPrefix } from 'virtual:holocron-modules' +import { getImportedMdxFiles, getModules, pagesDirPrefix } from 'virtual:holocron-modules' export const app = await createHolocronApp({ base, @@ -18,6 +18,7 @@ export const app = await createHolocronApp({ getMdxSource, getPageIconRefs, getModules, + getImportedMdxFiles, pagesDirPrefix, }) export type App = HolocronApp diff --git a/vite/src/lib/mdx-components-map.tsx b/vite/src/lib/mdx-components-map.tsx index 52419b4a..9b1434bd 100644 --- a/vite/src/lib/mdx-components-map.tsx +++ b/vite/src/lib/mdx-components-map.tsx @@ -314,19 +314,22 @@ export function RenderNodes({ markdown, nodes, modules, baseUrl }: { /** Render MDX imported from another MDX file, e.g. * `import Snippet from '/snippets/example.mdx'` followed by ``. - * Vite doesn't compile user MDX snippets as JSX, so the virtual modules map - * exposes raw markdown and this component renders it through the same safe-mdx - * component map used by pages. */ -export function RenderImportedMdx({ markdown, baseUrl }: { + * Vite doesn't compile user MDX snippets as JSX, so Holocron preprocesses + * them into markdown strings and this component renders them through the same + * safe-mdx component map used by pages. */ +export function RenderImportedMdx({ markdown, baseUrl, mdast, modules }: { markdown: string baseUrl?: string + mdast?: Root + modules?: EagerModules }) { return ( diff --git a/vite/src/lib/sync.test.ts b/vite/src/lib/sync.test.ts index 79116b5d..5c8815bc 100644 --- a/vite/src/lib/sync.test.ts +++ b/vite/src/lib/sync.test.ts @@ -352,6 +352,60 @@ import { Widget } from '/components/widget' expect(second.pageImports['index']?.map((i) => i.moduleKey)).toEqual(['./components/widget.tsx']) }) + test('preprocesses imported markdown snippets and resolves nested imports', async () => { + const project = tracked(createProject( + { + navigation: [ + { group: 'Guide', pages: ['index'] }, + ], + }, + { + index: `--- +title: Home +--- + +import Outer from '/snippets/outer.md' + +# Home + + +`, + }, + )) + + const snippetsDir = path.join(project.pagesDir, 'snippets') + fs.mkdirSync(snippetsDir, { recursive: true }) + fs.writeFileSync(path.join(snippetsDir, 'outer.md'), `import Inner from './inner.md' + +
+Outer summary + +Outer body. +
+ + +`) + fs.writeFileSync(path.join(snippetsDir, 'inner.md'), 'Nested imported markdown works.') + + const config = readConfig({ root: project.root }) + const result = await syncNavigation({ + config, + pagesDir: project.pagesDir, + publicDir: project.publicDir, + projectRoot: project.root, + distDir: project.distDir, + }) + + expect(Object.keys(result.importedMdxContent).sort()).toMatchInlineSnapshot(` + [ + "./pages/snippets/inner.md", + "./snippets/outer.md", + ] + `) + expect(result.importedMdxContent['./snippets/outer.md']).toContain(' i.moduleKey)).toEqual(['./pages/snippets/inner.md']) + }) + test('preserves typed page frontmatter metadata on NavPage', async () => { const project = tracked(createProject( { diff --git a/vite/src/lib/sync.ts b/vite/src/lib/sync.ts index 4f158113..cff6d847 100644 --- a/vite/src/lib/sync.ts +++ b/vite/src/lib/sync.ts @@ -103,6 +103,10 @@ export type SyncResult = { * and an `absPath` (absolute filesystem path for the import() call). * Built fresh on every sync from cached importSources + filesystem probing. */ pageImports: Record + /** Pre-processed imported `.md`/`.mdx` snippets keyed by safe-mdx module key. */ + importedMdxContent: Record + /** Imports discovered inside imported `.md`/`.mdx` snippets, keyed by module key. */ + importedMdxImports: Record parsedCount: number cachedCount: number } @@ -146,12 +150,65 @@ export async function syncNavigation({ const pageImportSources: Record = {} /** Resolved imports per page (computed fresh every sync from pageImportSources + filesystem) */ const pageImports: Record = {} + const importedMdxContent: Record = {} + const importedMdxImports: Record = {} const redirectBackedPageSlugs = new Set( config.redirects .map((rule) => redirectSourceToSlug(rule.source)) .filter(Boolean), ) + async function preprocessMdxContent({ content, mdxDir }: { content: string; mdxDir: string }) { + const processed = processMdx(content, config.icons.library) + const resolvedImages = new Map() + + // Resolve and process each image + for (const src of processed.imageSrcs) { + let meta + try { + if (src.startsWith('http://') || src.startsWith('https://')) { + const remoteBuffer = await fetchRemoteImageBuffer(src) + if (!remoteBuffer) { + continue + } + meta = await processImageBuffer({ buffer: remoteBuffer, cache: imageCache }) + if (meta) { + resolvedImages.set(src, { publicSrc: src, meta }) + } + continue + } + + const resolved = resolveImagePath({ src, mdxDir, publicDir, projectRoot }) + if (!resolved) { + continue + } + + meta = await processImage({ filePath: resolved.filePath, cache: imageCache }) + if (!meta) { + continue + } + + const publicSrc = resolved.needsCopy + ? `/_holocron/images/${copyToPublic({ filePath: resolved.filePath, imageOutputDir })}` + : src + + resolvedImages.set(src, { publicSrc, meta }) + } catch (e) { + logger.warn(formatHolocronWarning( + `failed to process image ${src}: ${e instanceof Error ? e.message : String(e)}`, + )) + continue + } + } + + return { + processed, + content: resolvedImages.size > 0 + ? rewriteMdxImages(processed.mdast, resolvedImages) + : processed.normalizedContent, + } + } + // 2. Enrich a single page slug async function enrichPage(slug: string): Promise { // Virtual pages (e.g. from OpenAPI) already have content in mdxContent @@ -192,65 +249,18 @@ export async function syncNavigation({ // without re-parsing the MDX. const cachedSources = oldPageImportSources[slug] ?? [] pageImportSources[slug] = cachedSources - pageImports[slug] = resolveImportSources({ importSources: cachedSources, slug, pagesDir, projectRoot }) + pageImports[slug] = resolveImportSourcesFromFile({ importSources: cachedSources, importerPath: mdxPath, pagesDir, projectRoot }) return cached } // Cache miss — full processing - const processed = processMdx(content, config.icons.library) + const { processed, content: finalMdx } = await preprocessMdxContent({ content, mdxDir: path.dirname(mdxPath) }) parsedCount++ - const mdxDir = path.dirname(mdxPath) - const resolvedImages = new Map() - - // Resolve and process each image - for (const src of processed.imageSrcs) { - let meta - try { - if (src.startsWith('http://') || src.startsWith('https://')) { - const remoteBuffer = await fetchRemoteImageBuffer(src) - if (!remoteBuffer) { - continue - } - meta = await processImageBuffer({ buffer: remoteBuffer, cache: imageCache }) - if (meta) { - resolvedImages.set(src, { publicSrc: src, meta }) - } - continue - } - - const resolved = resolveImagePath({ src, mdxDir, publicDir, projectRoot }) - if (!resolved) { - continue - } - - meta = await processImage({ filePath: resolved.filePath, cache: imageCache }) - if (!meta) { - continue - } - - const publicSrc = resolved.needsCopy - ? `/_holocron/images/${copyToPublic({ filePath: resolved.filePath, imageOutputDir })}` - : src - - resolvedImages.set(src, { publicSrc, meta }) - } catch (e) { - logger.warn(formatHolocronWarning( - `failed to process image ${src}: ${e instanceof Error ? e.message : String(e)}`, - )) - continue - } - } - - // Mutate mdast tree: rewrite image paths + inject dimensions, serialize back - const finalMdx = resolvedImages.size > 0 - ? rewriteMdxImages(processed.mdast, resolvedImages) - : processed.normalizedContent - pageIconRefs[slug] = processed.iconRefs // Cache raw import sources (for future cache hits) and resolve fresh pageImportSources[slug] = processed.importSources - pageImports[slug] = resolveImportSources({ importSources: processed.importSources, slug, pagesDir, projectRoot }) + pageImports[slug] = resolveImportSourcesFromFile({ importSources: processed.importSources, importerPath: mdxPath, pagesDir, projectRoot }) // Store MDX content separately from the nav tree mdxContent[slug] = finalMdx @@ -267,6 +277,35 @@ export async function syncNavigation({ } } + async function processImportedMdxFiles() { + const queue = Object.values(pageImports).flat() + const seenMdx = new Set() + + for (let i = 0; i < queue.length; i++) { + const entry = queue[i]! + if (!/\.mdx?$/.test(entry.absPath) || seenMdx.has(entry.moduleKey)) { + continue + } + seenMdx.add(entry.moduleKey) + + const content = fs.readFileSync(entry.absPath, 'utf-8') + const { processed, content: finalMdx } = await preprocessMdxContent({ + content, + mdxDir: path.dirname(entry.absPath), + }) + + importedMdxContent[entry.moduleKey] = finalMdx + const imports = resolveImportSourcesFromFile({ + importSources: processed.importSources, + importerPath: entry.absPath, + pagesDir, + projectRoot, + }) + importedMdxImports[entry.moduleKey] = imports + queue.push(...imports) + } + } + // 2b. Process virtual tabs (OpenAPI, etc.) — populate groups + inject virtual MDX pages await processVirtualTabs({ config, @@ -279,6 +318,8 @@ export async function syncNavigation({ const { navigation, switchers } = await buildEnrichedNavigation({ config, enrichPage }) const { versions, dropdowns } = switchers + await processImportedMdxFiles() + // 4c. Validate no duplicate page hrefs across versions/dropdowns if (versions.length > 0 || dropdowns.length > 0) { const hrefOwners = new Map() @@ -319,7 +360,7 @@ export async function syncNavigation({ writeMdxCache(mdxCachePath, { content: mdxContent, pageIconRefs, pageImportSources }) saveImageCache({ distDir, cache: imageCache }) - return { navigation, switchers, mdxContent, pageIconRefs, pageImports, parsedCount, cachedCount } + return { navigation, switchers, mdxContent, pageIconRefs, pageImports, importedMdxContent, importedMdxImports, parsedCount, cachedCount } } /* ── Image path resolution ───────────────────────────────────────────── */ @@ -523,25 +564,20 @@ export type ResolvedImport = { * We replicate that normalization here so the virtual:holocron-modules glob * keys exactly match what safe-mdx will look up at render time. */ -function resolveImportSources({ +function resolveImportSourcesFromFile({ importSources, - slug, + importerPath, pagesDir, projectRoot, }: { importSources: string[] - slug: string + importerPath: string pagesDir: string projectRoot: string }): ResolvedImport[] { const result: ResolvedImport[] = [] const seen = new Set() - // Directory containing the MDX file for this slug - const slugDir = slug.includes('/') ? slug.slice(0, slug.lastIndexOf('/')) : '' - const mdxDir = path.join(pagesDir, slugDir) - // pagesDirPrefix as computed by vite-plugin.ts (e.g. './pages/' or './') - const pagesDirRelative = path.relative(projectRoot, pagesDir) - const pagesDirPrefix = pagesDirRelative === '' ? './' : `./${pagesDirRelative}/` + const mdxDir = path.dirname(importerPath) for (const source of importSources) { if (source.startsWith('/')) { diff --git a/vite/src/virtual.d.ts b/vite/src/virtual.d.ts index 67cbf64f..e7c01096 100644 --- a/vite/src/virtual.d.ts +++ b/vite/src/virtual.d.ts @@ -33,6 +33,7 @@ declare module 'virtual:holocron-modules' { * Keys are relative paths from the Vite root (e.g. './snippets/card.tsx'). * Values are lazy loaders — call `await loader()` to get the module exports. */ export function getModules(): Record Promise>> + export function getImportedMdxFiles(): Record /** Pages directory relative to root, with ./ prefix and trailing slash. * E.g. './pages/' or './' when pagesDir is the project root. */ export const pagesDirPrefix: string diff --git a/vite/src/vite-plugin.ts b/vite/src/vite-plugin.ts index bf1de05b..fe415143 100644 --- a/vite/src/vite-plugin.ts +++ b/vite/src/vite-plugin.ts @@ -431,32 +431,41 @@ export function holocron(options: HolocronPluginOptions = {}): PluginOption { } } } + for (const imports of Object.values(syncResult.importedMdxImports)) { + for (const { moduleKey, absPath } of imports) { + if (!allImports.has(moduleKey)) { + allImports.set(moduleKey, absPath) + } + } + } const sortedImports = [...allImports.entries()] .sort(([a], [b]) => a.localeCompare(b)) - const hasMdxImports = sortedImports.some(([, absPath]) => /\.mdx?$/.test(absPath)) - const entries = sortedImports.map(([moduleKey, absPath]) => { - if (/\.mdx?$/.test(absPath)) { - const baseUrl = './' + path.relative(root, path.dirname(absPath)).replace(/\\/g, '/') + '/' - return [ - ` ${JSON.stringify(moduleKey)}: async () => {`, - ` const markdown = (await import(${JSON.stringify(absPath + '?raw')})).default`, - ` return { default: function ImportedMdx(props) {`, - ` return React.createElement(RenderImportedMdx, { ...props, markdown, baseUrl: ${JSON.stringify(baseUrl)} })`, - ` } }`, - ` }`, - ].join('\n') - } + for (const absPath of allImports.values()) { + this.addWatchFile(absPath) + } + const importedMdxFiles = Object.fromEntries( + Object.entries(syncResult.importedMdxContent).map(([moduleKey, markdown]) => { + const absPath = allImports.get(moduleKey) + const baseUrl = absPath + ? './' + path.relative(root, path.dirname(absPath)).replace(/\\/g, '/') + '/' + : './' + return [moduleKey, { markdown, baseUrl }] + }), + ) + const entries = sortedImports + .filter(([, absPath]) => !/\.mdx?$/.test(absPath)) + .map(([moduleKey, absPath]) => { return ` ${JSON.stringify(moduleKey)}: () => import(${JSON.stringify(absPath)})` }) return [ - hasMdxImports ? `import React from 'react'` : undefined, - hasMdxImports ? `import { RenderImportedMdx } from '@holocron.so/vite/src/lib/mdx-components-map'` : undefined, + `const importedMdxFiles = ${JSON.stringify(importedMdxFiles)}`, `const modules = {`, entries.join(',\n'), `}`, `export function getModules() { return modules }`, + `export function getImportedMdxFiles() { return importedMdxFiles }`, `export const pagesDirPrefix = ${JSON.stringify(pagesDirPrefix)}`, ].filter(Boolean).join('\n') }