diff --git a/.yarn/patches/react-router-mdx-npm-1.0.8-d4402c3003.patch b/.yarn/patches/react-router-mdx-npm-1.0.8-d4402c3003.patch deleted file mode 100644 index 6b1496b84..000000000 --- a/.yarn/patches/react-router-mdx-npm-1.0.8-d4402c3003.patch +++ /dev/null @@ -1,40 +0,0 @@ -diff --git a/dist/server/index.js b/dist/server/index.js -index 1e56a6c..b323b05 100644 ---- a/dist/server/index.js -+++ b/dist/server/index.js -@@ -59,15 +59,15 @@ import matter from "gray-matter"; - import { compile as compileMdx } from "@mdx-js/mdx"; - import remarkFrontmatter from "remark-frontmatter"; - var compile = async (content, options) => { -- const compiled = await compileMdx(content, { -+ const compiled = await compileMdx({ value: content, path: options?.path }, { - ...options ?? {}, - outputFormat: "function-body", -- remarkPlugins: [remarkFrontmatter, ...options?.remarkPlugins ?? []] -- }); -+ remarkPlugins: [remarkFrontmatter, ...options?.remarkPlugins ?? []], -+ },); - return String(compiled); - }; - var getAttributes = (content) => { -- const { data: attributes } = matter(content); -+ const { data: attributes, } = matter(content); - return attributes; - }; - -@@ -119,12 +119,13 @@ var loadMdx = async (request, options) => { - const path = getFilePathBasedOnUrl(request.url, paths, aliases); - const content = await getFileContent(path); - const [mdxContent, attributes] = await Promise.all([ -- compile(content, options), -+ compile(content, { ...options, path }), - getAttributes(content) - ]); - return { - __raw: mdxContent, -- attributes -+ attributes, -+ path - }; - }; - var loadAllMdx = async (filterByPaths) => { diff --git a/AGENTS.md b/AGENTS.md index 539a7ea1e..e87778a22 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -201,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/app/routes.res b/app/routes.res index db86b1ff5..86db78a6b 100644 --- a/app/routes.res +++ b/app/routes.res @@ -47,26 +47,31 @@ let blogArticleRoutes = 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 mdxRoutes = mdxRoutes("./routes/MdxRoute.jsx")->Array.filter(r => - !( - r.path - ->Option.map(path => - path === "blog" || - String.startsWith(path, "blog/") || - path === "community" || - String.startsWith(path, "community/") || - path === "docs/manual/api" || - path === "community" || - String.startsWith(path, "community/") - ) - ->Option.getOr(false) +let syntaxLookupDetailRoutes = + MdxFile.scanPaths(~dir="markdown-pages/syntax-lookup", ~alias="syntax-lookup")->Array.map(path => + route(path, "./routes/SyntaxLookupDetailRoute.jsx", ~options={id: path}) ) -) let default = [ index("./routes/LandingPageRoute.jsx"), @@ -85,7 +90,10 @@ let default = [ ...beltRoutes, ...domRoutes, ...blogArticleRoutes, + ...docsManualRoutes, + ...docsReactRoutes, + ...docsGuidelinesRoutes, ...communityRoutes, - ...mdxRoutes, + ...syntaxLookupDetailRoutes, route("*", "./routes/NotFoundRoute.jsx"), ] diff --git a/app/routes.resi b/app/routes.resi index d4cf9a37e..ebb3514d4 100644 --- a/app/routes.resi +++ b/app/routes.resi @@ -1,5 +1,13 @@ let stdlibPaths: array +let domPaths: array let beltPaths: array let stdlibRoutes: array +let domRoutes: array let beltRoutes: array -let default: Belt.Array.t +let blogArticleRoutes: array +let docsManualRoutes: array +let docsReactRoutes: array +let docsGuidelinesRoutes: array +let communityRoutes: array +let syntaxLookupDetailRoutes: array +let default: array diff --git a/app/routes/BlogRoute.res b/app/routes/BlogRoute.res index 7d7745226..d3999e6f4 100644 --- a/app/routes/BlogRoute.res +++ b/app/routes/BlogRoute.res @@ -3,8 +3,7 @@ type loaderData = {posts: array, category: Blog.category} let loader: ReactRouter.Loader.t = async ({request}) => { let showArchived = request.url->String.includes("archived") let posts = async () => - (await Mdx.allMdx(~filterByPaths=["markdown-pages/blog"])) - ->Mdx.filterMdxPages("blog") + (await MdxFile.loadAllAttributes(~dir="markdown-pages/blog")) ->Array.map(BlogLoader.transform) ->Array.toSorted((a, b) => { a.frontmatter.date->DateStr.toDate > b.frontmatter.date->DateStr.toDate ? -1.0 : 1.0 diff --git a/app/routes/CommunityRoute.res b/app/routes/CommunityRoute.res index 678575354..627efdf0c 100644 --- a/app/routes/CommunityRoute.res +++ b/app/routes/CommunityRoute.res @@ -7,38 +7,16 @@ type loaderData = { categories: array, } -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)) - let communityTableOfContents = async () => { let groups = - (await Mdx.allMdx(~filterByPaths=["markdown-pages/community"])) + (await MdxFile.loadAllAttributes(~dir="markdown-pages/community")) ->Mdx.filterMdxPages("community") ->Mdx.groupBySection - ->Dict.mapValues(values => values->Mdx.sortSection->convertToNavItems("/community")) + ->Dict.mapValues(values => + values->Mdx.sortSection->SidebarHelpers.convertToNavItems("/community") + ) - getAllGroups(groups, ["Resources"]) + SidebarHelpers.getAllGroups(groups, ["Resources"]) } let loader: ReactRouter.Loader.t = async ({request}) => { 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/MdxRoute.resi b/app/routes/DocsManualRoute.resi similarity index 57% rename from app/routes/MdxRoute.resi rename to app/routes/DocsManualRoute.resi index 8e4a827fb..a8a275a4c 100644 --- a/app/routes/MdxRoute.resi +++ b/app/routes/DocsManualRoute.resi @@ -1,12 +1,10 @@ type loaderData = { - ...Mdx.t, + compiledMdx: CompiledMdx.t, categories: array, entries: array, - mdxSources?: array, - activeSyntaxItem?: SyntaxLookup.item, - breadcrumbs?: list, title: string, - filePath: option, + description: string, + filePath: string, } let loader: ReactRouter.Loader.t 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 deleted file mode 100644 index 889a7c890..000000000 --- a/app/routes/MdxRoute.res +++ /dev/null @@ -1,357 +0,0 @@ -open Mdx - -type loaderData = { - ...Mdx.t, - categories: array, - entries: array, - 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 { - | 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)) - -// These are the pages for the language manual, sorted by their "order" field in the frontmatter -let manualTableOfContents = async () => { - let groups = - (await allMdx(~filterByPaths=["markdown-pages/docs"])) - ->filterMdxPages("docs/manual") - ->groupBySection - ->Dict.mapValues(values => values->sortSection->convertToNavItems("/docs/manual")) - - // these are the categories that appear in the sidebar - let categories: array = getAllGroups( - groups, - [ - "Overview", - "Guides", - "Language Features", - "JavaScript Interop", - "Build System", - "Advanced Features", - ], - ) - - 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 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("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, - } - res - } else { - let categories = { - if pathname->String.includes("docs/manual") { - await manualTableOfContents() - } else if pathname->String.includes("docs/react") { - await reactTableOfContents() - } 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)) - - let markdownTree = Mdast.fromMarkdown(fileContents) - 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 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{ - {Url.name: "Docs", href: "/docs/"}, - { - Url.name: "rescript-react", - href: "/docs/react/" ++ "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") { - "ReScript Language Manual" - } else { - "ReScript" - } - - title - } - - let title = 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 attributes = useMdxAttributes() - - let loaderData: loaderData = ReactRouter.useLoaderData() - - let {entries, categories, title} = loaderData - - <> - {if ( - (pathname :> string)->String.includes("docs/manual") || - (pathname :> string)->String.includes("docs/react") || - (pathname :> string)->String.includes("docs/guidelines") - ) { - <> - Nullable.getOr("")} /> - - { - let breadcrumbs = loaderData.breadcrumbs->Option.map(crumbs => - List.mapWithIndex(crumbs, (item, index) => { - 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 - } - } else { - item - } - }) - ) - let editHref = `https://github.com/rescript-lang/rescript-lang.org/blob/master${loaderData.filePath->Option.getOrThrow}` - - let sidebarContent = - - - <> - - {breadcrumbs->Option.mapOr(React.null, crumbs => - - )} - - {React.string("Edit")} - - - -
{component()}
-
- - } - - } 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 - } - }} - -} 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/app/routes/SyntaxLookupDetailRoute.resi b/app/routes/SyntaxLookupDetailRoute.resi new file mode 100644 index 000000000..5436eda52 --- /dev/null +++ b/app/routes/SyntaxLookupDetailRoute.resi @@ -0,0 +1,11 @@ +type loaderData = { + compiledMdx: CompiledMdx.t, + mdxSources: array, + activeSyntaxItem: option, + title: string, + description: string, +} + +let loader: ReactRouter.Loader.t + +let default: unit => React.element diff --git a/app/routes/SyntaxLookupRoute.res b/app/routes/SyntaxLookupRoute.res index b2a0e8e25..11ddf738c 100644 --- a/app/routes/SyntaxLookupRoute.res +++ b/app/routes/SyntaxLookupRoute.res @@ -1,6 +1,4 @@ open ReactRouter -open Mdx - type loaderData = {mdxSources: array} let convert = (mdx: Mdx.attributes): SyntaxLookup.item => { @@ -16,14 +14,8 @@ let convert = (mdx: Mdx.attributes): SyntaxLookup.item => { } let loader: Loader.t = async _ => { - let allMdx = await Shims.runWithoutLogging(() => loadAllMdx()) - let mdxSources = - allMdx - ->Array.filter(page => - page.path->Option.map(String.includes(_, "syntax-lookup"))->Option.getOr(false) - ) - ->Array.map(convert) + (await MdxFile.loadAllAttributes(~dir="markdown-pages/syntax-lookup"))->Array.map(convert) { mdxSources: mdxSources, diff --git a/generate-route-types.mjs b/generate-route-types.mjs index 1d36da5a0..9d9d80e02 100644 --- a/generate-route-types.mjs +++ b/generate-route-types.mjs @@ -1,15 +1,4 @@ import fs from "fs/promises"; -import { init } from "react-router-mdx/server"; - -init({ - paths: [ - "markdown-pages/blog", - "markdown-pages/docs", - "markdown-pages/community", - "markdown-pages/syntax-lookup", - ], - aliases: ["blog", "docs", "community", "syntax-lookup"], -}); const { default: routes } = await import("./app/routes.mjs"); diff --git a/package.json b/package.json index 21a3b1854..4cd26b55f 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "react-markdown": "^10.1.0", "react-router": "^7.14.0", "react-router-dom": "^7.14.0", - "react-router-mdx": "patch:react-router-mdx@npm%3A1.0.8#~/.yarn/patches/react-router-mdx-npm-1.0.8-d4402c3003.patch", "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.1", "remark": "^15.0.1", diff --git a/react-router.config.mjs b/react-router.config.mjs index ddb22da04..5e52df970 100644 --- a/react-router.config.mjs +++ b/react-router.config.mjs @@ -1,15 +1,4 @@ import * as fs from "node:fs"; -import { init } from "react-router-mdx/server"; - -const mdx = init({ - paths: [ - "markdown-pages/blog", - "markdown-pages/docs", - "markdown-pages/community", - "markdown-pages/syntax-lookup", - ], - aliases: ["blog", "docs", "community", "syntax-lookup"], -}); const { stdlibPaths } = await import("./app/routes.jsx"); @@ -17,11 +6,7 @@ export default { ssr: false, async prerender({ getStaticPaths }) { - return [ - ...(await getStaticPaths()), - ...(await mdx.paths()), - ...stdlibPaths, - ]; + return [...(await getStaticPaths()), ...stdlibPaths]; }, buildEnd: async () => { fs.cpSync("./build/client", "./out", { recursive: true }); diff --git a/src/Mdx.res b/src/Mdx.res index cd6b388db..9eccb018a 100644 --- a/src/Mdx.res +++ b/src/Mdx.res @@ -47,23 +47,6 @@ type remarkPlugin type tree -type loadMdxOptions = {remarkPlugins?: array} - -@module("react-router-mdx/server") -external loadMdx: (WebAPI.FetchAPI.request, ~options: loadMdxOptions=?) => promise = "loadMdx" - -@module("react-router-mdx/client") -external useMdxAttributes: unit => attributes = "useMdxAttributes" - -@module("react-router-mdx/client") -external useMdxComponent: (~components: {..}=?) => Jsx.component<'a> = "useMdxComponent" - -@module("react-router-mdx/server") -external loadAllMdx: (~filterByPaths: array=?) => promise> = "loadAllMdx" - -@module("react-router-mdx/client") -external useMdxFiles: unit => {..} = "useMdxFiles" - @module("remark-gfm") external gfm: remarkPlugin = "default" @@ -73,16 +56,13 @@ external validateLinks: remarkPlugin = "default" @module("mdast-util-to-string") external childrenToString: {..} => string = "toString" -// The loadAllMdx function logs out all of the file contents as it reads them, which is noisy and not useful. -// We can suppress that logging with this helper function. -let allMdx = async (~filterByPaths: option>=?) => - await Shims.runWithoutLogging(() => loadAllMdx(~filterByPaths?)) - 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 index 0366cf8b3..58657d7cf 100644 --- a/src/MdxFile.res +++ b/src/MdxFile.res @@ -74,3 +74,33 @@ let scanPaths = (~dir, ~alias) => { 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 index 9ca94395e..dc50472d3 100644 --- a/src/MdxFile.resi +++ b/src/MdxFile.resi @@ -24,3 +24,9 @@ let compileMdx: ( ~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/bindings/ReactRouter.res b/src/bindings/ReactRouter.res index 9aa991f79..194d31343 100644 --- a/src/bindings/ReactRouter.res +++ b/src/bindings/ReactRouter.res @@ -114,9 +114,6 @@ module Routes = { @module("@react-router/dev/routes") external layout: (string, array) => t = "layout" - - @module("react-router-mdx/server") - external mdxRoutes: string => array = "routes" } module BrowserRouter = { diff --git a/yarn.lock b/yarn.lock index 5d52d07ec..7159716fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1508,13 +1508,6 @@ __metadata: languageName: node linkType: hard -"@isaacs/cliui@npm:^9.0.0": - version: 9.0.0 - resolution: "@isaacs/cliui@npm:9.0.0" - checksum: 10c0/971063b7296419f85053dacd0a0285dcadaa3dfc139228b23e016c1a9848121ad4aa5e7fcca7522062014e1eb6239a7424188b9f2cba893a79c90aae5710319c - languageName: node - linkType: hard - "@isaacs/fs-minipass@npm:^4.0.0": version: 4.0.1 resolution: "@isaacs/fs-minipass@npm:4.0.1" @@ -1628,7 +1621,7 @@ __metadata: languageName: node linkType: hard -"@mdx-js/mdx@npm:^3.1.0, @mdx-js/mdx@npm:^3.1.1": +"@mdx-js/mdx@npm:^3.1.1": version: 3.1.1 resolution: "@mdx-js/mdx@npm:3.1.1" dependencies: @@ -1661,18 +1654,6 @@ __metadata: languageName: node linkType: hard -"@mdx-js/react@npm:^3.1.0": - version: 3.1.1 - resolution: "@mdx-js/react@npm:3.1.1" - dependencies: - "@types/mdx": "npm:^2.0.0" - peerDependencies: - "@types/react": ">=16" - react: ">=16" - checksum: 10c0/34ca98bc2a0f969894ea144dc5c8a5294690505458cd24965cd9be854d779c193ad9192bf9143c4c18438fafd1902e100d99067e045c69319288562d497558c6 - languageName: node - linkType: hard - "@mjackson/node-fetch-server@npm:^0.2.0": version: 0.2.0 resolution: "@mjackson/node-fetch-server@npm:0.2.0" @@ -3351,15 +3332,6 @@ __metadata: languageName: node linkType: hard -"argparse@npm:^1.0.7": - version: 1.0.10 - resolution: "argparse@npm:1.0.10" - dependencies: - sprintf-js: "npm:~1.0.2" - checksum: 10c0/b2972c5c23c63df66bca144dbc65d180efa74f25f8fd9b7d9a0a6c88ae839db32df3d54770dcb6460cf840d232b60695d1a6b1053f599d84e73f7437087712de - languageName: node - linkType: hard - "array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": version: 1.0.2 resolution: "array-buffer-byte-length@npm:1.0.2" @@ -5047,16 +5019,6 @@ __metadata: languageName: node linkType: hard -"esprima@npm:^4.0.0": - version: 4.0.1 - resolution: "esprima@npm:4.0.1" - bin: - esparse: ./bin/esparse.js - esvalidate: ./bin/esvalidate.js - checksum: 10c0/ad4bab9ead0808cf56501750fd9d3fb276f6b105f987707d059005d57e182d18a7c9ec7f3a01794ebddcca676773e42ca48a32d67a250c9d35e009ca613caba3 - languageName: node - linkType: hard - "estree-util-attach-comments@npm:^3.0.0": version: 3.0.0 resolution: "estree-util-attach-comments@npm:3.0.0" @@ -5246,15 +5208,6 @@ __metadata: languageName: node linkType: hard -"extend-shallow@npm:^2.0.1": - version: 2.0.1 - resolution: "extend-shallow@npm:2.0.1" - dependencies: - is-extendable: "npm:^0.1.0" - checksum: 10c0/ee1cb0a18c9faddb42d791b2d64867bd6cfd0f3affb711782eb6e894dd193e2934a7f529426aac7c8ddb31ac5d38000a00aa2caf08aa3dfc3e1c8ff6ba340bd9 - languageName: node - linkType: hard - "extend@npm:^3.0.0, extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -5486,7 +5439,7 @@ __metadata: languageName: node linkType: hard -"foreground-child@npm:^3.1.0, foreground-child@npm:^3.3.1": +"foreground-child@npm:^3.1.0": version: 3.3.1 resolution: "foreground-child@npm:3.3.1" dependencies: @@ -5755,22 +5708,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^11.0.2": - version: 11.1.0 - resolution: "glob@npm:11.1.0" - dependencies: - foreground-child: "npm:^3.3.1" - jackspeak: "npm:^4.1.1" - minimatch: "npm:^10.1.1" - minipass: "npm:^7.1.2" - package-json-from-dist: "npm:^1.0.0" - path-scurry: "npm:^2.0.0" - bin: - glob: dist/esm/bin.mjs - checksum: 10c0/1ceae07f23e316a6fa74581d9a74be6e8c2e590d2f7205034dd5c0435c53f5f7b712c2be00c3b65bf0a49294a1c6f4b98cd84c7637e29453b5aa13b79f1763a2 - languageName: node - linkType: hard - "glob@npm:^13.0.0": version: 13.0.6 resolution: "glob@npm:13.0.6" @@ -5815,18 +5752,6 @@ __metadata: languageName: node linkType: hard -"gray-matter@npm:^4.0.3": - version: 4.0.3 - resolution: "gray-matter@npm:4.0.3" - dependencies: - js-yaml: "npm:^3.13.1" - kind-of: "npm:^6.0.2" - section-matter: "npm:^1.0.0" - strip-bom-string: "npm:^1.0.0" - checksum: 10c0/e38489906dad4f162ca01e0dcbdbed96d1a53740cef446b9bf76d80bec66fa799af07776a18077aee642346c5e1365ed95e4c91854a12bf40ba0d4fb43a625a6 - languageName: node - linkType: hard - "handlebars@npm:^4.0.11": version: 4.7.9 resolution: "handlebars@npm:4.7.9" @@ -6371,13 +6296,6 @@ __metadata: languageName: node linkType: hard -"is-extendable@npm:^0.1.0": - version: 0.1.1 - resolution: "is-extendable@npm:0.1.1" - checksum: 10c0/dd5ca3994a28e1740d1e25192e66eed128e0b2ff161a7ea348e87ae4f616554b486854de423877a2a2c171d5f7cd6e8093b91f54533bc88a59ee1c9838c43879 - languageName: node - linkType: hard - "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -6707,15 +6625,6 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^4.1.1": - version: 4.2.3 - resolution: "jackspeak@npm:4.2.3" - dependencies: - "@isaacs/cliui": "npm:^9.0.0" - checksum: 10c0/b5c0c414f1607c2aa0597f4bf2c03b8443897fccd5fd3c2b3e4f77d556b2bc7c3d3413828ba91e0789f6fb40ad90242f7f89fb20aee9e9d705bc1681f7564f67 - languageName: node - linkType: hard - "jiti@npm:^2.6.1": version: 2.6.1 resolution: "jiti@npm:2.6.1" @@ -6746,18 +6655,6 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^3.13.1": - version: 3.14.2 - resolution: "js-yaml@npm:3.14.2" - dependencies: - argparse: "npm:^1.0.7" - esprima: "npm:^4.0.0" - bin: - js-yaml: bin/js-yaml.js - checksum: 10c0/3261f25912f5dd76605e5993d0a126c2b6c346311885d3c483706cd722efe34f697ea0331f654ce27c00a42b426e524518ec89d65ed02ea47df8ad26dcc8ce69 - languageName: node - linkType: hard - "jsbn@npm:~0.1.0": version: 0.1.1 resolution: "jsbn@npm:0.1.1" @@ -6901,13 +6798,6 @@ __metadata: languageName: node linkType: hard -"kind-of@npm:^6.0.0, kind-of@npm:^6.0.2": - version: 6.0.3 - resolution: "kind-of@npm:6.0.3" - checksum: 10c0/61cdff9623dabf3568b6445e93e31376bee1cdb93f8ba7033d86022c2a9b1791a1d9510e026e6465ebd701a6dd2f7b0808483ad8838341ac52f003f512e0b4c4 - languageName: node - linkType: hard - "kleur@npm:4.1.5, kleur@npm:^4.1.5": version: 4.1.5 resolution: "kleur@npm:4.1.5" @@ -8229,7 +8119,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.1.1, minimatch@npm:^10.2.2": +"minimatch@npm:^10.2.2": version: 10.2.5 resolution: "minimatch@npm:10.2.5" dependencies: @@ -8769,7 +8659,7 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^2.0.0, path-scurry@npm:^2.0.2": +"path-scurry@npm:^2.0.2": version: 2.0.2 resolution: "path-scurry@npm:2.0.2" dependencies: @@ -9234,42 +9124,6 @@ __metadata: languageName: node linkType: hard -"react-router-mdx@npm:1.0.8": - version: 1.0.8 - resolution: "react-router-mdx@npm:1.0.8" - dependencies: - "@mdx-js/mdx": "npm:^3.1.0" - "@mdx-js/react": "npm:^3.1.0" - glob: "npm:^11.0.2" - gray-matter: "npm:^4.0.3" - remark-frontmatter: "npm:^5.0.0" - slash: "npm:^5.1.0" - peerDependencies: - "@react-router/dev": ^7.6.2 - react: ^19.1.0 - react-router: ^7.0.0 - checksum: 10c0/2cc62d665c7aaab404bcccff7d3e0519f52061744811ca9c9e2d08b9becfc7b13a366c1ca0b803444d9502fc89afb31771b585964e6b50034f9095f44a061c54 - languageName: node - linkType: hard - -"react-router-mdx@patch:react-router-mdx@npm%3A1.0.8#~/.yarn/patches/react-router-mdx-npm-1.0.8-d4402c3003.patch": - version: 1.0.8 - resolution: "react-router-mdx@patch:react-router-mdx@npm%3A1.0.8#~/.yarn/patches/react-router-mdx-npm-1.0.8-d4402c3003.patch::version=1.0.8&hash=4e8a92" - dependencies: - "@mdx-js/mdx": "npm:^3.1.0" - "@mdx-js/react": "npm:^3.1.0" - glob: "npm:^11.0.2" - gray-matter: "npm:^4.0.3" - remark-frontmatter: "npm:^5.0.0" - slash: "npm:^5.1.0" - peerDependencies: - "@react-router/dev": ^7.6.2 - react: ^19.1.0 - react-router: ^7.0.0 - checksum: 10c0/8349a89d1bdfc8433bd93b5236cb9b4a81a5d6fa4884e290643b2f573275486fdf71320336c1a3e624e35158e05090174238b83e9f39e0ca47cc325b8a1abd49 - languageName: node - linkType: hard - "react-router@npm:7.14.0, react-router@npm:^7.14.0": version: 7.14.0 resolution: "react-router@npm:7.14.0" @@ -9678,7 +9532,6 @@ __metadata: react-markdown: "npm:^10.1.0" react-router: "npm:^7.14.0" react-router-dom: "npm:^7.14.0" - react-router-mdx: "patch:react-router-mdx@npm%3A1.0.8#~/.yarn/patches/react-router-mdx-npm-1.0.8-d4402c3003.patch" rehype-slug: "npm:^6.0.0" rehype-stringify: "npm:^10.0.1" remark: "npm:^15.0.1" @@ -10060,16 +9913,6 @@ __metadata: languageName: node linkType: hard -"section-matter@npm:^1.0.0": - version: 1.0.0 - resolution: "section-matter@npm:1.0.0" - dependencies: - extend-shallow: "npm:^2.0.1" - kind-of: "npm:^6.0.0" - checksum: 10c0/8007f91780adc5aaa781a848eaae50b0f680bbf4043b90cf8a96778195b8fab690c87fe7a989e02394ce69890e330811ec8dab22397d384673ce59f7d750641d - languageName: node - linkType: hard - "secure-json-parse@npm:^4.0.0": version: 4.1.0 resolution: "secure-json-parse@npm:4.1.0" @@ -10374,13 +10217,6 @@ __metadata: languageName: node linkType: hard -"slash@npm:^5.1.0": - version: 5.1.0 - resolution: "slash@npm:5.1.0" - checksum: 10c0/eb48b815caf0bdc390d0519d41b9e0556a14380f6799c72ba35caf03544d501d18befdeeef074bc9c052acf69654bc9e0d79d7f1de0866284137a40805299eb3 - languageName: node - linkType: hard - "slice-ansi@npm:^3.0.0": version: 3.0.0 resolution: "slice-ansi@npm:3.0.0" @@ -10509,13 +10345,6 @@ __metadata: languageName: node linkType: hard -"sprintf-js@npm:~1.0.2": - version: 1.0.3 - resolution: "sprintf-js@npm:1.0.3" - checksum: 10c0/ecadcfe4c771890140da5023d43e190b7566d9cf8b2d238600f31bec0fc653f328da4450eb04bd59a431771a8e9cc0e118f0aa3974b683a4981b4e07abc2a5bb - languageName: node - linkType: hard - "sshpk@npm:^1.18.0": version: 1.18.0 resolution: "sshpk@npm:1.18.0" @@ -10736,13 +10565,6 @@ __metadata: languageName: node linkType: hard -"strip-bom-string@npm:^1.0.0": - version: 1.0.0 - resolution: "strip-bom-string@npm:1.0.0" - checksum: 10c0/5c5717e2643225aa6a6d659d34176ab2657037f1fe2423ac6fcdb488f135e14fef1022030e426d8b4d0989e09adbd5c3288d5d3b9c632abeefd2358dfc512bca - languageName: node - linkType: hard - "strip-final-newline@npm:^2.0.0": version: 2.0.0 resolution: "strip-final-newline@npm:2.0.0"