diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 30934211c..e6a8c4a08 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -47,8 +47,3 @@ jobs: run: yarn playwright install --with-deps - name: Vitest run: yarn ci:test - - name: Commit and Push changes - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: "chore: update vitest screenshots [skip ci]" - file_pattern: "**/__screenshots__/**/*.png" diff --git a/AGENTS.md b/AGENTS.md index eef55bc62..e87778a22 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,6 +52,7 @@ __tests__/ → Vitest browser-mode tests (Playwright) - `.res` files must always be capitalized (PascalCase), matching ReScript module conventions. - Use the pipe-first operator (`->`) for chaining, which is idiomatic ReScript. - Resolve all warnings and treat them as errors. The project has `"error": "+8"` in `rescript.json`. +- Never directly edit an `.jsx` files, you must edit the corresponding `.res` file. The `.jsx` files are generated by the ReScript compiler and will be overwritten on the next compile. ## ReScript Rules @@ -64,6 +65,9 @@ __tests__/ → Vitest browser-mode tests (Playwright) - Output format is ES modules with `.jsx` suffix, compiled in-source (`.jsx` files sit alongside `.res` files). - Reference the abridged documentation for clarification on how ReScript's APIs work: https://rescript-lang.org/llms/manual/llm-small.txt - If you need more information you can access the full documentation, but do this only when needed as the docs are very large: https://rescript-lang.org/llms/manual/llm-full.txt +- Never use `%raw` unless you are specifically asked to +- Never use `Object.magic` +- Don't add type annotations unless necessary for clarity or to resolve an error. ReScript's type inference is powerful, and often explicit annotations are not needed. ### ReScript Dependencies @@ -178,8 +182,10 @@ let default: unit => React.element }) ``` -- Components requiring React Router context must be wrapped in ``. -- Run tests with `yarn vitest`. +- Components requiring React Router context must be wrapped in ``. +- Do not use `` in tests. +- Run tests with `yarn vitest --browser.headless --run`. +- Do not update snapshots without asking for confirmation. ## MDX Content @@ -195,6 +201,12 @@ let default: unit => React.element - **Lefthook** runs `yarn format` on pre-commit (auto-stages fixed files). - Generated `.mjs`/`.jsx` output files from ReScript are git-tracked but excluded from Prettier. +## Pull Requests and Commits + +- Use conventional commits format for commit messages (e.g. `feat: add new API docs`, `fix: resolve loader data issue`). +- Commit bodies should explain what changed with some concise details +- PR descriptions should provide context for the change, a summary of the changes with descriptions, and reference any related issues. + ## Important Warnings - Do **not** modify generated `.jsx` / `.mjs` files directly — they are ReScript compiler output. diff --git a/README.md b/README.md index 692d71a94..5cc3edbbe 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,27 @@ yarn dev ## Run Tests +### Unit Tests (Vitest) + +We use [Vitest](https://vitest.dev/) with browser mode (Playwright) for component-level unit tests. Test files live in `__tests__/` and are written in ReScript. + +```sh +# Run tests in watch mode (headed browser) +yarn vitest + +# Run tests once in headless mode (same as CI) +yarn ci:test +``` + +**Updating screenshots:** Screenshot baselines should only be updated in headless mode so they match CI and stay consistent across devices. Use the dedicated command: + +```sh +yarn vitest:update +``` + +This runs the full suite headlessly with the `--update` flag, regenerating any screenshot baselines that have changed. Commit the updated `.png` files alongside your code changes. +Please be selective in pushing up changes to screenshots and only update files that you have added or expected to change. Pushing up all changes can make it hard to review PRs with small image differences based on different devices or environments that wouldn't trigger failures in CI. + ### Markdown Codeblock Tests We check the validity of our code examples marked with: diff --git a/__tests__/MarkdownComponents_.test.res b/__tests__/MarkdownComponents_.test.res index 2d7a21efd..62ffc75c1 100644 --- a/__tests__/MarkdownComponents_.test.res +++ b/__tests__/MarkdownComponents_.test.res @@ -203,12 +203,14 @@ test("renders Image with caption", async () => { let screen = await render(
, ) - let caption = await screen->getByText("The ReScript logo") + let caption = await screen->getByText("A sample image caption") await element(caption)->toBeVisible let wrapper = await screen->getByTestId("image-wrapper") @@ -226,9 +228,6 @@ test("renders Video with caption", async () => { let caption = await screen->getByText("A sample video") await element(caption)->toBeVisible - - let wrapper = await screen->getByTestId("video-wrapper") - await element(wrapper)->toMatchScreenshot("markdown-video") }) test("renders horizontal rule", async () => { diff --git a/__tests__/NavbarSecondary_.test.res b/__tests__/NavbarSecondary_.test.res index 4746c83ca..796b30a99 100644 --- a/__tests__/NavbarSecondary_.test.res +++ b/__tests__/NavbarSecondary_.test.res @@ -5,9 +5,9 @@ test("desktop secondary navbar shows all doc section links", async () => { await viewport(1440, 500) let screen = await render( - + - , +
, ) let navbar = await screen->getByTestId("navbar-secondary") @@ -24,9 +24,9 @@ test("mobile secondary navbar shows all links", async () => { await viewport(600, 500) let screen = await render( - + - , + , ) let navbar = await screen->getByTestId("navbar-secondary") diff --git a/__tests__/__screenshots__/ApiLayout_.test.jsx/desktop-API-layout-shows-sidebar-categories-and-version-select-1.png b/__tests__/__screenshots__/ApiLayout_.test.jsx/desktop-API-layout-shows-sidebar-categories-and-version-select-1.png deleted file mode 100644 index b751321f8..000000000 Binary files a/__tests__/__screenshots__/ApiLayout_.test.jsx/desktop-API-layout-shows-sidebar-categories-and-version-select-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/ApiLayout_.test.jsx/mobile-API-layout-hides-sidebar-1.png b/__tests__/__screenshots__/ApiLayout_.test.jsx/mobile-API-layout-hides-sidebar-1.png deleted file mode 100644 index 80c3ddc0c..000000000 Binary files a/__tests__/__screenshots__/ApiLayout_.test.jsx/mobile-API-layout-hides-sidebar-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/ApiLayout_.test.jsx/old-docs-warning-shows-version-info-1.png b/__tests__/__screenshots__/ApiLayout_.test.jsx/old-docs-warning-shows-version-info-1.png deleted file mode 100644 index f993aad86..000000000 Binary files a/__tests__/__screenshots__/ApiLayout_.test.jsx/old-docs-warning-shows-version-info-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/ApiOverviewLayout_.test.jsx/desktop-API-overview-shows-all-category-items-1.png b/__tests__/__screenshots__/ApiOverviewLayout_.test.jsx/desktop-API-overview-shows-all-category-items-1.png deleted file mode 100644 index e83429f07..000000000 Binary files a/__tests__/__screenshots__/ApiOverviewLayout_.test.jsx/desktop-API-overview-shows-all-category-items-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/ApiOverviewLayout_.test.jsx/desktop-API-overview-shows-sidebar-categories-and-content-1.png b/__tests__/__screenshots__/ApiOverviewLayout_.test.jsx/desktop-API-overview-shows-sidebar-categories-and-content-1.png deleted file mode 100644 index bd9d90404..000000000 Binary files a/__tests__/__screenshots__/ApiOverviewLayout_.test.jsx/desktop-API-overview-shows-sidebar-categories-and-content-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/ApiOverviewLayout_.test.jsx/mobile-API-overview-hides-sidebar-1.png b/__tests__/__screenshots__/ApiOverviewLayout_.test.jsx/mobile-API-overview-hides-sidebar-1.png deleted file mode 100644 index 3f1b3f67b..000000000 Binary files a/__tests__/__screenshots__/ApiOverviewLayout_.test.jsx/mobile-API-overview-hides-sidebar-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/ApiOverviewLayout_.test.jsx/tablet-API-overview-1.png b/__tests__/__screenshots__/ApiOverviewLayout_.test.jsx/tablet-API-overview-1.png deleted file mode 100644 index 72019c22f..000000000 Binary files a/__tests__/__screenshots__/ApiOverviewLayout_.test.jsx/tablet-API-overview-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/Banner_.test.jsx/mobile-banner-1.png b/__tests__/__screenshots__/Banner_.test.jsx/mobile-banner-1.png deleted file mode 100644 index 70886689f..000000000 Binary files a/__tests__/__screenshots__/Banner_.test.jsx/mobile-banner-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/Banner_.test.jsx/renders-banner-with-content-1.png b/__tests__/__screenshots__/Banner_.test.jsx/renders-banner-with-content-1.png deleted file mode 100644 index ac79539d8..000000000 Binary files a/__tests__/__screenshots__/Banner_.test.jsx/renders-banner-with-content-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-archived-blog-article-shows-warning-banner-1.png b/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-archived-blog-article-shows-warning-banner-1.png deleted file mode 100644 index 22cb38495..000000000 Binary files a/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-archived-blog-article-shows-warning-banner-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-blog-article-renders-header--author--date--and-body-1.png b/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-blog-article-renders-header--author--date--and-body-1.png deleted file mode 100644 index 4eda750d1..000000000 Binary files a/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-blog-article-renders-header--author--date--and-body-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-blog-article-with-article-image-shows-image-1.png b/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-blog-article-with-article-image-shows-image-1.png deleted file mode 100644 index 7af51cf47..000000000 Binary files a/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-blog-article-with-article-image-shows-image-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-blog-article-with-co-authors-shows-all-authors-1.png b/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-blog-article-with-co-authors-shows-all-authors-1.png deleted file mode 100644 index 185153fcb..000000000 Binary files a/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-blog-article-with-co-authors-shows-all-authors-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-blog-article-without-description-1.png b/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-blog-article-without-description-1.png deleted file mode 100644 index 4c2cc165d..000000000 Binary files a/__tests__/__screenshots__/BlogArticle_.test.jsx/desktop-blog-article-without-description-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/BlogArticle_.test.jsx/mobile-blog-article-1.png b/__tests__/__screenshots__/BlogArticle_.test.jsx/mobile-blog-article-1.png deleted file mode 100644 index 5912562dd..000000000 Binary files a/__tests__/__screenshots__/BlogArticle_.test.jsx/mobile-blog-article-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/Blog_.test.jsx/desktop-blog-shows-archived-posts-1.png b/__tests__/__screenshots__/Blog_.test.jsx/desktop-blog-shows-archived-posts-1.png deleted file mode 100644 index 1e1455afe..000000000 Binary files a/__tests__/__screenshots__/Blog_.test.jsx/desktop-blog-shows-archived-posts-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/Blog_.test.jsx/desktop-blog-with-single-post-1.png b/__tests__/__screenshots__/Blog_.test.jsx/desktop-blog-with-single-post-1.png deleted file mode 100644 index c0fe2ebc1..000000000 Binary files a/__tests__/__screenshots__/Blog_.test.jsx/desktop-blog-with-single-post-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/Button_.test.jsx/renders-PrimaryBlue-button-1.png b/__tests__/__screenshots__/Button_.test.jsx/renders-PrimaryBlue-button-1.png deleted file mode 100644 index f96acbe35..000000000 Binary files a/__tests__/__screenshots__/Button_.test.jsx/renders-PrimaryBlue-button-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/Button_.test.jsx/renders-PrimaryRed-button-1.png b/__tests__/__screenshots__/Button_.test.jsx/renders-PrimaryRed-button-1.png deleted file mode 100644 index 23ce58ce2..000000000 Binary files a/__tests__/__screenshots__/Button_.test.jsx/renders-PrimaryRed-button-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/Button_.test.jsx/renders-SecondaryRed-button-1.png b/__tests__/__screenshots__/Button_.test.jsx/renders-SecondaryRed-button-1.png deleted file mode 100644 index f022fb79b..000000000 Binary files a/__tests__/__screenshots__/Button_.test.jsx/renders-SecondaryRed-button-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/Button_.test.jsx/renders-Small-button-1.png b/__tests__/__screenshots__/Button_.test.jsx/renders-Small-button-1.png deleted file mode 100644 index 370ea8fff..000000000 Binary files a/__tests__/__screenshots__/Button_.test.jsx/renders-Small-button-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/CodeExample_.test.jsx/renders-code-block-with-highlighted-lines-1.png b/__tests__/__screenshots__/CodeExample_.test.jsx/renders-code-block-with-highlighted-lines-1.png deleted file mode 100644 index d9ac8de5d..000000000 Binary files a/__tests__/__screenshots__/CodeExample_.test.jsx/renders-code-block-with-highlighted-lines-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/CodeExample_.test.jsx/renders-code-block-with-language-label-1.png b/__tests__/__screenshots__/CodeExample_.test.jsx/renders-code-block-with-language-label-1.png deleted file mode 100644 index 7a34b7c9c..000000000 Binary files a/__tests__/__screenshots__/CodeExample_.test.jsx/renders-code-block-with-language-label-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/CodeExample_.test.jsx/renders-code-block-without-label-when-showLabel-is-false-1.png b/__tests__/__screenshots__/CodeExample_.test.jsx/renders-code-block-without-label-when-showLabel-is-false-1.png deleted file mode 100644 index 52833d29e..000000000 Binary files a/__tests__/__screenshots__/CodeExample_.test.jsx/renders-code-block-without-label-when-showLabel-is-false-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/CodeExample_.test.jsx/renders-toggle-with-multiple-tabs-1.png b/__tests__/__screenshots__/CodeExample_.test.jsx/renders-toggle-with-multiple-tabs-1.png deleted file mode 100644 index c53aad15f..000000000 Binary files a/__tests__/__screenshots__/CodeExample_.test.jsx/renders-toggle-with-multiple-tabs-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/CodeExample_.test.jsx/toggle-switches-between-tabs-on-click-1.png b/__tests__/__screenshots__/CodeExample_.test.jsx/toggle-switches-between-tabs-on-click-1.png deleted file mode 100644 index 3d0be472f..000000000 Binary files a/__tests__/__screenshots__/CodeExample_.test.jsx/toggle-switches-between-tabs-on-click-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/CommunityLayout_.test.jsx/desktop-community-layout-shows-sidebar-and-content-1.png b/__tests__/__screenshots__/CommunityLayout_.test.jsx/desktop-community-layout-shows-sidebar-and-content-1.png deleted file mode 100644 index c400e4c93..000000000 Binary files a/__tests__/__screenshots__/CommunityLayout_.test.jsx/desktop-community-layout-shows-sidebar-and-content-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/CommunityLayout_.test.jsx/desktop-community-layout-with-multiple-categories-1.png b/__tests__/__screenshots__/CommunityLayout_.test.jsx/desktop-community-layout-with-multiple-categories-1.png deleted file mode 100644 index 404ed6794..000000000 Binary files a/__tests__/__screenshots__/CommunityLayout_.test.jsx/desktop-community-layout-with-multiple-categories-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/CommunityLayout_.test.jsx/mobile-community-layout-hides-sidebar-1.png b/__tests__/__screenshots__/CommunityLayout_.test.jsx/mobile-community-layout-hides-sidebar-1.png deleted file mode 100644 index 462bb857f..000000000 Binary files a/__tests__/__screenshots__/CommunityLayout_.test.jsx/mobile-community-layout-hides-sidebar-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/CommunityLayout_.test.jsx/tablet-community-layout-1.png b/__tests__/__screenshots__/CommunityLayout_.test.jsx/tablet-community-layout-1.png deleted file mode 100644 index aebf5cda8..000000000 Binary files a/__tests__/__screenshots__/CommunityLayout_.test.jsx/tablet-community-layout-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/DocsLayout_.test.jsx/desktop-docs-layout-highlights-active-nav-item-1.png b/__tests__/__screenshots__/DocsLayout_.test.jsx/desktop-docs-layout-highlights-active-nav-item-1.png deleted file mode 100644 index e5b21d679..000000000 Binary files a/__tests__/__screenshots__/DocsLayout_.test.jsx/desktop-docs-layout-highlights-active-nav-item-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/DocsLayout_.test.jsx/desktop-docs-layout-shows-pagination--prev-next--1.png b/__tests__/__screenshots__/DocsLayout_.test.jsx/desktop-docs-layout-shows-pagination--prev-next--1.png deleted file mode 100644 index 8515a1585..000000000 Binary files a/__tests__/__screenshots__/DocsLayout_.test.jsx/desktop-docs-layout-shows-pagination--prev-next--1.png and /dev/null differ diff --git a/__tests__/__screenshots__/DocsLayout_.test.jsx/desktop-docs-layout-shows-sidebar-with-categories-1.png b/__tests__/__screenshots__/DocsLayout_.test.jsx/desktop-docs-layout-shows-sidebar-with-categories-1.png deleted file mode 100644 index 1fe3ddeb5..000000000 Binary files a/__tests__/__screenshots__/DocsLayout_.test.jsx/desktop-docs-layout-shows-sidebar-with-categories-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/DocsLayout_.test.jsx/desktop-docs-layout-shows-table-of-contents-entries-1.png b/__tests__/__screenshots__/DocsLayout_.test.jsx/desktop-docs-layout-shows-table-of-contents-entries-1.png deleted file mode 100644 index 1fe3ddeb5..000000000 Binary files a/__tests__/__screenshots__/DocsLayout_.test.jsx/desktop-docs-layout-shows-table-of-contents-entries-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/DocsLayout_.test.jsx/mobile-docs-layout-hides-sidebar-by-default-1.png b/__tests__/__screenshots__/DocsLayout_.test.jsx/mobile-docs-layout-hides-sidebar-by-default-1.png deleted file mode 100644 index fa7845427..000000000 Binary files a/__tests__/__screenshots__/DocsLayout_.test.jsx/mobile-docs-layout-hides-sidebar-by-default-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/DocsOverview_.test.jsx/desktop-docs-overview-shows-all-section-cards-1.png b/__tests__/__screenshots__/DocsOverview_.test.jsx/desktop-docs-overview-shows-all-section-cards-1.png deleted file mode 100644 index a6394633f..000000000 Binary files a/__tests__/__screenshots__/DocsOverview_.test.jsx/desktop-docs-overview-shows-all-section-cards-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/DocsOverview_.test.jsx/desktop-docs-overview-shows-ecosystem-links-1.png b/__tests__/__screenshots__/DocsOverview_.test.jsx/desktop-docs-overview-shows-ecosystem-links-1.png deleted file mode 100644 index a6394633f..000000000 Binary files a/__tests__/__screenshots__/DocsOverview_.test.jsx/desktop-docs-overview-shows-ecosystem-links-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/DocsOverview_.test.jsx/mobile-docs-overview-1.png b/__tests__/__screenshots__/DocsOverview_.test.jsx/mobile-docs-overview-1.png deleted file mode 100644 index bf6897b24..000000000 Binary files a/__tests__/__screenshots__/DocsOverview_.test.jsx/mobile-docs-overview-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/Footer_.test.jsx/desktop-footer-shows-all-sections-and-links-1.png b/__tests__/__screenshots__/Footer_.test.jsx/desktop-footer-shows-all-sections-and-links-1.png deleted file mode 100644 index 110fcffb2..000000000 Binary files a/__tests__/__screenshots__/Footer_.test.jsx/desktop-footer-shows-all-sections-and-links-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/Footer_.test.jsx/mobile-footer-stacks-sections-vertically-1.png b/__tests__/__screenshots__/Footer_.test.jsx/mobile-footer-stacks-sections-vertically-1.png deleted file mode 100644 index 57e8570d1..000000000 Binary files a/__tests__/__screenshots__/Footer_.test.jsx/mobile-footer-stacks-sections-vertically-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/MainLayout_.test.jsx/desktop-main-layout-renders-children-and-footer-1.png b/__tests__/__screenshots__/MainLayout_.test.jsx/desktop-main-layout-renders-children-and-footer-1.png deleted file mode 100644 index 64ce5a766..000000000 Binary files a/__tests__/__screenshots__/MainLayout_.test.jsx/desktop-main-layout-renders-children-and-footer-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/MainLayout_.test.jsx/mobile-main-layout-1.png b/__tests__/__screenshots__/MainLayout_.test.jsx/mobile-main-layout-1.png deleted file mode 100644 index 6d5674b79..000000000 Binary files a/__tests__/__screenshots__/MainLayout_.test.jsx/mobile-main-layout-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/markdown-image-chromium-linux.png b/__tests__/__screenshots__/MarkdownComponents_.test.jsx/markdown-image-chromium-linux.png index cea55eff3..6d361a284 100644 Binary files a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/markdown-image-chromium-linux.png and b/__tests__/__screenshots__/MarkdownComponents_.test.jsx/markdown-image-chromium-linux.png differ diff --git a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/markdown-video-chromium-linux.png b/__tests__/__screenshots__/MarkdownComponents_.test.jsx/markdown-video-chromium-linux.png deleted file mode 100644 index 067090d24..000000000 Binary files a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/markdown-video-chromium-linux.png and /dev/null differ diff --git a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-Anchor-with-link-icon-1.png b/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-Anchor-with-link-icon-1.png deleted file mode 100644 index fc4b67bdf..000000000 Binary files a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-Anchor-with-link-icon-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-Cite-without-author-1.png b/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-Cite-without-author-1.png deleted file mode 100644 index 924936093..000000000 Binary files a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-Cite-without-author-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-Image-with-small-size-1.png b/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-Image-with-small-size-1.png deleted file mode 100644 index 7b4b2a8fc..000000000 Binary files a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-Image-with-small-size-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-Strong-text-1.png b/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-Strong-text-1.png deleted file mode 100644 index 964b64ac3..000000000 Binary files a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-Strong-text-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-inline-code-1.png b/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-inline-code-1.png deleted file mode 100644 index 574d95c4e..000000000 Binary files a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-inline-code-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-lists-1.png b/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-lists-1.png deleted file mode 100644 index a59fef67a..000000000 Binary files a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-lists-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-nested-list--ul-inside-li--1.png b/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-nested-list--ul-inside-li--1.png deleted file mode 100644 index 4f4afe78f..000000000 Binary files a/__tests__/__screenshots__/MarkdownComponents_.test.jsx/renders-nested-list--ul-inside-li--1.png and /dev/null differ diff --git a/__tests__/__screenshots__/NavbarSecondary_.test.jsx/desktop-navbar-secondary-chromium-linux.png b/__tests__/__screenshots__/NavbarSecondary_.test.jsx/desktop-navbar-secondary-chromium-linux.png index bc51442ee..ceeb81806 100644 Binary files a/__tests__/__screenshots__/NavbarSecondary_.test.jsx/desktop-navbar-secondary-chromium-linux.png and b/__tests__/__screenshots__/NavbarSecondary_.test.jsx/desktop-navbar-secondary-chromium-linux.png differ diff --git a/__tests__/__screenshots__/NavbarSecondary_.test.jsx/desktop-secondary-navbar-shows-all-doc-section-links-1.png b/__tests__/__screenshots__/NavbarSecondary_.test.jsx/desktop-secondary-navbar-shows-all-doc-section-links-1.png deleted file mode 100644 index 68e650f4f..000000000 Binary files a/__tests__/__screenshots__/NavbarSecondary_.test.jsx/desktop-secondary-navbar-shows-all-doc-section-links-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/NavbarSecondary_.test.jsx/mobile-navbar-secondary-chromium-linux.png b/__tests__/__screenshots__/NavbarSecondary_.test.jsx/mobile-navbar-secondary-chromium-linux.png index 5ba05892c..870cc995f 100644 Binary files a/__tests__/__screenshots__/NavbarSecondary_.test.jsx/mobile-navbar-secondary-chromium-linux.png and b/__tests__/__screenshots__/NavbarSecondary_.test.jsx/mobile-navbar-secondary-chromium-linux.png differ diff --git a/__tests__/__screenshots__/NavbarSecondary_.test.jsx/mobile-secondary-navbar-shows-all-links-1.png b/__tests__/__screenshots__/NavbarSecondary_.test.jsx/mobile-secondary-navbar-shows-all-links-1.png deleted file mode 100644 index b68ce07da..000000000 Binary files a/__tests__/__screenshots__/NavbarSecondary_.test.jsx/mobile-secondary-navbar-shows-all-links-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/NavbarSecondary_.test.jsx/secondary-navbar-highlights-active-section-1.png b/__tests__/__screenshots__/NavbarSecondary_.test.jsx/secondary-navbar-highlights-active-section-1.png deleted file mode 100644 index 89bc240df..000000000 Binary files a/__tests__/__screenshots__/NavbarSecondary_.test.jsx/secondary-navbar-highlights-active-section-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SearchBox_.test.jsx/renders-with-a-value-1.png b/__tests__/__screenshots__/SearchBox_.test.jsx/renders-with-a-value-1.png deleted file mode 100644 index 210666bc7..000000000 Binary files a/__tests__/__screenshots__/SearchBox_.test.jsx/renders-with-a-value-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SearchBox_.test.jsx/renders-with-placeholder-text-1.png b/__tests__/__screenshots__/SearchBox_.test.jsx/renders-with-placeholder-text-1.png deleted file mode 100644 index 345e0e81e..000000000 Binary files a/__tests__/__screenshots__/SearchBox_.test.jsx/renders-with-placeholder-text-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SidebarLayout_.test.jsx/breadcrumbs-render-path-segments-1.png b/__tests__/__screenshots__/SidebarLayout_.test.jsx/breadcrumbs-render-path-segments-1.png deleted file mode 100644 index 6bbd23878..000000000 Binary files a/__tests__/__screenshots__/SidebarLayout_.test.jsx/breadcrumbs-render-path-segments-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SidebarLayout_.test.jsx/breadcrumbs-with-deep-path-1.png b/__tests__/__screenshots__/SidebarLayout_.test.jsx/breadcrumbs-with-deep-path-1.png deleted file mode 100644 index a3e7c46aa..000000000 Binary files a/__tests__/__screenshots__/SidebarLayout_.test.jsx/breadcrumbs-with-deep-path-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SidebarLayout_.test.jsx/sidebar-category-highlights-active-item-1.png b/__tests__/__screenshots__/SidebarLayout_.test.jsx/sidebar-category-highlights-active-item-1.png deleted file mode 100644 index b5d9f7416..000000000 Binary files a/__tests__/__screenshots__/SidebarLayout_.test.jsx/sidebar-category-highlights-active-item-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SidebarLayout_.test.jsx/sidebar-category-renders-title-and-nav-items-1.png b/__tests__/__screenshots__/SidebarLayout_.test.jsx/sidebar-category-renders-title-and-nav-items-1.png deleted file mode 100644 index 76d010528..000000000 Binary files a/__tests__/__screenshots__/SidebarLayout_.test.jsx/sidebar-category-renders-title-and-nav-items-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SidebarLayout_.test.jsx/sidebar-category-with-active-TOC-renders-entries-1.png b/__tests__/__screenshots__/SidebarLayout_.test.jsx/sidebar-category-with-active-TOC-renders-entries-1.png deleted file mode 100644 index 2ac0bb615..000000000 Binary files a/__tests__/__screenshots__/SidebarLayout_.test.jsx/sidebar-category-with-active-TOC-renders-entries-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SidebarLayout_.test.jsx/sidebar-category-with-many-items-1.png b/__tests__/__screenshots__/SidebarLayout_.test.jsx/sidebar-category-with-many-items-1.png deleted file mode 100644 index 7999cec54..000000000 Binary files a/__tests__/__screenshots__/SidebarLayout_.test.jsx/sidebar-category-with-many-items-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SyntaxLookup_.test.jsx/deprecated-items-show-with-line-through-styling-1.png b/__tests__/__screenshots__/SyntaxLookup_.test.jsx/deprecated-items-show-with-line-through-styling-1.png deleted file mode 100644 index 5cf8af1c0..000000000 Binary files a/__tests__/__screenshots__/SyntaxLookup_.test.jsx/deprecated-items-show-with-line-through-styling-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SyntaxLookup_.test.jsx/desktop-syntax-lookup-renders-categories-and-tags-1.png b/__tests__/__screenshots__/SyntaxLookup_.test.jsx/desktop-syntax-lookup-renders-categories-and-tags-1.png deleted file mode 100644 index 5cf8af1c0..000000000 Binary files a/__tests__/__screenshots__/SyntaxLookup_.test.jsx/desktop-syntax-lookup-renders-categories-and-tags-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SyntaxLookup_.test.jsx/desktop-syntax-lookup-with-active-item-shows-detail-box-1.png b/__tests__/__screenshots__/SyntaxLookup_.test.jsx/desktop-syntax-lookup-with-active-item-shows-detail-box-1.png deleted file mode 100644 index 049ff22ce..000000000 Binary files a/__tests__/__screenshots__/SyntaxLookup_.test.jsx/desktop-syntax-lookup-with-active-item-shows-detail-box-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SyntaxLookup_.test.jsx/mobile-syntax-lookup-1.png b/__tests__/__screenshots__/SyntaxLookup_.test.jsx/mobile-syntax-lookup-1.png deleted file mode 100644 index d5c7ce17a..000000000 Binary files a/__tests__/__screenshots__/SyntaxLookup_.test.jsx/mobile-syntax-lookup-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SyntaxLookup_.test.jsx/mobile-syntax-lookup-with-active-item-1.png b/__tests__/__screenshots__/SyntaxLookup_.test.jsx/mobile-syntax-lookup-with-active-item-1.png deleted file mode 100644 index 8a6257e68..000000000 Binary files a/__tests__/__screenshots__/SyntaxLookup_.test.jsx/mobile-syntax-lookup-with-active-item-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/SyntaxLookup_.test.jsx/syntax-lookup-detail-box-shows-summary-1.png b/__tests__/__screenshots__/SyntaxLookup_.test.jsx/syntax-lookup-detail-box-shows-summary-1.png deleted file mode 100644 index 0642aa1fd..000000000 Binary files a/__tests__/__screenshots__/SyntaxLookup_.test.jsx/syntax-lookup-detail-box-shows-summary-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/Tag_.test.jsx/renders-multiple-tags-side-by-side-1.png b/__tests__/__screenshots__/Tag_.test.jsx/renders-multiple-tags-side-by-side-1.png deleted file mode 100644 index 17f02a83b..000000000 Binary files a/__tests__/__screenshots__/Tag_.test.jsx/renders-multiple-tags-side-by-side-1.png and /dev/null differ diff --git a/__tests__/__screenshots__/Tag_.test.jsx/renders-subtle-tag-with-text-1.png b/__tests__/__screenshots__/Tag_.test.jsx/renders-subtle-tag-with-text-1.png deleted file mode 100644 index a052ecada..000000000 Binary files a/__tests__/__screenshots__/Tag_.test.jsx/renders-subtle-tag-with-text-1.png and /dev/null differ diff --git a/app/routes.res b/app/routes.res index 656db708c..55fcacf08 100644 --- a/app/routes.res +++ b/app/routes.res @@ -28,7 +28,57 @@ let stdlibRoutes = let beltRoutes = beltPaths->Array.map(path => route(path, "./routes/ApiRoute.jsx", ~options={id: path})) -let mdxRoutes = mdxRoutes("./routes/MdxRoute.jsx") +let blogArticleRoutes = + MdxFile.scanPaths(~dir="markdown-pages/blog", ~alias="blog")->Array.map(path => + route(path, "./routes/BlogArticleRoute.jsx", ~options={id: path}) + ) + +let docsManualRoutes = + MdxFile.scanPaths(~dir="markdown-pages/docs/manual", ~alias="docs/manual") + ->Array.filter(path => !String.includes(path, "docs/manual/api")) + ->Array.map(path => route(path, "./routes/DocsManualRoute.jsx", ~options={id: path})) + +let docsReactRoutes = + MdxFile.scanPaths(~dir="markdown-pages/docs/react", ~alias="docs/react")->Array.map(path => + route(path, "./routes/DocsReactRoute.jsx", ~options={id: path}) + ) + +let docsGuidelinesRoutes = + MdxFile.scanPaths( + ~dir="markdown-pages/docs/guidelines", + ~alias="docs/guidelines", + )->Array.map(path => route(path, "./routes/DocsGuidelinesRoute.jsx", ~options={id: path})) + +let communityRoutes = + MdxFile.scanPaths(~dir="markdown-pages/community", ~alias="community")->Array.map(path => + route(path, "./routes/CommunityRoute.jsx", ~options={id: path}) + ) + +let syntaxLookupDetailRoutes = + MdxFile.scanPaths(~dir="markdown-pages/syntax-lookup", ~alias="syntax-lookup")->Array.map(path => + route(path, "./routes/SyntaxLookupDetailRoute.jsx", ~options={id: path}) + ) + +let mdxRoutes = mdxRoutes("./routes/MdxRoute.jsx")->Array.filter(r => + !( + r.path + ->Option.map(path => + path === "blog" || + String.startsWith(path, "blog/") || + path === "docs/manual" || + String.startsWith(path, "docs/manual/") || + path === "docs/react" || + String.startsWith(path, "docs/react/") || + path === "docs/guidelines" || + String.startsWith(path, "docs/guidelines/") || + path === "community" || + String.startsWith(path, "community/") || + path === "syntax-lookup" || + String.startsWith(path, "syntax-lookup/") + ) + ->Option.getOr(false) + ) +) let default = [ index("./routes/LandingPageRoute.jsx"), @@ -44,6 +94,12 @@ let default = [ route("docs/manual/api/dom", "./routes/ApiRoute.jsx", ~options={id: "api-dom"}), ...stdlibRoutes, ...beltRoutes, + ...blogArticleRoutes, + ...docsManualRoutes, + ...docsReactRoutes, + ...docsGuidelinesRoutes, + ...communityRoutes, + ...syntaxLookupDetailRoutes, ...mdxRoutes, route("*", "./routes/NotFoundRoute.jsx"), ] diff --git a/app/routes/BlogArticleRoute.res b/app/routes/BlogArticleRoute.res new file mode 100644 index 000000000..6644a1509 --- /dev/null +++ b/app/routes/BlogArticleRoute.res @@ -0,0 +1,54 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + blogPost: BlogApi.post, + title: string, +} + +let loader: ReactRouter.Loader.t = async ({request}) => { + let {pathname} = WebAPI.URL.make(~url=request.url) + let filePath = MdxFile.resolveFilePath( + (pathname :> string), + ~dir="markdown-pages/blog", + ~alias="blog", + ) + + let raw = await Node.Fs.readFile(filePath, "utf-8") + let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw) + + let frontmatter = switch BlogFrontmatter.decode(frontmatter) { + | Ok(fm) => fm + | Error(msg) => JsError.throwWithMessage(msg) + } + + let compiledMdx = await MdxFile.compileMdx(raw, ~filePath, ~remarkPlugins=Mdx.plugins) + + let archived = filePath->String.includes("/archived/") + + let slug = + filePath + ->Node.Path.basename + ->String.replace(".mdx", "") + ->String.replaceRegExp(/^\d\d\d\d-\d\d-\d\d-/, "") + + let path = archived ? "archived/" ++ slug : slug + + let blogPost: BlogApi.post = { + path, + archived, + frontmatter, + } + + { + compiledMdx, + blogPost, + title: `${frontmatter.title} | ReScript Blog`, + } +} + +let default = () => { + let {compiledMdx, blogPost: {frontmatter, archived, path}} = ReactRouter.useLoaderData() + + + + +} diff --git a/app/routes/BlogArticleRoute.resi b/app/routes/BlogArticleRoute.resi new file mode 100644 index 000000000..40ba9580c --- /dev/null +++ b/app/routes/BlogArticleRoute.resi @@ -0,0 +1,9 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + blogPost: BlogApi.post, + title: string, +} + +let loader: ReactRouter.Loader.t + +let default: unit => React.element diff --git a/app/routes/CommunityRoute.res b/app/routes/CommunityRoute.res new file mode 100644 index 000000000..627efdf0c --- /dev/null +++ b/app/routes/CommunityRoute.res @@ -0,0 +1,97 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + entries: array, + title: string, + description: string, + filePath: string, + categories: array, +} + +let communityTableOfContents = async () => { + let groups = + (await MdxFile.loadAllAttributes(~dir="markdown-pages/community")) + ->Mdx.filterMdxPages("community") + ->Mdx.groupBySection + ->Dict.mapValues(values => + values->Mdx.sortSection->SidebarHelpers.convertToNavItems("/community") + ) + + SidebarHelpers.getAllGroups(groups, ["Resources"]) +} + +let loader: ReactRouter.Loader.t = async ({request}) => { + let {pathname} = WebAPI.URL.make(~url=request.url) + let filePath = MdxFile.resolveFilePath( + (pathname :> string), + ~dir="markdown-pages/community", + ~alias="community", + ) + + let raw = await Node.Fs.readFile(filePath, "utf-8") + let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw) + + let description = switch frontmatter { + | Object(dict) => + switch dict->Dict.get("description") { + | Some(String(s)) => s + | _ => "" + } + | _ => "" + } + + let title = switch frontmatter { + | Object(dict) => + switch dict->Dict.get("title") { + | Some(String(s)) => s + | _ => "" + } + | _ => "" + } + + let compiledMdx = await MdxFile.compileMdx(raw, ~filePath, ~remarkPlugins=Mdx.plugins) + + let markdownTree = Mdast.fromMarkdown(raw) + let tocResult = Mdast.toc(markdownTree, {maxDepth: 2}) + + let headers = Dict.make() + Mdast.reduceHeaders(tocResult.map, headers) + + let entries = + headers + ->Dict.toArray + ->Array.map(((header, url)): TableOfContents.entry => { + header, + href: (url :> string), + }) + ->Array.slice(~start=2) + + let categories = await communityTableOfContents() + + { + compiledMdx, + entries, + title: `${title} | ReScript Community`, + description, + filePath, + categories, + } +} + +let default = () => { + let {compiledMdx, entries, filePath, categories} = ReactRouter.useLoaderData() + + let editHref = `https://github.com/rescript-lang/rescript-lang.org/blob/master/${filePath}` + + <> + +
+ +
+ + {React.string("Edit")} + +
+ +} diff --git a/app/routes/DocsGuidelinesRoute.res b/app/routes/DocsGuidelinesRoute.res new file mode 100644 index 000000000..8cec83187 --- /dev/null +++ b/app/routes/DocsGuidelinesRoute.res @@ -0,0 +1,88 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + entries: array, + title: string, + description: string, + filePath: string, +} + +let loader: ReactRouter.Loader.t = async ({request}) => { + let {pathname} = WebAPI.URL.make(~url=request.url) + let filePath = MdxFile.resolveFilePath( + (pathname :> string), + ~dir="markdown-pages/docs/guidelines", + ~alias="docs/guidelines", + ) + + let raw = await Node.Fs.readFile(filePath, "utf-8") + let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw) + + let description = switch frontmatter { + | Object(dict) => + switch dict->Dict.get("description") { + | Some(String(s)) => s + | _ => "" + } + | _ => "" + } + + let title = switch frontmatter { + | Object(dict) => + switch dict->Dict.get("title") { + | Some(String(s)) => s + | _ => "" + } + | _ => "" + } + + let compiledMdx = await MdxFile.compileMdx(raw, ~filePath, ~remarkPlugins=Mdx.plugins) + + // Build table of contents entries from markdown headings + let markdownTree = Mdast.fromMarkdown(raw) + let tocResult = Mdast.toc(markdownTree, {maxDepth: 2}) + + let headers = Dict.make() + Mdast.reduceHeaders(tocResult.map, headers) + + let entries = + headers + ->Dict.toArray + ->Array.map(((header, url)): TableOfContents.entry => { + header, + href: (url :> string), + }) + ->Array.slice(~start=2) // skip document entry and H1 title, keep h2 sections + + { + compiledMdx, + entries, + title: `${title} | ReScript Guidelines`, + description, + filePath, + } +} + +let default = () => { + let {compiledMdx, entries, title, description, filePath} = ReactRouter.useLoaderData() + + let editHref = `https://github.com/rescript-lang/rescript-lang.org/blob/master/${filePath}` + + let categories: array = [] + + <> + + + + + {React.string("Edit")} + + + +
+ +
+
+ +} diff --git a/app/routes/DocsGuidelinesRoute.resi b/app/routes/DocsGuidelinesRoute.resi new file mode 100644 index 000000000..307767dcb --- /dev/null +++ b/app/routes/DocsGuidelinesRoute.resi @@ -0,0 +1,11 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + entries: array, + title: string, + description: string, + filePath: string, +} + +let loader: ReactRouter.Loader.t + +let default: unit => React.element diff --git a/app/routes/DocsManualRoute.res b/app/routes/DocsManualRoute.res new file mode 100644 index 000000000..dd44b2596 --- /dev/null +++ b/app/routes/DocsManualRoute.res @@ -0,0 +1,159 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + categories: array, + entries: array, + title: string, + description: string, + filePath: string, +} + +// Build sidebar categories from all manual docs, sorted by their "order" field in frontmatter +let manualTableOfContents = async () => { + let groups = + (await MdxFile.loadAllAttributes(~dir="markdown-pages/docs")) + ->Mdx.filterMdxPages("docs/manual") + ->Mdx.groupBySection + ->Dict.mapValues(values => + values->Mdx.sortSection->SidebarHelpers.convertToNavItems("/docs/manual") + ) + + SidebarHelpers.getAllGroups( + groups, + [ + "Overview", + "Guides", + "Language Features", + "JavaScript Interop", + "Build System", + "Advanced Features", + ], + ) +} + +let loader: ReactRouter.Loader.t = async ({request}) => { + let {pathname} = WebAPI.URL.make(~url=request.url) + let filePath = MdxFile.resolveFilePath( + (pathname :> string), + ~dir="markdown-pages/docs/manual", + ~alias="docs/manual", + ) + + let raw = await Node.Fs.readFile(filePath, "utf-8") + let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw) + + let description = switch frontmatter { + | Object(dict) => + switch dict->Dict.get("description") { + | Some(String(s)) => s + | _ => "" + } + | _ => "" + } + + let title = switch frontmatter { + | Object(dict) => + switch dict->Dict.get("title") { + | Some(String(s)) => s + | _ => "" + } + | _ => "" + } + + let categories = await manualTableOfContents() + + let compiledMdx = await MdxFile.compileMdx(raw, ~filePath, ~remarkPlugins=Mdx.plugins) + + // Build table of contents entries from markdown headings + let markdownTree = Mdast.fromMarkdown(raw) + let tocResult = Mdast.toc(markdownTree, {maxDepth: 2}) + + let headers = Dict.make() + Mdast.reduceHeaders(tocResult.map, headers) + + let entries = + headers + ->Dict.toArray + ->Array.map(((header, url)): TableOfContents.entry => { + header, + href: (url :> string), + }) + ->Array.slice(~start=2) // skip document entry and H1 title, keep h2 sections + + { + compiledMdx, + categories, + entries, + title: `${title} | ReScript Language Manual`, + description, + filePath, + } +} + +let default = () => { + let {pathname} = ReactRouter.useLocation() + let {compiledMdx, categories, entries, title, description, filePath} = ReactRouter.useLoaderData() + + let breadcrumbs = list{ + {Url.name: "Docs", href: "/docs/manual/introduction"}, + { + Url.name: "Language Manual", + href: "/docs/manual/introduction", + }, + } + + let editHref = `https://github.com/rescript-lang/rescript-lang.org/blob/master/${filePath}` + + let sidebarContent = + + + <> + + + + + + {React.string("Edit")} + + + +
+ +
+
+ +} diff --git a/app/routes/DocsManualRoute.resi b/app/routes/DocsManualRoute.resi new file mode 100644 index 000000000..a8a275a4c --- /dev/null +++ b/app/routes/DocsManualRoute.resi @@ -0,0 +1,12 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + categories: array, + entries: array, + title: string, + description: string, + filePath: string, +} + +let loader: ReactRouter.Loader.t + +let default: unit => React.element diff --git a/app/routes/DocsReactRoute.res b/app/routes/DocsReactRoute.res new file mode 100644 index 000000000..644fd5d87 --- /dev/null +++ b/app/routes/DocsReactRoute.res @@ -0,0 +1,152 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + categories: array, + entries: array, + title: string, + description: string, + filePath: string, +} + +// Build sidebar categories from all React docs, sorted by their "order" field in frontmatter +let reactTableOfContents = async () => { + let groups = + (await MdxFile.loadAllAttributes(~dir="markdown-pages/docs")) + ->Mdx.filterMdxPages("docs/react") + ->Mdx.groupBySection + ->Dict.mapValues(values => + values->Mdx.sortSection->SidebarHelpers.convertToNavItems("/docs/react") + ) + + SidebarHelpers.getAllGroups( + groups, + ["Overview", "Main Concepts", "Hooks & State Management", "Guides"], + ) +} + +let loader: ReactRouter.Loader.t = async ({request}) => { + let {pathname} = WebAPI.URL.make(~url=request.url) + let filePath = MdxFile.resolveFilePath( + (pathname :> string), + ~dir="markdown-pages/docs/react", + ~alias="docs/react", + ) + + let raw = await Node.Fs.readFile(filePath, "utf-8") + let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw) + + let description = switch frontmatter { + | Object(dict) => + switch dict->Dict.get("description") { + | Some(String(s)) => s + | _ => "" + } + | _ => "" + } + + let title = switch frontmatter { + | Object(dict) => + switch dict->Dict.get("title") { + | Some(String(s)) => s + | _ => "" + } + | _ => "" + } + + let categories = await reactTableOfContents() + + let compiledMdx = await MdxFile.compileMdx(raw, ~filePath, ~remarkPlugins=Mdx.plugins) + + // Build table of contents entries from markdown headings + let markdownTree = Mdast.fromMarkdown(raw) + let tocResult = Mdast.toc(markdownTree, {maxDepth: 2}) + + let headers = Dict.make() + Mdast.reduceHeaders(tocResult.map, headers) + + let entries = + headers + ->Dict.toArray + ->Array.map(((header, url)): TableOfContents.entry => { + header, + href: (url :> string), + }) + ->Array.slice(~start=2) // skip document entry and H1 title, keep h2 sections + + { + compiledMdx, + categories, + entries, + title: `${title} | ReScript React`, + description, + filePath, + } +} + +let default = () => { + let {pathname} = ReactRouter.useLocation() + let {compiledMdx, categories, entries, title, description, filePath} = ReactRouter.useLoaderData() + + let breadcrumbs = list{ + {Url.name: "Docs", href: "/docs/react/introduction"}, + { + Url.name: "rescript-react", + href: "/docs/react/introduction", + }, + } + + let editHref = `https://github.com/rescript-lang/rescript-lang.org/blob/master/${filePath}` + + let sidebarContent = + + + <> + + + + + + {React.string("Edit")} + + + +
+ +
+
+ +} diff --git a/app/routes/DocsReactRoute.resi b/app/routes/DocsReactRoute.resi new file mode 100644 index 000000000..a8a275a4c --- /dev/null +++ b/app/routes/DocsReactRoute.resi @@ -0,0 +1,12 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + categories: array, + entries: array, + title: string, + description: string, + filePath: string, +} + +let loader: ReactRouter.Loader.t + +let default: unit => React.element diff --git a/app/routes/MdxRoute.res b/app/routes/MdxRoute.res index 55f1bf4fb..ca1ae17f1 100644 --- a/app/routes/MdxRoute.res +++ b/app/routes/MdxRoute.res @@ -4,54 +4,11 @@ type loaderData = { ...Mdx.t, categories: array, entries: array, - blogPost?: BlogApi.post, - mdxSources?: array, - activeSyntaxItem?: SyntaxLookup.item, breadcrumbs?: list, title: string, filePath: option, } -/** - This configures the MDX component to use our custom markdown components - */ -let components = { - // Replacing HTML defaults - "a": Markdown.A.make, - "blockquote": Markdown.Blockquote.make, - "code": Markdown.Code.make, - "h1": Markdown.H1.make, - "h2": Markdown.H2.make, - "h3": Markdown.H3.make, - "h4": Markdown.H4.make, - "h5": Markdown.H5.make, - "hr": Markdown.Hr.make, - "intro": Markdown.Intro.make, - "li": Markdown.Li.make, - "ol": Markdown.Ol.make, - "p": Markdown.P.make, - "pre": Markdown.Pre.make, - "strong": Markdown.Strong.make, - "table": Markdown.Table.make, - "th": Markdown.Th.make, - "thead": Markdown.Thead.make, - "td": Markdown.Td.make, - "ul": Markdown.Ul.make, - // These are custom components we provide - "Cite": Markdown.Cite.make, - "CodeTab": Markdown.CodeTab.make, - "Image": Markdown.Image.make, - "Info": Markdown.Info.make, - "Intro": Markdown.Intro.make, - "UrlBox": Markdown.UrlBox.make, - "Video": Markdown.Video.make, - "Warn": Markdown.Warn.make, - "CommunityContent": CommunityContent.make, - "WarningTable": WarningTable.make, - "Docson": DocsonLazy.make, - "Suspense": React.Suspense.make, -} - let convertToNavItems = (items, rootPath) => Array.map(items, (item): SidebarLayout.Sidebar.NavItem.t => { let href = switch item.slug { @@ -100,189 +57,107 @@ let manualTableOfContents = async () => { categories } -let reactTableOfContents = async () => { - let groups = - (await allMdx(~filterByPaths=["markdown-pages/docs"])) - ->filterMdxPages("docs/react") - ->groupBySection - ->Dict.mapValues(values => values->sortSection->convertToNavItems("/docs/react")) - - // these are the categories that appear in the sidebar - let categories: array = getAllGroups( - groups, - ["Overview", "Main Concepts", "Hooks & State Management", "Guides"], - ) - - categories -} - -let communityTableOfContents = async () => { - let groups = - (await allMdx(~filterByPaths=["markdown-pages/community"])) - ->filterMdxPages("community") - ->groupBySection - ->Dict.mapValues(values => values->sortSection->convertToNavItems("/community")) - - // these are the categories that appear in the sidebar - let categories: array = getAllGroups(groups, ["Resources"]) - - categories -} - let loader: ReactRouter.Loader.t = async ({request}) => { let {pathname} = WebAPI.URL.make(~url=request.url) let mdx = await loadMdx(request, ~options={remarkPlugins: Mdx.plugins}) - if pathname->String.includes("blog") { - let res: loaderData = { - __raw: mdx.__raw, - attributes: mdx.attributes, - entries: [], - categories: [], - blogPost: mdx.attributes->BlogLoader.transform, - title: `${mdx.attributes.title} | ReScript Blog`, - filePath: None, + let categories = { + if pathname == "/docs/manual/api" { + [] + } else if pathname->String.includes("docs/manual") { + await manualTableOfContents() + } else { + [] } - res - } else if pathname->String.includes("syntax-lookup") { - let mdxSources = - (await allMdx(~filterByPaths=["markdown-pages/syntax-lookup"])) - ->Array.filter(page => - page.path - ->Option.map(String.includes(_, "syntax-lookup")) - ->Option.getOr(false) - ) - ->Array.map(SyntaxLookupRoute.convert) - - let activeSyntaxItem = - mdxSources->Array.find(item => item.id == mdx.attributes.id->Option.getOrThrow) + } - let res: loaderData = { - __raw: mdx.__raw, - attributes: mdx.attributes, - entries: [], - categories: [], - mdxSources, - ?activeSyntaxItem, - title: mdx.attributes.title, // TODO RR7: check if this is correct - filePath: None, + let filePath = ref(None) + + let fileContents = await (await allMdx()) + ->Array.filter(mdx => { + switch (mdx.slug, mdx.canonical) { + // Having a canonical path is the best way to ensure we get the right file + | (_, Nullable.Value(canonical)) => pathname == (canonical :> string) + // if we don't have a canonical path, see if we can find the slug in the pathname + | (Some(slug), _) => pathname->String.includes(slug) + // otherwise we can't match it and the build should fail + | _ => false } - res - } else { - let categories = { - if pathname == "/docs/manual/api" { - [] - } else if pathname->String.includes("docs/manual") { - await manualTableOfContents() - } else if pathname->String.includes("docs/react") { - await reactTableOfContents() - } else if pathname->String.includes("community") { - await communityTableOfContents() - } else { - [] - } - } - - let filePath = ref(None) - - let fileContents = await (await allMdx()) - ->Array.filter(mdx => { - switch (mdx.slug, mdx.canonical) { - // Having a canonical path is the best way to ensure we get the right file - | (_, Nullable.Value(canonical)) => pathname == (canonical :> string) - // if we don't have a canonical path, see if we can find the slug in the pathname - | (Some(slug), _) => pathname->String.includes(slug) - // otherwise we can't match it and the build should fail - | _ => false - } - }) - ->Array.get(0) - ->Option.flatMap(mdx => { - filePath := - mdx.path->Option.map(mdxPath => - String.slice(mdxPath, ~start=mdxPath->String.indexOf("rescript-lang.org/") + 17) - ) - // remove the filesystem path to get the relative path to the files in the repo - mdx.path - }) - ->Option.map(path => Node.Fs.readFile(path, "utf-8")) - ->Option.getOrThrow(~message="Could not find MDX file for path " ++ (pathname :> string)) + }) + ->Array.get(0) + ->Option.flatMap(mdx => { + filePath := + mdx.path->Option.map(mdxPath => + String.slice(mdxPath, ~start=mdxPath->String.indexOf("rescript-lang.org/") + 17) + ) + // remove the filesystem path to get the relative path to the files in the repo + mdx.path + }) + ->Option.map(path => Node.Fs.readFile(path, "utf-8")) + ->Option.getOrThrow(~message="Could not find MDX file for path " ++ (pathname :> string)) - let markdownTree = Mdast.fromMarkdown(fileContents) - let tocResult = Mdast.toc(markdownTree, {maxDepth: 2}) + let markdownTree = Mdast.fromMarkdown(fileContents) + let tocResult = Mdast.toc(markdownTree, {maxDepth: 2}) - let headers = Dict.make() + let headers = Dict.make() - Mdast.reduceHeaders(tocResult.map, headers) + Mdast.reduceHeaders(tocResult.map, headers) - let entries = - headers - ->Dict.toArray - ->Array.map(((header, url)): TableOfContents.entry => { - header, - href: (url :> string), - }) - ->Array.slice(~start=2) // skip first two entries which are the document entry and the H1 title for the page, we just want the h2 sections + let entries = + headers + ->Dict.toArray + ->Array.map(((header, url)): TableOfContents.entry => { + header, + href: (url :> string), + }) + ->Array.slice(~start=2) // skip first two entries which are the document entry and the H1 title for the page, we just want the h2 sections - let breadcrumbs = - pathname->String.includes("docs/manual") - ? Some(list{ - {Url.name: "Docs", href: "/docs/"}, - { - Url.name: "Language Manual", - href: "/docs/manual/" ++ "introduction", - }, - }) - : pathname->String.includes("docs/react") - ? Some(list{ + let breadcrumbs = + pathname->String.includes("docs/manual") + ? Some(list{ {Url.name: "Docs", href: "/docs/"}, { - Url.name: "rescript-react", - href: "/docs/react/" ++ "introduction", + Url.name: "Language Manual", + href: "/docs/manual/" ++ "introduction", }, }) - : None - - let metaTitleCategory = { - let path = (pathname :> string) - let title = if path->String.includes("docs/react") { - "ReScript React" - } else if path->String.includes("docs/manual/api") { - "ReScript API" - } else if path->String.includes("docs/manual") { - "ReScript Language Manual" - } else if path->String.includes("community") { - "ReScript Community" - } else { - "ReScript" - } - - title - } - - let title = if pathname == "/docs/manual/api" { - "API" + : None + + let metaTitleCategory = { + let path = (pathname :> string) + let title = if path->String.includes("docs/manual/api") { + "ReScript API" + } else if path->String.includes("docs/manual") { + "ReScript Language Manual" } else { - mdx.attributes.title + "ReScript" } - let res: loaderData = { - __raw: mdx.__raw, - attributes: mdx.attributes, - entries, - categories, - ?breadcrumbs, - title: `${title} | ${metaTitleCategory}`, - filePath: filePath.contents, - } - res + title + } + + let title = if pathname == "/docs/manual/api" { + "API" + } else { + mdx.attributes.title + } + + let res: loaderData = { + __raw: mdx.__raw, + attributes: mdx.attributes, + entries, + categories, + ?breadcrumbs, + title: `${title} | ${metaTitleCategory}`, + filePath: filePath.contents, } + res } let default = () => { let {pathname} = ReactRouter.useLocation() - let component = useMdxComponent(~components) + let component = useMdxComponent() let attributes = useMdxAttributes() let loaderData: loaderData = ReactRouter.useLoaderData() @@ -334,8 +209,7 @@ let default = () => { } else if ( (pathname :> string)->String.includes("docs/manual") || - (pathname :> string)->String.includes("docs/react") || - (pathname :> string)->String.includes("docs/guidelines") + (pathname :> string)->String.includes("docs/react") ) { <> Nullable.getOr("")} /> @@ -346,8 +220,6 @@ let default = () => { if index === 0 { if (pathname :> string)->String.includes("docs/manual") { {...item, href: "/docs/manual/introduction"} - } else if (pathname :> string)->String.includes("docs/react") { - {...item, href: "/docs/react/introduction"} } else { item } @@ -414,33 +286,8 @@ let default = () => { } - } else if (pathname :> string)->String.includes("community") { - -
{component()}
-
- } else if (pathname :> string)->String.includes("blog") { - switch loaderData.blogPost { - | Some({frontmatter, archived, path}) => - {component()} - | None => React.null // TODO: Post RR7 show an error? - } } else { - switch loaderData.mdxSources { - | Some(mdxSources) => - <> - Option.map(item => item.name) - ->Option.getOr("Syntax Lookup | ReScript API")} - description={attributes.description->Nullable.getOr("")} - /> - - - {component()} - - - | None => React.null - } + React.null }} } diff --git a/app/routes/MdxRoute.resi b/app/routes/MdxRoute.resi index b6a26c12c..8e4a827fb 100644 --- a/app/routes/MdxRoute.resi +++ b/app/routes/MdxRoute.resi @@ -2,7 +2,6 @@ type loaderData = { ...Mdx.t, categories: array, entries: array, - blogPost?: BlogApi.post, mdxSources?: array, activeSyntaxItem?: SyntaxLookup.item, breadcrumbs?: list, diff --git a/app/routes/SyntaxLookupDetailRoute.res b/app/routes/SyntaxLookupDetailRoute.res new file mode 100644 index 000000000..139e56985 --- /dev/null +++ b/app/routes/SyntaxLookupDetailRoute.res @@ -0,0 +1,71 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + mdxSources: array, + activeSyntaxItem: option, + title: string, + description: string, +} + +let loader: ReactRouter.Loader.t = async ({request}) => { + let {pathname} = WebAPI.URL.make(~url=request.url) + let filePath = MdxFile.resolveFilePath( + (pathname :> string), + ~dir="markdown-pages/syntax-lookup", + ~alias="syntax-lookup", + ) + + let raw = await Node.Fs.readFile(filePath, "utf-8") + let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw) + + let id = switch frontmatter { + | Object(dict) => + switch dict->Dict.get("id") { + | Some(String(s)) => s + | _ => "" + } + | _ => "" + } + + let name = switch frontmatter { + | Object(dict) => + switch dict->Dict.get("name") { + | Some(String(s)) => s + | _ => "" + } + | _ => "" + } + + let compiledMdx = await MdxFile.compileMdx(raw, ~filePath, ~remarkPlugins=Mdx.plugins) + + let mdxSources = + (await MdxFile.loadAllAttributes(~dir="markdown-pages/syntax-lookup"))->Array.map( + SyntaxLookupRoute.convert, + ) + + let activeSyntaxItem = mdxSources->Array.find(item => item.id == id) + + { + compiledMdx, + mdxSources, + activeSyntaxItem, + title: name, + description: "", + } +} + +let default = () => { + let {compiledMdx, mdxSources, activeSyntaxItem} = ReactRouter.useLoaderData() + + <> + Option.map(item => item.name) + ->Option.getOr("Syntax Lookup | ReScript API")} + description="" + /> + + + + + +} diff --git a/package.json b/package.json index b232590b1..8f43a6852 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "build:vite": "react-router build", "build": "yarn build:res && yarn build:scripts && yarn build:update-index && yarn build:vite", "ci:format": "prettier . --check --experimental-cli", - "ci:test": "yarn vitest --run --browser.headless --update", + "ci:test": "yarn vitest --run --browser.headless", "clean:res": "rescript clean", "convert-images": "auto-convert-images", "dev:res": "rescript watch", @@ -30,7 +30,8 @@ "preview": "yarn build && static-server build/client", "reanalyze": "rescript-tools reanalyze -all-cmt .", "test": "node scripts/test-examples.mjs && node scripts/test-hrefs.mjs", - "vitest": "vitest" + "vitest": "vitest", + "vitest:update": "vitest --run --browser.headless --update" }, "dependencies": { "@babel/generator": "^7.24.7", @@ -47,6 +48,7 @@ "@docsearch/react": "^4.3.1", "@headlessui/react": "^2.2.4", "@lezer/highlight": "^1.2.1", + "@mdx-js/mdx": "^3.1.1", "@node-cli/static-server": "^3.1.4", "@react-router/node": "^7.8.1", "@replit/codemirror-vim": "^6.3.0", diff --git a/src/Mdx.res b/src/Mdx.res index cd6b388db..b7840e20e 100644 --- a/src/Mdx.res +++ b/src/Mdx.res @@ -82,7 +82,9 @@ let sortSection = mdxPages => Array.toSorted(mdxPages, (a: attributes, b: attributes) => switch (a.order, b.order) { | (Some(a), Some(b)) => a > b ? 1.0 : -1.0 - | _ => -1.0 + | (Some(_), None) => -1.0 + | (None, Some(_)) => 1.0 + | (None, None) => 0.0 } ) diff --git a/src/MdxFile.res b/src/MdxFile.res new file mode 100644 index 000000000..58657d7cf --- /dev/null +++ b/src/MdxFile.res @@ -0,0 +1,106 @@ +type fileData = { + content: string, + frontmatter: JSON.t, +} + +type compileInput = {value: string, path: string} +type compileOptions = { + outputFormat: string, + remarkPlugins: array, +} +@module("@mdx-js/mdx") +external compile: (compileInput, compileOptions) => promise = "compile" + +@module("remark-frontmatter") external remarkFrontmatter: Mdx.remarkPlugin = "default" + +let compileMdx = async (content, ~filePath, ~remarkPlugins=[]) => { + let compiled = await compile( + {value: content, path: filePath}, + { + outputFormat: "function-body", + remarkPlugins: [remarkFrontmatter, ...remarkPlugins], + }, + ) + compiled->CompiledMdx.fromCompileResult +} + +let resolveFilePath = (pathname, ~dir, ~alias) => { + let path = if pathname->String.startsWith("/") { + pathname->String.slice(~start=1, ~end=String.length(pathname)) + } else { + pathname + } + let relativePath = if path->String.startsWith(alias ++ "/") { + let rest = path->String.slice(~start=String.length(alias) + 1, ~end=String.length(path)) + Node.Path.join2(dir, rest) + } else if path->String.startsWith(alias) { + let rest = path->String.slice(~start=String.length(alias), ~end=String.length(path)) + Node.Path.join2(dir, rest) + } else { + path + } + relativePath ++ ".mdx" +} + +let loadFile = async filePath => { + let raw = await Node.Fs.readFile(filePath, "utf-8") + let {frontmatter, content}: MarkdownParser.result = MarkdownParser.parseSync(raw) + {content, frontmatter} +} + +// Recursively scan a directory for .mdx files +let rec scanDir = (baseDir, currentDir) => { + let entries = Node.Fs.readdirSync(currentDir) + entries->Array.flatMap(entry => { + let fullPath = Node.Path.join2(currentDir, entry) + if Node.Fs.statSync(fullPath)["isDirectory"]() { + scanDir(baseDir, fullPath) + } else if Node.Path.extname(entry) === ".mdx" { + // Get the relative path from baseDir + let relativePath = + fullPath + ->String.replaceAll("\\", "/") + ->String.replace(baseDir->String.replaceAll("\\", "/") ++ "/", "") + ->String.replace(".mdx", "") + [relativePath] + } else { + [] + } + }) +} + +let scanPaths = (~dir, ~alias) => { + scanDir(dir, dir)->Array.map(relativePath => { + alias ++ "/" ++ relativePath + }) +} + +// Convert frontmatter JSON dict to Mdx.attributes +// This is the same unsafe approach as react-router-mdx — frontmatter YAML +// becomes a JS object that we type as Mdx.attributes. Fields not present +// in the frontmatter (e.g. blog-specific `author`, `date`) are undefined at +// runtime, which is fine because docs/community code never accesses them. +external dictToAttributes: Dict.t => Mdx.attributes = "%identity" + +let loadAllAttributes = async (~dir) => { + let files = scanDir(dir, dir) + await Promise.all( + files->Array.map(async relativePath => { + let fullPath = Node.Path.join2(dir, relativePath ++ ".mdx")->String.replaceAll("\\", "/") + let raw = await Node.Fs.readFile(fullPath, "utf-8") + let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw) + + let dict = switch frontmatter { + | Object(dict) => dict + | _ => Dict.make() + } + + // Add path and slug fields (same as react-router-mdx does) + dict->Dict.set("path", JSON.String(fullPath)) + let slug = Node.Path.basename(relativePath) + dict->Dict.set("slug", JSON.String(slug)) + + dictToAttributes(dict) + }), + ) +} diff --git a/src/MdxFile.resi b/src/MdxFile.resi new file mode 100644 index 000000000..dc50472d3 --- /dev/null +++ b/src/MdxFile.resi @@ -0,0 +1,32 @@ +type fileData = { + content: string, + frontmatter: JSON.t, +} + +/** Maps a URL pathname to an .mdx file path on disk. + * e.g. `/blog/release-12-0-0` with ~dir="markdown-pages/blog" ~alias="blog" + * → `markdown-pages/blog/release-12-0-0.mdx` + */ +let resolveFilePath: (string, ~dir: string, ~alias: string) => string + +/** Read a file from disk and parse its frontmatter using MarkdownParser. */ +let loadFile: string => promise + +/** Scan a directory recursively for .mdx files and return URL paths. + * e.g. scanPaths(~dir="markdown-pages/blog", ~alias="blog") + * → ["blog/release-12-0-0", "blog/archived/some-post", ...] + */ +let scanPaths: (~dir: string, ~alias: string) => array + +/** Compile raw MDX content into a function-body string using @mdx-js/mdx. */ +let compileMdx: ( + string, + ~filePath: string, + ~remarkPlugins: array=?, +) => promise + +/** Scan all .mdx files in a directory, parse frontmatter only, and return + * as Mdx.attributes with `path` and `slug` fields populated. + * Replaces `react-router-mdx`'s `loadAllMdx`. + */ +let loadAllAttributes: (~dir: string) => promise> diff --git a/src/SidebarHelpers.res b/src/SidebarHelpers.res new file mode 100644 index 000000000..f9f4a3532 --- /dev/null +++ b/src/SidebarHelpers.res @@ -0,0 +1,23 @@ +let convertToNavItems = (items, rootPath) => + Array.map(items, (item): SidebarLayout.Sidebar.NavItem.t => { + let href = switch item.Mdx.slug { + | Some(slug) => `${rootPath}/${slug}` + | None => rootPath + } + { + name: item.title, + href, + } + }) + +let getGroup = (groups, groupName): SidebarLayout.Sidebar.Category.t => { + { + name: groupName, + items: groups + ->Dict.get(groupName) + ->Option.getOr([]), + } +} + +let getAllGroups = (groups, groupNames): array => + groupNames->Array.map(item => getGroup(groups, item)) diff --git a/src/SidebarHelpers.resi b/src/SidebarHelpers.resi new file mode 100644 index 000000000..10b79cecd --- /dev/null +++ b/src/SidebarHelpers.resi @@ -0,0 +1,14 @@ +/** Convert Mdx.attributes to sidebar nav items, building hrefs from rootPath + slug. */ +let convertToNavItems: (array, string) => array + +/** Get a single sidebar category by name from a dict of grouped nav items. */ +let getGroup: ( + Dict.t>, + string, +) => SidebarLayout.Sidebar.Category.t + +/** Get multiple sidebar categories by name from a dict of grouped nav items. */ +let getAllGroups: ( + Dict.t>, + array, +) => array diff --git a/src/common/CompiledMdx.res b/src/common/CompiledMdx.res new file mode 100644 index 000000000..840c4dde2 --- /dev/null +++ b/src/common/CompiledMdx.res @@ -0,0 +1,5 @@ +type t = string + +type compileResult + +@send external fromCompileResult: compileResult => t = "toString" diff --git a/src/common/CompiledMdx.resi b/src/common/CompiledMdx.resi new file mode 100644 index 000000000..3e9eee388 --- /dev/null +++ b/src/common/CompiledMdx.resi @@ -0,0 +1,5 @@ +type t + +type compileResult + +@send external fromCompileResult: compileResult => t = "toString" diff --git a/src/common/MarkdownParser.res b/src/common/MarkdownParser.res index 3a0a756ab..1974c75a5 100644 --- a/src/common/MarkdownParser.res +++ b/src/common/MarkdownParser.res @@ -27,6 +27,15 @@ type result = { let vfileMatterPlugin = makePlugin(_options => (_tree, vfile) => vfileMatter(vfile)) +type remarkNode = {@as("type") type_: string} +type remarkTree = {mutable children: array} + +let stripFrontmatterPlugin = makePlugin(_options => + (tree, _vfile) => { + tree.children = tree.children->Array.filter(node => node.type_ !== "yaml") + } +) + let parser = make() ->use(remarkParse) @@ -35,6 +44,7 @@ let parser = ->use(remarkComment) ->useOptions(remarkFrontmatter, [{"type": "yaml", "marker": "-"}]) ->use(vfileMatterPlugin) + ->use(stripFrontmatterPlugin) let parseSync = content => { let vfile = parser->processSync(content) diff --git a/src/components/MdxContent.res b/src/components/MdxContent.res new file mode 100644 index 000000000..9b51cc393 --- /dev/null +++ b/src/components/MdxContent.res @@ -0,0 +1,100 @@ +// --------------------------------------------------------------------------- +// JSX runtime values needed by runSync +// --------------------------------------------------------------------------- + +// We re-import the jsx-runtime exports as opaque values so we can pass them +// through to runSync without running into ReScript's monomorphisation of +// the polymorphic `React.jsx` / `React.jsxs` signatures. +/** + * MdxContent — renders compiled MDX content as a React component. + * + * Uses `runSync` from `@mdx-js/mdx` to evaluate compiled MDX (produced by + * `MdxFile.compileMdx`) and renders the result with a shared component map. + */ +type jsxRuntimeValue + +@module("react/jsx-runtime") external fragment: jsxRuntimeValue = "Fragment" +@module("react/jsx-runtime") external jsx: jsxRuntimeValue = "jsx" +@module("react/jsx-runtime") external jsxs: jsxRuntimeValue = "jsxs" + +@val @scope(("import", "meta")) external importMetaUrl: string = "url" + +// --------------------------------------------------------------------------- +// @mdx-js/mdx runSync binding +// --------------------------------------------------------------------------- + +type runOptions = { + @as("Fragment") fragment: jsxRuntimeValue, + jsx: jsxRuntimeValue, + jsxs: jsxRuntimeValue, + baseUrl: string, +} + +type mdxModule + +@module("@mdx-js/mdx") +external runSync: (CompiledMdx.t, runOptions) => mdxModule = "runSync" + +@get external getDefault: mdxModule => React.component<{..}> = "default" + +let runOptions = { + fragment, + jsx, + jsxs, + baseUrl: importMetaUrl, +} + +// --------------------------------------------------------------------------- +// Shared MDX component map +// --------------------------------------------------------------------------- + +let components = { + // Standard HTML element overrides + "a": Markdown.A.make, + "blockquote": Markdown.Blockquote.make, + "code": Markdown.Code.make, + "h1": Markdown.H1.make, + "h2": Markdown.H2.make, + "h3": Markdown.H3.make, + "h4": Markdown.H4.make, + "h5": Markdown.H5.make, + "hr": Markdown.Hr.make, + "li": Markdown.Li.make, + "ol": Markdown.Ol.make, + "p": Markdown.P.make, + "pre": Markdown.Pre.make, + "strong": Markdown.Strong.make, + "table": Markdown.Table.make, + "th": Markdown.Th.make, + "thead": Markdown.Thead.make, + "td": Markdown.Td.make, + "ul": Markdown.Ul.make, + // Custom MDX components + "Cite": Markdown.Cite.make, + "CodeTab": Markdown.CodeTab.make, + "Image": Markdown.Image.make, + "Info": Markdown.Info.make, + "Intro": Markdown.Intro.make, + "UrlBox": Markdown.UrlBox.make, + "Video": Markdown.Video.make, + "Warn": Markdown.Warn.make, + "CommunityContent": CommunityContent.make, + "WarningTable": WarningTable.make, + "Docson": DocsonLazy.make, + "Suspense": React.Suspense.make, +} + +// --------------------------------------------------------------------------- +// React component +// --------------------------------------------------------------------------- + +@react.component +let make = (~compiledMdx: CompiledMdx.t) => { + let element = React.useMemo(() => { + let mdxModule = runSync(compiledMdx, runOptions) + let content = getDefault(mdxModule) + React.jsx(content, {"components": components}) + }, [compiledMdx]) + + element +} diff --git a/src/components/MdxContent.resi b/src/components/MdxContent.resi new file mode 100644 index 000000000..882a3530b --- /dev/null +++ b/src/components/MdxContent.resi @@ -0,0 +1,2 @@ +@react.component +let make: (~compiledMdx: CompiledMdx.t) => React.element diff --git a/styles/test-overrides.css b/styles/test-overrides.css new file mode 100644 index 000000000..b4631ed3b --- /dev/null +++ b/styles/test-overrides.css @@ -0,0 +1,20 @@ +/* + * Test overrides for consistent screenshots. + * This file is only imported in vitest.setup.mjs. + */ + +/* Disable all CSS animations and transitions */ +*, +*::before, +*::after { + animation-duration: 0s !important; + animation-delay: 0s !important; + transition-duration: 0s !important; + transition-delay: 0s !important; +} + +/* Force overlay scrollbars so scrollbar width is 0 in both headless and headed mode */ +* { + scrollbar-width: none !important; + scrollbar-gutter: auto !important; +} diff --git a/vitest.config.mjs b/vitest.config.mjs index 49417630f..dbaeaa05f 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -10,14 +10,31 @@ export default defineConfig({ setupFiles: ["./vitest.setup.mjs"], browser: { enabled: true, - provider: playwright(), + provider: playwright({ + contextOptions: { + deviceScaleFactor: 1, + }, + }), + ui: false, // https://vitest.dev/config/browser/playwright + provider: playwright(), instances: [ { browser: "chromium", viewport: { width: 1440, height: 900 }, }, ], + expect: { + toMatchScreenshot: { + screenshotOptions: { + scale: "css", + }, + comparatorOptions: { + threshold: 0.2, + allowedMismatchedPixelRatio: 0.05, + }, + }, + }, }, }, }); diff --git a/vitest.setup.mjs b/vitest.setup.mjs index 6d48aafc1..3ba70e746 100644 --- a/vitest.setup.mjs +++ b/vitest.setup.mjs @@ -1,2 +1,3 @@ import "./styles/main.css"; import "./styles/utils.css"; +import "./styles/test-overrides.css"; diff --git a/yarn.lock b/yarn.lock index 02ae82c71..6ccb0d181 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2003,7 +2003,7 @@ __metadata: languageName: node linkType: hard -"@mdx-js/mdx@npm:^3.1.0": +"@mdx-js/mdx@npm:^3.1.0, @mdx-js/mdx@npm:^3.1.1": version: 3.1.1 resolution: "@mdx-js/mdx@npm:3.1.1" dependencies: @@ -9489,6 +9489,7 @@ __metadata: "@docsearch/react": "npm:^4.3.1" "@headlessui/react": "npm:^2.2.4" "@lezer/highlight": "npm:^1.2.1" + "@mdx-js/mdx": "npm:^3.1.1" "@node-cli/static-server": "npm:^3.1.4" "@prettier/plugin-oxc": "npm:^0.0.4" "@react-router/dev": "npm:^7.8.1"