From 94ca39f25df000b5e06961e105990845b2ce586d Mon Sep 17 00:00:00 2001
From: Florian Hammerschmidt
Date: Mon, 12 Oct 2026 11:29:32 +0200
Subject: [PATCH 1/7] refactor: consolidate interop helpers and follow ReScript
rules
Share clipboard writes, Algolia validation, highlighting, Markdown and Cypress bindings. Replace raw JavaScript and unsafe casts with typed interop, migrate Belt calls to Stdlib, and isolate router tests with MemoryRouter.
Add clipboard and MDX regressions, including exact meta element iteration and share feedback reset coverage.
---
.../__tests__/BlogArticleBehavior_.test.res | 4 +-
apps/docs/__tests__/CopyButtons_.test.res | 62 ++++++
.../__tests__/HighlightLanguages_.test.res | 47 ++---
.../NavbarTertiaryBehavior_.test.res | 4 +-
apps/docs/__tests__/Search_.test.res | 65 +++----
.../__tests__/SyntaxLookupBehavior_.test.res | 4 +-
.../__tests__/visual/BlogArticle_.test.res | 24 +--
apps/docs/__tests__/visual/Blog_.test.res | 24 +--
.../__tests__/visual/CodeExample_.test.res | 8 +-
.../visual/CommunityLayout_.test.res | 16 +-
apps/docs/__tests__/visual/Footer_.test.res | 8 +-
.../__tests__/visual/MainLayout_.test.res | 8 +-
.../visual/MarkdownComponents_.test.res | 4 +-
.../__tests__/visual/NavbarPrimary_.test.res | 12 +-
.../__tests__/visual/NavbarTertiary_.test.res | 8 +-
.../__tests__/visual/SidebarNav_.test.res | 12 +-
.../__tests__/visual/SyntaxLookup_.test.res | 24 +--
apps/docs/app/routes/ApiDocs.res | 6 +-
apps/docs/app/routes/Packages.res | 4 +-
apps/docs/e2e/Breadcrumbs.cy.res | 10 +-
apps/docs/e2e/Navigation.cy.res | 68 +++----
apps/docs/e2e/Playground.cy.res | 60 +++---
apps/docs/e2e/bindings/Cy.res | 141 --------------
apps/docs/e2e/bindings/Cypress.res | 24 +++
.../functions/ogimage/[[path]]/index.png.res | 2 -
apps/docs/package.json | 1 -
apps/docs/scripts/__tests__/Interop_.test.res | 82 ++++++++
.../docs/src/bindings/HighlightJsBindings.res | 17 --
apps/docs/src/common/AlgoliaConfig.res | 24 +--
apps/docs/src/common/Hooks.res | 16 --
apps/docs/src/common/Util.res | 19 +-
apps/docs/src/common/Util.resi | 4 -
apps/docs/src/components/CodeCopyButton.res | 78 ++++++++
apps/docs/src/components/CodeExample.res | 100 +---------
apps/docs/src/components/Markdown.res | 178 +++++-------------
apps/docs/src/components/Markdown.resi | 6 +-
.../src/components/MarkdownComponents.res | 12 +-
apps/docs/src/components/SearchBox.res | 19 +-
.../src/data/LandingPagePlaygroundLoader.res | 10 +-
apps/docs/src/data/MetaTagsApi.res | 10 +-
apps/docs/src/markdown/MdxLegacy.res | 89 +++------
apps/guide/__tests__/GuideHome_.test.res | 27 +--
apps/guide/__tests__/GuideTestFixtures.res | 17 +-
apps/guide/app/GuideHome.res | 2 +-
apps/guide/app/GuideMarkdown.res | 2 -
apps/guide/package.json | 1 -
packages/playground/src/EvalIFrame.res | 8 +-
packages/playground/src/Playground.res | 102 ++--------
.../playground/src/PlaygroundShareButton.res | 42 +++++
packages/shared/package.json | 1 +
packages/shared/src/Ansi.res | 9 +-
.../shared/src}/Clipboard.res | 0
packages/shared/src/HighlightJs.res | 15 +-
packages/shared/src/HighlightLanguages.res | 8 +
packages/shared/src/HighlightLanguages.resi | 9 +
packages/shared/src/HtmlRef.res | 15 ++
.../shared/src}/ReactMarkdown.res | 4 +-
packages/shared/src/ReactRouter.res | 5 -
.../shared/src/WarningFlagDescription.res | 12 +-
yarn.lock | 3 +-
60 files changed, 679 insertions(+), 917 deletions(-)
create mode 100644 apps/docs/__tests__/CopyButtons_.test.res
delete mode 100644 apps/docs/e2e/bindings/Cy.res
create mode 100644 apps/docs/scripts/__tests__/Interop_.test.res
create mode 100644 apps/docs/src/components/CodeCopyButton.res
delete mode 100644 apps/guide/app/GuideMarkdown.res
create mode 100644 packages/playground/src/PlaygroundShareButton.res
rename {apps/docs/src/common => packages/shared/src}/Clipboard.res (100%)
create mode 100644 packages/shared/src/HtmlRef.res
rename {apps/docs/src/bindings => packages/shared/src}/ReactMarkdown.res (56%)
diff --git a/apps/docs/__tests__/BlogArticleBehavior_.test.res b/apps/docs/__tests__/BlogArticleBehavior_.test.res
index 6b0f0d317..05857a4dd 100644
--- a/apps/docs/__tests__/BlogArticleBehavior_.test.res
+++ b/apps/docs/__tests__/BlogArticleBehavior_.test.res
@@ -26,11 +26,11 @@ test("blog article marks body content for DocSearch crawling", async () => {
await viewport(1440, 900)
let _screen = await render(
-
+
{React.string("This is the blog post body content for testing.")}
- ,
+ ,
)
switch document->WebAPI.Document.querySelector("article.DocSearch-content") {
diff --git a/apps/docs/__tests__/CopyButtons_.test.res b/apps/docs/__tests__/CopyButtons_.test.res
new file mode 100644
index 000000000..82a2886e2
--- /dev/null
+++ b/apps/docs/__tests__/CopyButtons_.test.res
@@ -0,0 +1,62 @@
+open Vitest
+
+test(
+ "code copy waits for the write, copies exact code, and permits retry after failure",
+ async () => {
+ let copied = ref(None)
+ let complete = ref(_ => ())
+ let writeClipboard = code => {
+ copied := Some(code)
+ Promise.make((resolve, _) => complete := resolve)
+ }
+ let screen = await render( )
+ let button = await screen->getByLabelText("Copy code")
+ await button->click
+ await element(button)->toBeDisabled
+ expect(copied.contents)->toEqual(Some("let answer = 42"))
+ complete.contents(Error(Clipboard.WriteFailed))
+ let retry = await screen->getByLabelText("Copy failed, try again")
+ await element(retry)->notToBeDisabled
+ await retry->click
+ complete.contents(Ok())
+ let feedback = await screen->getByText("Copied to clipboard")
+ await element(feedback)->toBeVisible
+ await element(button)->toBeDisabled
+ await screen->unmount
+ await element(feedback)->notToBeInTheDocument
+ },
+)
+
+test(
+ "share copy writes the full URL and shows success only after the write completes",
+ async () => {
+ let copied = ref(None)
+ let complete = ref(_ => ())
+ let writeClipboard = url => {
+ copied := Some(url)
+ Promise.make((resolve, _) => complete := resolve)
+ }
+ let screen = await render( )
+ let button = await screen->getByText("Copy Share Link")
+ await button->click
+ await element(button)->toBeDisabled
+ expect(copied.contents)->toEqual(Some(window.location.href))
+ complete.contents(Error(Clipboard.WriteFailed))
+ let retry = await screen->getByText("Copy failed, try again")
+ await element(retry)->notToBeDisabled
+ await retry->click
+ complete.contents(Ok())
+ await element(await screen->getByText("Copied to clipboard!"))->toBeVisible
+ },
+)
+
+test("a previous share write cannot change feedback for a new share link", async () => {
+ let complete = ref(_ => ())
+ let writeClipboard = _ => Promise.make((resolve, _) => complete := resolve)
+ let screen = await render( )
+ await (await screen->getByText("Copy Share Link"))->click
+ await screen->rerender( )
+ complete.contents(Ok())
+ await element(await screen->getByText("Copy Share Link"))->notToBeDisabled
+ await element(await screen->getByText("Copied to clipboard!"))->notToBeInTheDocument
+})
diff --git a/apps/docs/__tests__/HighlightLanguages_.test.res b/apps/docs/__tests__/HighlightLanguages_.test.res
index 6b1179bde..634b5212b 100644
--- a/apps/docs/__tests__/HighlightLanguages_.test.res
+++ b/apps/docs/__tests__/HighlightLanguages_.test.res
@@ -1,50 +1,29 @@
open Vitest
-type registeredLanguage
-type highlightOptions = {language: string}
-type highlightResult = {value: string}
-
-@module("highlight.js/lib/core") @scope("default")
-external createHighlighter: unit => HighlightLanguages.highlighter = "newInstance"
-
-@module("highlight.js/lib/languages/javascript")
-external javascript: HighlightLanguages.definition = "default"
-
-@module("highlight.js/lib/languages/plaintext")
-external plaintext: HighlightLanguages.definition = "default"
-
-@send
-external getLanguage: (HighlightLanguages.highlighter, string) => option =
- "getLanguage"
-
-@send
-external highlight: (HighlightLanguages.highlighter, string, highlightOptions) => highlightResult =
- "highlight"
-
test("registering a missing language makes its grammar and aliases available", async () => {
- let highlighter = createHighlighter()
+ let highlighter = HighlightLanguages.make(HighlightLanguages.defaultInstance)
- expect(highlighter->getLanguage("javascript"))->toEqual(None)
- expect(highlighter->getLanguage("js"))->toEqual(None)
- highlighter->HighlightLanguages.ensureRegistered("javascript", javascript)
+ expect(highlighter->HighlightLanguages.getLanguage("javascript"))->toEqual(None)
+ expect(highlighter->HighlightLanguages.getLanguage("js"))->toEqual(None)
+ highlighter->HighlightLanguages.ensureRegistered("javascript", HighlightJsBindings.javascript)
- let result = highlighter->highlight("const answer = 42;", {language: "js"})
+ let result = highlighter->HighlightLanguages.highlight("const answer = 42;", {language: "js"})
expect(result.value)->toBe(
"const answer = 42 ;",
)
})
test("registering an existing name or alias does not replace its grammar", async () => {
- let highlighter = createHighlighter()
- highlighter->HighlightLanguages.ensureRegistered("javascript", javascript)
- let original = highlighter->getLanguage("javascript")
+ let highlighter = HighlightLanguages.make(HighlightLanguages.defaultInstance)
+ highlighter->HighlightLanguages.ensureRegistered("javascript", HighlightJsBindings.javascript)
+ let original = highlighter->HighlightLanguages.getLanguage("javascript")
- highlighter->HighlightLanguages.ensureRegistered("javascript", plaintext)
- highlighter->HighlightLanguages.ensureRegistered("js", plaintext)
+ highlighter->HighlightLanguages.ensureRegistered("javascript", HighlightJsBindings.text)
+ highlighter->HighlightLanguages.ensureRegistered("js", HighlightJsBindings.text)
- expect(highlighter->getLanguage("javascript"))->toBe(original)
- expect(highlighter->getLanguage("js"))->toBe(original)
- let result = highlighter->highlight("const answer = 42;", {language: "js"})
+ expect(highlighter->HighlightLanguages.getLanguage("javascript"))->toBe(original)
+ expect(highlighter->HighlightLanguages.getLanguage("js"))->toBe(original)
+ let result = highlighter->HighlightLanguages.highlight("const answer = 42;", {language: "js"})
expect(result.value)->toBe(
"const answer = 42 ;",
)
diff --git a/apps/docs/__tests__/NavbarTertiaryBehavior_.test.res b/apps/docs/__tests__/NavbarTertiaryBehavior_.test.res
index 8e860e1cc..0e777fb82 100644
--- a/apps/docs/__tests__/NavbarTertiaryBehavior_.test.res
+++ b/apps/docs/__tests__/NavbarTertiaryBehavior_.test.res
@@ -19,12 +19,12 @@ test("mobile drawer can be toggled open", async () => {
await viewport(600, 1200)
let screen = await render(
-
+
breadcrumbs
editLink
- ,
+ ,
)
let sidebar = await screen->getByTestId("sidebar-categories")
diff --git a/apps/docs/__tests__/Search_.test.res b/apps/docs/__tests__/Search_.test.res
index c27590892..503e4de4c 100644
--- a/apps/docs/__tests__/Search_.test.res
+++ b/apps/docs/__tests__/Search_.test.res
@@ -27,6 +27,25 @@ let makeHit = (~type_: DocSearch.contentType, ~url: string): DocSearch.docSearch
_snippetResult: {content: Nullable.null},
}
+let makeCrawlerHit = (): DocSearch.docSearchHit => {
+ objectID: "crawler-hit",
+ content: Nullable.make("map(array, fn) returns a new array."),
+ url: "https://rescript-lang.org/docs/manual/api/stdlib/array/#value-map",
+ url_without_anchor: Nullable.undefined,
+ type_: Content,
+ anchor: Nullable.undefined,
+ deprecated: None,
+ hierarchy: {
+ lvl0: Nullable.make("Array"),
+ lvl1: Nullable.make("map"),
+ lvl2: Nullable.null,
+ lvl3: Nullable.null,
+ lvl4: Nullable.null,
+ lvl5: Nullable.null,
+ lvl6: Nullable.null,
+ },
+}
+
let withHierarchy = (
hit: DocSearch.docSearchHit,
~lvl0: string,
@@ -268,28 +287,7 @@ test("hitComponent routes relative hit URLs through React Router", async () => {
test(
"getContentHtml falls back to plain content when crawler hit has no snippet result",
async () => {
- let hit: DocSearch.docSearchHit = Obj.magic(
- Dict.fromArray([
- ("objectID", "crawler-hit"),
- ("content", "map(array, fn) returns a new array."),
- ("url", "https://rescript-lang.org/docs/manual/api/stdlib/array/#value-map"),
- ("type", "content"),
- (
- "hierarchy",
- Obj.magic(
- Dict.fromArray([
- ("lvl0", Obj.magic("Array")),
- ("lvl1", Obj.magic("map")),
- ("lvl2", Obj.magic(Nullable.null)),
- ("lvl3", Obj.magic(Nullable.null)),
- ("lvl4", Obj.magic(Nullable.null)),
- ("lvl5", Obj.magic(Nullable.null)),
- ("lvl6", Obj.magic(Nullable.null)),
- ]),
- ),
- ),
- ]),
- )
+ let hit: DocSearch.docSearchHit = makeCrawlerHit()
expect(SearchResults.getContentHtml(hit))->toEqual(Some("map(array, fn) returns a new array."))
},
@@ -480,28 +478,7 @@ test("normalizeHitUrls rewrites absolute site URLs to relative paths", async ()
})
test("normalizeHitUrls tolerates crawler hits without url_without_anchor", async () => {
- let hit: DocSearch.docSearchHit = Obj.magic(
- Dict.fromArray([
- ("objectID", "crawler-hit"),
- ("content", "map(array, fn) returns a new array."),
- ("url", "https://rescript-lang.org/docs/manual/api/stdlib/array/#value-map"),
- ("type", "content"),
- (
- "hierarchy",
- Obj.magic(
- Dict.fromArray([
- ("lvl0", Obj.magic("Array")),
- ("lvl1", Obj.magic("map")),
- ("lvl2", Obj.magic(Nullable.null)),
- ("lvl3", Obj.magic(Nullable.null)),
- ("lvl4", Obj.magic(Nullable.null)),
- ("lvl5", Obj.magic(Nullable.null)),
- ("lvl6", Obj.magic(Nullable.null)),
- ]),
- ),
- ),
- ]),
- )
+ let hit: DocSearch.docSearchHit = makeCrawlerHit()
let result = SearchResults.normalizeHitUrls([hit], ~siteUrl="https://rescript-lang.org/")
diff --git a/apps/docs/__tests__/SyntaxLookupBehavior_.test.res b/apps/docs/__tests__/SyntaxLookupBehavior_.test.res
index 94b8fbe10..06b8473ad 100644
--- a/apps/docs/__tests__/SyntaxLookupBehavior_.test.res
+++ b/apps/docs/__tests__/SyntaxLookupBehavior_.test.res
@@ -46,11 +46,11 @@ test("syntax lookup detail marks active content for DocSearch crawling", async (
await viewport(1440, 900)
let _screen = await render(
-
+
Array.getUnsafe(0)}>
{React.string("Detail content for @as decorator.")}
- ,
+ ,
)
switch document->WebAPI.Document.querySelector(".DocSearch-content h1") {
diff --git a/apps/docs/__tests__/visual/BlogArticle_.test.res b/apps/docs/__tests__/visual/BlogArticle_.test.res
index c3f6083a4..3e642fa13 100644
--- a/apps/docs/__tests__/visual/BlogArticle_.test.res
+++ b/apps/docs/__tests__/visual/BlogArticle_.test.res
@@ -24,13 +24,13 @@ test("desktop blog article renders header, author, date, and body", async () =>
await viewport(1440, 900)
let screen = await render(
-
+
{React.string("This is the blog post body content for testing.")}
- ,
+ ,
)
let title = await screen->getByText("Test Blog Post Title")
@@ -50,13 +50,13 @@ test("desktop archived blog article shows warning banner", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
{React.string("This is the blog post body content for testing.")}
- ,
+ ,
)
let important = await screen->getByText("Important:")
@@ -70,13 +70,13 @@ test("mobile blog article", async () => {
await viewport(600, 1200)
let screen = await render(
-
+
{React.string("This is the blog post body content for testing.")}
- ,
+ ,
)
let title = await screen->getByText("Test Blog Post Title")
@@ -115,7 +115,7 @@ test("desktop blog article with co-authors shows all authors", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
{
{React.string("Collaborative content.")}
- ,
+ ,
)
let mainAuthor = await screen->getByText("Test Author")
@@ -151,7 +151,7 @@ test("desktop blog article with article image shows image", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
{
{React.string("Content below the article image.")}
- ,
+ ,
)
let title = await screen->getByText("Blog Post With Article Image")
@@ -185,13 +185,13 @@ test("desktop blog article without description", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
{React.string("Content without a description above.")}
- ,
+ ,
)
let title = await screen->getByText("Blog Post Without Description")
diff --git a/apps/docs/__tests__/visual/Blog_.test.res b/apps/docs/__tests__/visual/Blog_.test.res
index 66c62d518..ba5675155 100644
--- a/apps/docs/__tests__/visual/Blog_.test.res
+++ b/apps/docs/__tests__/visual/Blog_.test.res
@@ -58,11 +58,11 @@ test("desktop blog index shows featured post and cards", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
- ,
+ ,
)
let featured = await screen->getByText("Announcing ReScript 12")
@@ -82,11 +82,11 @@ test("desktop blog shows category selector", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
- ,
+ ,
)
let allTab = await screen->getByText("All")
@@ -103,11 +103,11 @@ test("mobile blog index", async () => {
await viewport(600, 1200)
let screen = await render(
-
+
- ,
+ ,
)
let featured = await screen->getByText("Announcing ReScript 12")
@@ -121,11 +121,11 @@ test("blog shows empty state when no posts", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
- ,
+ ,
)
let emptyMsg = await screen->getByText("Blog not yet available")
@@ -170,11 +170,11 @@ test("desktop blog shows archived posts", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
- ,
+ ,
)
let post1 = await screen->getByText("An Old Archived Post")
@@ -208,11 +208,11 @@ test("desktop blog with single post", async () => {
]
let screen = await render(
-
+
- ,
+ ,
)
let post = await screen->getByText("The Only Post")
diff --git a/apps/docs/__tests__/visual/CodeExample_.test.res b/apps/docs/__tests__/visual/CodeExample_.test.res
index 2497fb025..6ddd0e527 100644
--- a/apps/docs/__tests__/visual/CodeExample_.test.res
+++ b/apps/docs/__tests__/visual/CodeExample_.test.res
@@ -62,11 +62,11 @@ test("renders toggle with multiple tabs", async () => {
]
let screen = await render(
-
+
- ,
+ ,
)
let resTab = await screen->getByText("ReScript")
@@ -98,11 +98,11 @@ test("toggle switches between tabs on click", async () => {
]
let screen = await render(
-
+
- ,
+ ,
)
// Click the JavaScript tab
diff --git a/apps/docs/__tests__/visual/CommunityLayout_.test.res b/apps/docs/__tests__/visual/CommunityLayout_.test.res
index dbbe7cb7f..1a288854b 100644
--- a/apps/docs/__tests__/visual/CommunityLayout_.test.res
+++ b/apps/docs/__tests__/visual/CommunityLayout_.test.res
@@ -21,13 +21,13 @@ test("desktop community layout shows sidebar and content", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
{React.string("Community content goes here.")}
- ,
+ ,
)
let resources = await screen->getByText("Resources")
@@ -47,13 +47,13 @@ test("mobile community layout hides sidebar", async () => {
await viewport(600, 1200)
let screen = await render(
-
+
{React.string("Community content goes here.")}
- ,
+ ,
)
let resources = await screen->getByText("Resources")
@@ -87,13 +87,13 @@ test("desktop community layout with multiple categories", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
{React.string("Community content with multiple categories.")}
- ,
+ ,
)
let resources = await screen->getByText("Resources")
@@ -113,13 +113,13 @@ test("tablet community layout", async () => {
await viewport(900, 900)
let screen = await render(
-
+
{React.string("Community content goes here.")}
- ,
+ ,
)
let resources = await screen->getByText("Resources")
diff --git a/apps/docs/__tests__/visual/Footer_.test.res b/apps/docs/__tests__/visual/Footer_.test.res
index 9d20af2ea..f50fe9ffe 100644
--- a/apps/docs/__tests__/visual/Footer_.test.res
+++ b/apps/docs/__tests__/visual/Footer_.test.res
@@ -5,11 +5,11 @@ test("desktop footer shows all sections and links", async () => {
await viewport(1440, 500)
let screen = await render(
-
+
- ,
+ ,
)
let community = await screen->getByText("Community")
@@ -35,11 +35,11 @@ test("mobile footer stacks sections vertically", async () => {
await viewport(600, 800)
let screen = await render(
-
+
- ,
+ ,
)
let community = await screen->getByText("Community")
diff --git a/apps/docs/__tests__/visual/MainLayout_.test.res b/apps/docs/__tests__/visual/MainLayout_.test.res
index 670d180c0..2b8154cff 100644
--- a/apps/docs/__tests__/visual/MainLayout_.test.res
+++ b/apps/docs/__tests__/visual/MainLayout_.test.res
@@ -5,13 +5,13 @@ test("desktop main layout renders children and footer", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
{React.string("Main page content goes here.")}
- ,
+ ,
)
let content = await screen->getByTestId("main-content")
@@ -28,13 +28,13 @@ test("mobile main layout", async () => {
await viewport(600, 1200)
let screen = await render(
-
+
{React.string("Main page content goes here.")}
- ,
+ ,
)
let content = await screen->getByTestId("main-content")
diff --git a/apps/docs/__tests__/visual/MarkdownComponents_.test.res b/apps/docs/__tests__/visual/MarkdownComponents_.test.res
index 7976d557f..eee8e75d4 100644
--- a/apps/docs/__tests__/visual/MarkdownComponents_.test.res
+++ b/apps/docs/__tests__/visual/MarkdownComponents_.test.res
@@ -171,7 +171,7 @@ test("renders links", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
@@ -184,7 +184,7 @@ test("renders links", async () => {
- ,
+ ,
)
let externalLink = await screen->getByText("External link")
diff --git a/apps/docs/__tests__/visual/NavbarPrimary_.test.res b/apps/docs/__tests__/visual/NavbarPrimary_.test.res
index 23c7b50e3..5d8f2025f 100644
--- a/apps/docs/__tests__/visual/NavbarPrimary_.test.res
+++ b/apps/docs/__tests__/visual/NavbarPrimary_.test.res
@@ -5,9 +5,9 @@ test("desktop has everything visible", async () => {
await viewport(1440, 500)
let screen = await render(
-
+
- ,
+ ,
)
let leftContent = await screen->getByTestId("navbar-primary-left-content")
@@ -33,9 +33,9 @@ test("tablet has everything visible", async () => {
await viewport(900, 500)
let screen = await render(
-
+
- ,
+ ,
)
let leftContent = await screen->getByTestId("navbar-primary-left-content")
@@ -61,9 +61,9 @@ test("phone has some things hidden and a mobile nav that can be toggled", async
await viewport(600, 1200)
let screen = await render(
-
+
- ,
+ ,
)
let leftContent = await screen->getByTestId("navbar-primary-left-content")
diff --git a/apps/docs/__tests__/visual/NavbarTertiary_.test.res b/apps/docs/__tests__/visual/NavbarTertiary_.test.res
index 015c6d11e..ba8daf6df 100644
--- a/apps/docs/__tests__/visual/NavbarTertiary_.test.res
+++ b/apps/docs/__tests__/visual/NavbarTertiary_.test.res
@@ -19,12 +19,12 @@ test("desktop shows breadcrumbs and edit link", async () => {
await viewport(1440, 500)
let screen = await render(
-
+
breadcrumbs
editLink
- ,
+ ,
)
let navbar = await screen->getByTestId("navbar-tertiary")
@@ -44,12 +44,12 @@ test("mobile shows breadcrumbs and drawer button", async () => {
await viewport(600, 1200)
let screen = await render(
-
+
breadcrumbs
editLink
- ,
+ ,
)
let navbar = await screen->getByTestId("navbar-tertiary")
diff --git a/apps/docs/__tests__/visual/SidebarNav_.test.res b/apps/docs/__tests__/visual/SidebarNav_.test.res
index 94303a657..70c5906f0 100644
--- a/apps/docs/__tests__/visual/SidebarNav_.test.res
+++ b/apps/docs/__tests__/visual/SidebarNav_.test.res
@@ -14,11 +14,11 @@ test("sidebar category renders title and nav items", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
()} />
- ,
+ ,
)
let introduction = await screen->getByText("Introduction")
@@ -38,7 +38,7 @@ test("sidebar category highlights active item", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
{
onClick={_ => ()}
/>
- ,
+ ,
)
let introduction = await screen->getByText("Introduction")
@@ -117,11 +117,11 @@ test("sidebar category with many items", async () => {
}
let screen = await render(
-
+
()} />
- ,
+ ,
)
let stringItem = await screen->getByText("String")
diff --git a/apps/docs/__tests__/visual/SyntaxLookup_.test.res b/apps/docs/__tests__/visual/SyntaxLookup_.test.res
index 38b3bb864..2192b9fad 100644
--- a/apps/docs/__tests__/visual/SyntaxLookup_.test.res
+++ b/apps/docs/__tests__/visual/SyntaxLookup_.test.res
@@ -44,11 +44,11 @@ test("desktop syntax lookup renders categories and tags", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
- ,
+ ,
)
let decoratorsHeading = await screen->getByText("Decorators")
@@ -71,13 +71,13 @@ test("desktop syntax lookup with active item shows detail box", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
Array.getUnsafe(0)}>
{React.string("Detail content for @as decorator.")}
- ,
+ ,
)
let detail = await screen->getByText("Detail content for @as decorator.")
@@ -91,11 +91,11 @@ test("mobile syntax lookup", async () => {
await viewport(600, 1200)
let screen = await render(
-
+
- ,
+ ,
)
let decoratorsHeading = await screen->getByText("Decorators")
@@ -118,11 +118,11 @@ test("deprecated items show with line-through styling", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
- ,
+ ,
)
let deprecatedTag = await screen->getByText("|>")
@@ -136,13 +136,13 @@ test("syntax lookup detail box shows summary", async () => {
await viewport(1440, 900)
let screen = await render(
-
+
Array.getUnsafe(2)}>
{React.string("Detailed documentation about the pipe operator.")}
- ,
+ ,
)
let detail = await screen->getByText("Detailed documentation about the pipe operator.")
@@ -156,13 +156,13 @@ test("mobile syntax lookup with active item", async () => {
await viewport(600, 1200)
let screen = await render(
-
+
Array.getUnsafe(0)}>
{React.string("Detail content for @as decorator.")}
- ,
+ ,
)
let detail = await screen->getByText("Detail content for @as decorator.")
diff --git a/apps/docs/app/routes/ApiDocs.res b/apps/docs/app/routes/ApiDocs.res
index 3d5df459b..b12265d5f 100644
--- a/apps/docs/app/routes/ApiDocs.res
+++ b/apps/docs/app/routes/ApiDocs.res
@@ -221,7 +221,7 @@ module MarkdownStylize = {
let make = (~content, ~rehypePlugins) => {
let components = {
...MarkdownComponents.default,
- h2: MarkdownComponents.default.h3->Obj.magic,
+ h2: props => props.children ,
}
content
}
@@ -367,7 +367,7 @@ module Data = {
}
let processStaticProps = (~slug: array) => {
- let moduleName = slug->Belt.Array.getExn(0)
+ let moduleName = slug[0]->Option.getOrThrow
let modulePath = slug->Array.join("/")
let content =
@@ -456,7 +456,7 @@ let processStaticProps = (~slug: array) => {
items: sortedItems,
}
- Ok({module_, toctree: Obj.magic({name: "root", path: [], children: []})})
+ Ok({module_, toctree: {name: "root", path: [], children: []}})
| None => Error(`Failed to get API Data for module ${moduleName}`)
}
diff --git a/apps/docs/app/routes/Packages.res b/apps/docs/app/routes/Packages.res
index c8d2e416c..319501055 100644
--- a/apps/docs/app/routes/Packages.res
+++ b/apps/docs/app/routes/Packages.res
@@ -112,7 +112,7 @@ module Resource = {
let filteredNpm = applyNpmSearch(allNpms, pattern)->Array.map(m => Npm(m["item"]))
let filteredOutdated = applyNpmSearch(allOutDated, pattern)->Array.map(m => Outdated(m["item"]))
- Belt.Array.concatMany([filteredNpm, filteredOutdated])
+ Array.concat(filteredNpm, filteredOutdated)
}
}
@@ -326,7 +326,7 @@ let make = (props: props) => {
let allResources = {
let npms = props.packages->Array.map(pkg => Resource.Npm(pkg))
let outdated = props.unmaintained->Array.map(pkg => Resource.Outdated(pkg))
- Belt.Array.concatMany([npms, outdated])
+ Array.concat(npms, outdated)
}
let resources = switch state {
diff --git a/apps/docs/e2e/Breadcrumbs.cy.res b/apps/docs/e2e/Breadcrumbs.cy.res
index 25d6ad419..ba30797bc 100644
--- a/apps/docs/e2e/Breadcrumbs.cy.res
+++ b/apps/docs/e2e/Breadcrumbs.cy.res
@@ -1,15 +1,15 @@
-open Cy
+open Cypress
let waitForPage = () => {
- getByTestId("navbar-primary")->shouldBeVisible->ignore
- getByTestId("navbar-tertiary")->shouldBeVisible->ignore
- cyWindow()->its("document.readyState")->shouldWithValue("eq", "complete")->ignore
+ getByTestId("navbar-primary")->should("be.visible")->ignore
+ getByTestId("navbar-tertiary")->should("be.visible")->ignore
+ cyWindow()->propertyString("document.readyState")->shouldEqual("complete")->ignore
}
let expectBreadcrumbsText = text => {
getByTestId("breadcrumbs")
->should("be.visible")
- ->shouldWithValue("have.text", text)
+ ->shouldText(text)
->ignore
}
diff --git a/apps/docs/e2e/Navigation.cy.res b/apps/docs/e2e/Navigation.cy.res
index e046d26a3..c123961f9 100644
--- a/apps/docs/e2e/Navigation.cy.res
+++ b/apps/docs/e2e/Navigation.cy.res
@@ -1,12 +1,12 @@
-open Cy
+open Cypress
// Wait for the app to fully hydrate before interacting with links.
// The production build is pre-rendered so React must attach event
// handlers before client-side navigation works.
let waitForHydration = () => {
- getByTestId("navbar-primary")->shouldBeVisible->ignore
- cyWindow()->its("document.readyState")->shouldWithValue("eq", "complete")->ignore
- wait(2000)
+ getByTestId("navbar-primary")->should("be.visible")->ignore
+ cyWindow()->propertyString("document.readyState")->shouldEqual("complete")->ignore
+ pause(2000)
}
// Use short, re-queryable selectors to avoid detached DOM issues.
@@ -17,7 +17,7 @@ let waitForHydration = () => {
let clickNavLink = (~testId, ~text) => {
getByTestId(testId)
->should("be.visible")
- ->containsSelectorChainable("a", text)
+ ->containsChildIn("a", text)
->scrollIntoView
->should("be.visible")
->click
@@ -27,7 +27,7 @@ let clickNavLink = (~testId, ~text) => {
let clickMobileNavLink = text => {
getByTestId("mobile-nav")
->should("be.visible")
- ->containsSelectorChainable("a", text)
+ ->containsChildIn("a", text)
->scrollIntoView
->should("be.visible")
->click
@@ -53,8 +53,8 @@ describe("Desktop Navigation", () => {
"should navigate to Docs via navbar link",
() => {
clickNavLink(~testId="navbar-primary-left-content", ~text="Docs")
- url()->shouldInclude("/docs/manual/introduction")->ignore
- get("h1")->shouldBeVisible->ignore
+ currentUrl()->shouldInclude("/docs/manual/introduction")->ignore
+ get("h1")->should("be.visible")->ignore
},
)
@@ -62,7 +62,7 @@ describe("Desktop Navigation", () => {
"should navigate to Playground via navbar link",
() => {
clickNavLink(~testId="navbar-primary-left-content", ~text="Playground")
- url()->shouldInclude("/try")->ignore
+ currentUrl()->shouldInclude("/try")->ignore
},
)
@@ -70,7 +70,7 @@ describe("Desktop Navigation", () => {
"should navigate to Blog via navbar link",
() => {
clickNavLink(~testId="navbar-primary-left-content", ~text="Blog")
- url()->shouldInclude("/blog")->ignore
+ currentUrl()->shouldInclude("/blog")->ignore
},
)
@@ -78,8 +78,8 @@ describe("Desktop Navigation", () => {
"should navigate to Community via navbar link",
() => {
clickNavLink(~testId="navbar-primary-left-content", ~text="Community")
- url()->shouldInclude("/community")->ignore
- get("h1")->shouldBeVisible->ignore
+ currentUrl()->shouldInclude("/community")->ignore
+ get("h1")->should("be.visible")->ignore
},
)
@@ -87,10 +87,10 @@ describe("Desktop Navigation", () => {
"should navigate home via logo after clicking away",
() => {
clickNavLink(~testId="navbar-primary-left-content", ~text="Blog")
- url()->shouldInclude("/blog")->ignore
+ currentUrl()->shouldInclude("/blog")->ignore
get("a[aria-label='homepage']")->should("be.visible")->first->click->ignore
- cyLocation("pathname")->shouldWithValue("eq", "/")->ignore
+ cyLocation("pathname")->shouldEqual("/")->ignore
},
)
})
@@ -101,28 +101,28 @@ describe("Desktop Navigation", () => {
() => {
// Click Docs in primary nav to reveal the secondary nav
clickNavLink(~testId="navbar-primary-left-content", ~text="Docs")
- url()->shouldInclude("/docs/manual/introduction")->ignore
+ currentUrl()->shouldInclude("/docs/manual/introduction")->ignore
// Language Manual
clickNavLink(~testId="navbar-secondary", ~text="Language Manual")
- url()->shouldInclude("/docs/manual/introduction")->ignore
+ currentUrl()->shouldInclude("/docs/manual/introduction")->ignore
// API
clickNavLink(~testId="navbar-secondary", ~text="API")
- url()->shouldInclude("/docs/manual/api")->ignore
+ currentUrl()->shouldInclude("/docs/manual/api")->ignore
// Guides
clickNavLink(~testId="navbar-secondary", ~text="Guides")
- url()->shouldInclude("/docs/guides/overview")->ignore
+ currentUrl()->shouldInclude("/docs/guides/overview")->ignore
get("h1")->shouldContainText("Guides")->ignore
// Syntax Lookup
clickNavLink(~testId="navbar-secondary", ~text="Syntax Lookup")
- url()->shouldInclude("/syntax-lookup")->ignore
+ currentUrl()->shouldInclude("/syntax-lookup")->ignore
// React
clickNavLink(~testId="navbar-secondary", ~text="React")
- url()->shouldInclude("/docs/react/introduction")->ignore
+ currentUrl()->shouldInclude("/docs/react/introduction")->ignore
},
)
})
@@ -142,8 +142,8 @@ describe("Mobile Navigation", () => {
"should navigate to Docs via navbar link",
() => {
clickNavLink(~testId="navbar-primary-left-content", ~text="Docs")
- url()->shouldInclude("/docs/manual/introduction")->ignore
- get("h1")->shouldBeVisible->ignore
+ currentUrl()->shouldInclude("/docs/manual/introduction")->ignore
+ get("h1")->should("be.visible")->ignore
},
)
@@ -151,10 +151,10 @@ describe("Mobile Navigation", () => {
"should navigate home via logo after clicking away",
() => {
clickNavLink(~testId="navbar-primary-left-content", ~text="Docs")
- url()->shouldInclude("/docs/manual/introduction")->ignore
+ currentUrl()->shouldInclude("/docs/manual/introduction")->ignore
get("a[aria-label='homepage']")->should("be.visible")->first->click->ignore
- cyLocation("pathname")->shouldWithValue("eq", "/")->ignore
+ cyLocation("pathname")->shouldEqual("/")->ignore
},
)
})
@@ -165,7 +165,7 @@ describe("Mobile Navigation", () => {
() => {
openMobileMenu()
clickMobileNavLink("Playground")
- url()->shouldInclude("/try")->ignore
+ currentUrl()->shouldInclude("/try")->ignore
},
)
@@ -174,7 +174,7 @@ describe("Mobile Navigation", () => {
() => {
openMobileMenu()
clickMobileNavLink("Blog")
- url()->shouldInclude("/blog")->ignore
+ currentUrl()->shouldInclude("/blog")->ignore
},
)
@@ -183,8 +183,8 @@ describe("Mobile Navigation", () => {
() => {
openMobileMenu()
clickMobileNavLink("Community")
- url()->shouldInclude("/community")->ignore
- get("h1")->shouldBeVisible->ignore
+ currentUrl()->shouldInclude("/community")->ignore
+ get("h1")->should("be.visible")->ignore
},
)
})
@@ -195,35 +195,35 @@ describe("Mobile Navigation", () => {
() => {
// Click Docs in primary nav to reveal the secondary nav
clickNavLink(~testId="navbar-primary-left-content", ~text="Docs")
- url()->shouldInclude("/docs/manual/introduction")->ignore
+ currentUrl()->shouldInclude("/docs/manual/introduction")->ignore
// Scroll to top so the secondary nav is visible
cyScrollTo("top")
// Language Manual
clickNavLink(~testId="navbar-secondary", ~text="Language Manual")
- url()->shouldInclude("/docs/manual/introduction")->ignore
+ currentUrl()->shouldInclude("/docs/manual/introduction")->ignore
// API
cyScrollTo("top")
clickNavLink(~testId="navbar-secondary", ~text="API")
- url()->shouldInclude("/docs/manual/api")->ignore
+ currentUrl()->shouldInclude("/docs/manual/api")->ignore
// Guides
cyScrollTo("top")
clickNavLink(~testId="navbar-secondary", ~text="Guides")
- url()->shouldInclude("/docs/guides/overview")->ignore
+ currentUrl()->shouldInclude("/docs/guides/overview")->ignore
get("h1")->shouldContainText("Guides")->ignore
// Syntax Lookup
cyScrollTo("top")
clickNavLink(~testId="navbar-secondary", ~text="Syntax Lookup")
- url()->shouldInclude("/syntax-lookup")->ignore
+ currentUrl()->shouldInclude("/syntax-lookup")->ignore
// React
cyScrollTo("top")
clickNavLink(~testId="navbar-secondary", ~text="React")
- url()->shouldInclude("/docs/react/introduction")->ignore
+ currentUrl()->shouldInclude("/docs/react/introduction")->ignore
},
)
})
diff --git a/apps/docs/e2e/Playground.cy.res b/apps/docs/e2e/Playground.cy.res
index 8e8bf97c6..165510c1e 100644
--- a/apps/docs/e2e/Playground.cy.res
+++ b/apps/docs/e2e/Playground.cy.res
@@ -1,20 +1,20 @@
-open Cy
+open Cypress
let waitForHydration = () => {
- getByTestId("navbar-primary")->shouldBeVisible->ignore
- cyWindow()->its("document.readyState")->shouldWithValue("eq", "complete")->ignore
- wait(2000)
+ getByTestId("navbar-primary")->should("be.visible")->ignore
+ cyWindow()->propertyString("document.readyState")->shouldEqual("complete")->ignore
+ pause(2000)
}
let waitForPlayground = () => {
get(".cm-editor")->should("be.visible")->ignore
getByTestId("control-panel")->should("be.visible")->ignore
- wait(2000)
+ pause(2000)
}
let clickNavLink = (~testId, ~text) => {
get(`[data-testid="${testId}"] a:visible`)
- ->containsChainable(text)
+ ->containsChild(text)
->click
->ignore
}
@@ -29,7 +29,7 @@ let compileAndRunImportedCode = () => {
)
->ignore
- wait(3000)
+ pause(3000)
contains("JavaScript")->click->ignore
get("pre.whitespace-pre-wrap")
@@ -39,7 +39,7 @@ let compileAndRunImportedCode = () => {
getByTestId("control-panel")
->find("button")
- ->containsChainable("Run")
+ ->containsChild("Run")
->click
->ignore
@@ -66,9 +66,9 @@ describe("Playground", () => {
it("should highlight JavaScript after a direct playground load", () => {
visit("/try")
- get(".cm-editor")->shouldBeVisible->ignore
- get("main")->shouldWithKeyValue("have.css", "background-color", "rgb(11, 13, 34)")->ignore
- get(".cm-editor")->shouldWithKeyValue("have.css", "background-color", "rgb(11, 13, 34)")->ignore
+ get(".cm-editor")->should("be.visible")->ignore
+ get("main")->shouldCss("background-color", "rgb(11, 13, 34)")->ignore
+ get(".cm-editor")->shouldCss("background-color", "rgb(11, 13, 34)")->ignore
get("pre.whitespace-pre-wrap")->shouldContainText("react/jsx-runtime")->ignore
get("pre code.lang-js span[class^='hljs-']")->should("exist")->ignore
})
@@ -76,7 +76,7 @@ describe("Playground", () => {
it("should compile and run imported code in the playground", () => {
// Navigate to the playground from the homepage
clickNavLink(~testId="navbar-primary-left-content", ~text="Playground")
- url()->shouldInclude("/try")->ignore
+ currentUrl()->shouldInclude("/try")->ignore
compileAndRunImportedCode()
})
@@ -91,7 +91,7 @@ describe("Playground", () => {
contains("Settings")->click->ignore
get(`select[name="compilerVersions"]`)
->select(version)
- ->shouldWithValue("have.value", version)
+ ->shouldValue(version)
->ignore
compileAndRunImportedCode()
@@ -116,7 +116,7 @@ Console.log(isRetriable(HttpError({status: 500})))`
get("pre.whitespace-pre-wrap")->shouldContainText("Primitive_exceptions.mjs")->ignore
getByTestId("control-panel")
->find("button")
- ->containsChainable("Run")
+ ->containsChild("Run")
->click
->ignore
@@ -124,13 +124,13 @@ Console.log(isRetriable(HttpError({status: 500})))`
})
it("should open the landing page example in the playground with code and compiled output", () => {
- containsSelector("a", "Edit this example in Playground")
+ containsIn("a", "Edit this example in Playground")
->scrollIntoView
->should("be.visible")
->click
->ignore
- url()->shouldInclude("/try?code=")->ignore
+ currentUrl()->shouldInclude("/try?code=")->ignore
waitForPlayground()
get(".cm-content")
@@ -148,45 +148,43 @@ Console.log(isRetriable(HttpError({status: 500})))`
it("should switch to light mode from toast and back to dark mode in settings", () => {
// Navigate to playground and wait for initial render
clickNavLink(~testId="navbar-primary-left-content", ~text="Playground")
- url()->shouldInclude("/try")->ignore
+ currentUrl()->shouldInclude("/try")->ignore
waitForPlayground()
// Switch to light mode through the onboarding toast
getByTestId("playground-lightmode-toast")
->should("be.visible")
->find("button")
- ->containsChainable("Try it now")
+ ->containsChild("Try it now")
->click
->ignore
// Verify playground shell is in light mode
- get("main")->shouldWithValue("have.class", "playground-theme-light")->ignore
- get("main")->shouldWithKeyValue("have.css", "background-color", "rgb(250, 251, 252)")->ignore
+ get("main")->shouldClass("playground-theme-light")->ignore
+ get("main")->shouldCss("background-color", "rgb(250, 251, 252)")->ignore
get(".cm-editor")
- ->shouldWithKeyValue("have.css", "background-color", "rgb(255, 255, 255)")
+ ->shouldCss("background-color", "rgb(255, 255, 255)")
->ignore
cyWindow()
- ->its("localStorage")
- ->invokeWithArg("getItem", "playgroundTheme")
- ->shouldWithValue("eq", "light")
+ ->thenMap(window => window.localStorage->WebAPI.Storage.getItem("playgroundTheme"))
+ ->shouldEqual(Null.make("light"))
->ignore
// Switch back to dark mode from Settings
contains("Settings")->click->ignore
get("main")
->find("button")
- ->containsChainable("Dark")
+ ->containsChild("Dark")
->click
->ignore
// Verify playground shell is back to dark mode
- get("main")->shouldWithValue("have.class", "playground-theme-dark")->ignore
- get("main")->shouldWithKeyValue("have.css", "background-color", "rgb(11, 13, 34)")->ignore
- get(".cm-editor")->shouldWithKeyValue("have.css", "background-color", "rgb(11, 13, 34)")->ignore
+ get("main")->shouldClass("playground-theme-dark")->ignore
+ get("main")->shouldCss("background-color", "rgb(11, 13, 34)")->ignore
+ get(".cm-editor")->shouldCss("background-color", "rgb(11, 13, 34)")->ignore
cyWindow()
- ->its("localStorage")
- ->invokeWithArg("getItem", "playgroundTheme")
- ->shouldWithValue("eq", "dark")
+ ->thenMap(window => window.localStorage->WebAPI.Storage.getItem("playgroundTheme"))
+ ->shouldEqual(Null.make("dark"))
->ignore
})
})
diff --git a/apps/docs/e2e/bindings/Cy.res b/apps/docs/e2e/bindings/Cy.res
deleted file mode 100644
index d34ef610d..000000000
--- a/apps/docs/e2e/bindings/Cy.res
+++ /dev/null
@@ -1,141 +0,0 @@
-// -- Chainable type -----------------------------------------------------------
-
-type t
-
-// -- Cypress global commands --------------------------------------------------
-
-@val @scope("cy")
-external visit: string => unit = "visit"
-
-@val @scope("cy")
-external visitWithOptions: (string, {..}) => unit = "visit"
-
-@val @scope("cy")
-external get: string => t = "get"
-
-@val @scope("cy")
-external contains: string => t = "contains"
-
-@val @scope("cy")
-external containsSelector: (string, string) => t = "contains"
-
-@val @scope("cy")
-external title: unit => t = "title"
-
-@val @scope("cy")
-external url: unit => t = "url"
-
-@val @scope("cy")
-external cyLocation: string => t = "location"
-
-@val @scope("cy")
-external wait: int => unit = "wait"
-
-@val @scope("cy")
-external reload: unit => unit = "reload"
-
-@val @scope("cy")
-external go: string => unit = "go"
-
-@val @scope("cy")
-external viewport: (int, int) => unit = "viewport"
-
-@val @scope("cy")
-external cyScrollTo: string => unit = "scrollTo"
-
-@val @scope("cy")
-external log: string => unit = "log"
-
-// -- Chainable commands -------------------------------------------------------
-
-// Contains (chainable versions)
-@send external containsChainable: (t, string) => t = "contains"
-@send external containsSelectorChainable: (t, string, string) => t = "contains"
-
-// Queries
-@send external find: (t, string) => t = "find"
-@send external first: t => t = "first"
-@send external last: t => t = "last"
-@send external eq: (t, int) => t = "eq"
-@send external children: t => t = "children"
-@send external parent: t => t = "parent"
-@send external parents: (t, string) => t = "parents"
-@send external closest: (t, string) => t = "closest"
-@send external next: t => t = "next"
-@send external prev: t => t = "prev"
-@send external siblings: t => t = "siblings"
-@send external filter: (t, string) => t = "filter"
-@send external not_: (t, string) => t = "not"
-
-// Actions
-@send external click: t => t = "click"
-@send external clickWithOptions: (t, {..}) => t = "click"
-@send external dblclick: t => t = "dblclick"
-@send external type_: (t, string) => t = "type"
-@send external typeWithOptions: (t, string, {..}) => t = "type"
-@send external clear: t => t = "clear"
-@send external check: t => t = "check"
-@send external uncheck: t => t = "uncheck"
-@send external select: (t, string) => t = "select"
-@send external trigger: (t, string) => t = "trigger"
-@send external scrollIntoView: t => t = "scrollIntoView"
-@send external focus: t => t = "focus"
-@send external blur: t => t = "blur"
-
-// Assertions
-@send external should: (t, string) => t = "should"
-@send external shouldWithValue: (t, string, string) => t = "should"
-@send external shouldWithKeyValue: (t, string, string, string) => t = "should"
-@send external and_: (t, string) => t = "and"
-@send external andWithValue: (t, string, string) => t = "and"
-
-// Traversal
-@send external each: (t, t => unit) => t = "each"
-@send external then: (t, t => unit) => t = "then"
-@send external its: (t, string) => t = "its"
-
-// Attributes
-@send external invoke: (t, string) => t = "invoke"
-@send external invokeWithArg: (t, string, string) => t = "invoke"
-@send external attr: (t, string) => t = "attr"
-
-// Visibility & state
-@send external asChainable: (t, string) => t = "as"
-
-// Yielding
-@send external within: (t, unit => unit) => t = "within"
-
-// -- Describe / It (Mocha globals) --------------------------------------------
-
-@val external describe: (string, unit => unit) => unit = "describe"
-@val external it: (string, unit => unit) => unit = "it"
-@val external before: (unit => unit) => unit = "before"
-@val external beforeEach: (unit => unit) => unit = "beforeEach"
-@val external after: (unit => unit) => unit = "after"
-@val external afterEach: (unit => unit) => unit = "afterEach"
-@val external context: (string, unit => unit) => unit = "context"
-
-// -- Window -------------------------------------------------------------------
-
-@val @scope("cy")
-external cyWindow: unit => t = "window"
-
-// -- Convenience helpers ------------------------------------------------------
-
-let getByTestId = testId => get(`[data-testid="${testId}"]`)
-
-let getByRole = role => get(`[role="${role}"]`)
-
-let getByHref = href => get(`a[href="${href}"]`)
-
-let shouldBeVisible = chain => chain->should("be.visible")
-
-let shouldBeVisibleAndClick = chain => chain->should("be.visible")->click
-
-let shouldExist = chain => chain->should("exist")
-
-let shouldContainText = (chain, text) => chain->shouldWithValue("contain.text", text)
-
-let shouldHaveAttr = (chain, attr, value) => chain->shouldWithKeyValue("have.attr", attr, value)
-
-let shouldInclude = (chain, value) => chain->shouldWithValue("include", value)
diff --git a/apps/docs/e2e/bindings/Cypress.res b/apps/docs/e2e/bindings/Cypress.res
index be8c3c607..e9c164ff8 100644
--- a/apps/docs/e2e/bindings/Cypress.res
+++ b/apps/docs/e2e/bindings/Cypress.res
@@ -12,6 +12,7 @@ type rec window = {
console: console,
navigator: {clipboard: clipboard},
scrollY: float,
+ localStorage: WebAPI.WebStorageAPI.storage,
}
and clipboard
type response = {status: int, body: string, headers: Dict.t}
@@ -214,3 +215,26 @@ external addEventListenerOnce: (Dom.document, string, unit => unit, listenerOpti
@get external fontWeight: cssStyle => string = "fontWeight"
@get external fontStyle: cssStyle => string = "fontStyle"
@send external propertyValue: (cssStyle, string) => string = "getPropertyValue"
+
+@val external describe: (string, unit => unit) => unit = "describe"
+@val @scope("cy") external currentUrl: unit => chain = "url"
+@val @scope("cy") external pause: int => unit = "wait"
+@val @scope("cy") external cyScrollTo: string => unit = "scrollTo"
+@send external find: (chain, string) => chain = "find"
+@send external containsChild: (chain, string) => chain = "contains"
+@send external containsChildIn: (chain, string, string) => chain = "contains"
+@send external select: (chain, string) => chain = "select"
+@send
+external typeWithOptions: (
+ chain,
+ string,
+ {"force": bool, "delay": int},
+) => chain = "type"
+@send
+external shouldContainText: (chain, @as("contain.text") _, string) => chain =
+ "should"
+@send external shouldInclude: (chain, @as("include") _, string) => chain = "should"
+@send
+external shouldClass: (chain, @as("have.class") _, string) => chain = "should"
+
+let getByTestId = testId => get(`[data-testid="${testId}"]`)
diff --git a/apps/docs/functions/ogimage/[[path]]/index.png.res b/apps/docs/functions/ogimage/[[path]]/index.png.res
index e3526a3f3..7b5d6f480 100644
--- a/apps/docs/functions/ogimage/[[path]]/index.png.res
+++ b/apps/docs/functions/ogimage/[[path]]/index.png.res
@@ -1,7 +1,5 @@
open WebAPI
-%%raw("import React from 'react'")
-
let loadGoogleFont = async (family: string) => {
let url = `https://fonts.googleapis.com/css2?family=${family}`
let css = await (await fetch(url))->Response.text
diff --git a/apps/docs/package.json b/apps/docs/package.json
index e7350a978..4345a9bc8 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -63,7 +63,6 @@
"mdast-util-toc": "^7.1.0",
"react": "^19.3.0",
"react-dom": "^19.3.0",
- "react-markdown": "^10.1.0",
"react-router": "^8.3.1",
"rehype-slug": "^6.0.0",
"rehype-stringify": "^10.0.1",
diff --git a/apps/docs/scripts/__tests__/Interop_.test.res b/apps/docs/scripts/__tests__/Interop_.test.res
new file mode 100644
index 000000000..269b21954
--- /dev/null
+++ b/apps/docs/scripts/__tests__/Interop_.test.res
@@ -0,0 +1,82 @@
+open ScriptTest
+
+@module("react-dom/server") external renderHtml: React.element => string = "renderToStaticMarkup"
+
+test("MDX list items preserve paragraph and nested list structure", async () => {
+ let html = renderHtml(
+
+ {React.string("Parent")}
+
+ {React.string("Child")}
+
+ ,
+ )
+ expect(html)->toContain("Parent
String.includes("
toBe(false)
+ expect(renderHtml( {React.null} ))->toContain("")
+})
+
+test(
+ "MDX code tabs read code, language, labels and line metadata from React children",
+ async () => {
+ HighlightLanguages.defaultInstance->ContentHighlighting.register
+ let html = renderHtml(
+
+
+
+
+ {React.string("let a = 1\nlet b = 2")}
+
+
+
+
+ {React.string("const a = 1;")}
+
+
+
+ ,
+ )
+ expect(html)->toContain("Source")
+ expect(html)->toContain("Output")
+ expect(html)->toContain("lang-rescript")
+ expect(html)->toContain("opacity-50")
+ expect(html)->toContain("let")
+ expect(html->String.replaceAllRegExp(/<[^>]*>/g, ""))->toContain("let b = 2")
+ },
+)
+
+test("URL boxes preserve their first paragraph, following content and destination", async () => {
+ let html = renderHtml(
+
+
+ {React.string("First paragraph")}
+ {React.string("Second paragraph")}
+
+ ,
+ )
+ expect(html)->toContain("First paragraph")
+ expect(html)->toContain("Second paragraph")
+ expect(html)->toContain("Read more")
+ expect(html)->toContain("href=\"/docs/manual/introduction\"")
+ expect(html->String.includes("
toBe(false)
+})
+
+test("meta tag extraction visits exactly the available HTML elements", async () => {
+ let html = ` `
+ let result = await MetaTagsApi.extractMetaTags(`data:text/html,${encodeURIComponent(html)}`)
+ expect(result)->toStrictEqual(
+ Some({
+ MetaTagsApi.title: Some("Title"),
+ description: Some("Description"),
+ image: Some("/image.png"),
+ }),
+ )
+ let empty = await MetaTagsApi.extractMetaTags("data:text/html,
No meta ")
+ expect(empty)->toStrictEqual(Some({MetaTagsApi.title: None, description: None, image: None}))
+})
+
+test("typed string helpers preserve empty strings and repeated hyphen behavior", async () => {
+ expect(Util.String.camelCase("hello--world-42-next"))->toBe("hello-World-42Next")
+ expect(Util.String.capitalize(""))->toBe("")
+ expect(Util.String.capitalize("hello"))->toBe("Hello")
+})
diff --git a/apps/docs/src/bindings/HighlightJsBindings.res b/apps/docs/src/bindings/HighlightJsBindings.res
index f793d247f..3943cb613 100644
--- a/apps/docs/src/bindings/HighlightJsBindings.res
+++ b/apps/docs/src/bindings/HighlightJsBindings.res
@@ -1,9 +1,3 @@
-type highlightOptions = {language: string}
-type highlightResult = {value: string}
-
-@module("highlight.js/lib/core") @scope("default")
-external make: unit => HighlightLanguages.highlighter = "newInstance"
-
@module("highlight.js/lib/languages/bash")
external bash: HighlightLanguages.definition = "default"
@@ -36,14 +30,3 @@ external toml: HighlightLanguages.definition = "default"
@module("highlightjs-rescript")
external rescript: HighlightLanguages.definition = "default"
-
-@send
-external registerLanguage: (
- HighlightLanguages.highlighter,
- string,
- HighlightLanguages.definition,
-) => unit = "registerLanguage"
-
-@send
-external highlight: (HighlightLanguages.highlighter, string, highlightOptions) => highlightResult =
- "highlight"
diff --git a/apps/docs/src/common/AlgoliaConfig.res b/apps/docs/src/common/AlgoliaConfig.res
index e486ee425..f557262e4 100644
--- a/apps/docs/src/common/AlgoliaConfig.res
+++ b/apps/docs/src/common/AlgoliaConfig.res
@@ -4,24 +4,12 @@ type publicConfig = {
searchApiKey: string,
}
-let isPresent = value =>
- switch value {
- | Some(v) => v !== ""
- | None => false
- }
-
-let missingPublicVars = (~appId, ~indexName, ~searchApiKey): array => {
- let missing = []
- if !isPresent(appId) {
- missing->Array.push("VITE_ALGOLIA_APP_ID")
- }
- if !isPresent(indexName) {
- missing->Array.push("VITE_ALGOLIA_INDEX_NAME")
- }
- if !isPresent(searchApiKey) {
- missing->Array.push("VITE_ALGOLIA_SEARCH_API_KEY")
- }
- missing
+let missingPublicVars = (~appId, ~indexName, ~searchApiKey) => {
+ let env = Dict.make()
+ appId->Option.forEach(value => env->Dict.set("VITE_ALGOLIA_APP_ID", value))
+ indexName->Option.forEach(value => env->Dict.set("VITE_ALGOLIA_INDEX_NAME", value))
+ searchApiKey->Option.forEach(value => env->Dict.set("VITE_ALGOLIA_SEARCH_API_KEY", value))
+ AlgoliaEnvStatus.getMissingPublicAlgoliaVars(~env)
}
let publicConfigFrom = (~appId, ~indexName, ~searchApiKey): option =>
diff --git a/apps/docs/src/common/Hooks.res b/apps/docs/src/common/Hooks.res
index 682355c21..99b79381f 100644
--- a/apps/docs/src/common/Hooks.res
+++ b/apps/docs/src/common/Hooks.res
@@ -1,19 +1,3 @@
-/* Contains some generic hooks */
-let useOutsideClick: (ReactDOM.Ref.t, unit => unit) => unit = %raw(`(outerRef, trigger) => {
- function handleClickOutside(event) {
- if (outerRef.current && !outerRef.current.contains(event.target)) {
- trigger();
- }
- }
-
- React.useEffect(() => {
- document.addEventListener("mousedown", handleClickOutside);
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- };
- });
-}`)
-
/** scrollDir is not memo-friendly.
It must be used with pattern matching.
Do not pass it directly to child components. */
diff --git a/apps/docs/src/common/Util.res b/apps/docs/src/common/Util.res
index 27e8d354e..2922d8aa8 100644
--- a/apps/docs/src/common/Util.res
+++ b/apps/docs/src/common/Util.res
@@ -1,11 +1,11 @@
-module Unsafe = {
- external elementAsString: React.element => string = "%identity"
-}
-
module String = {
- let camelCase: string => string = %raw("str => {
- return str.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
- }")
+ let camelCase = str =>
+ str->String.replaceRegExpBy1Unsafe(/-([a-z])/g, (
+ ~match as _,
+ ~group1,
+ ~offset as _,
+ ~input as _,
+ ) => group1->String.toUpperCase)
let kebabCase = string =>
string
@@ -13,9 +13,8 @@ module String = {
->String.toLowerCase
->String.replaceAll(" ", "-")
- let capitalize: string => string = %raw("str => {
- return str && str.charAt(0).toUpperCase() + str.substring(1);
- }")
+ let capitalize = str =>
+ str->String.slice(~start=0, ~end=1)->String.toUpperCase ++ str->String.slice(~start=1)
let capitalizeSentence = str =>
str
diff --git a/apps/docs/src/common/Util.resi b/apps/docs/src/common/Util.resi
index fb0da2991..6ed3f78f6 100644
--- a/apps/docs/src/common/Util.resi
+++ b/apps/docs/src/common/Util.resi
@@ -6,10 +6,6 @@ module Url: {
let makeOpenGraphImageUrl: string => string
}
-module Unsafe: {
- let elementAsString: React.element => string
-}
-
module String: {
let camelCase: string => string
let kebabCase: string => string
diff --git a/apps/docs/src/components/CodeCopyButton.res b/apps/docs/src/components/CodeCopyButton.res
new file mode 100644
index 000000000..ed56f0187
--- /dev/null
+++ b/apps/docs/src/components/CodeCopyButton.res
@@ -0,0 +1,78 @@
+type state =
+ | Init
+ | Pending
+ | Copied
+ | Failed
+
+@react.component
+let make = (~code, ~writeClipboard=Clipboard.writeText) => {
+ let (state, setState) = React.useState(_ => Init)
+
+ let buttonRef = React.useRef(Nullable.null)
+
+ let copy = async () => {
+ setState(_ => Pending)
+ let result = await writeClipboard(code)
+ setState(_ =>
+ switch result {
+ | Ok() => Copied
+ | Error(_) => Failed
+ }
+ )
+ }
+ let onClick = evt => {
+ ReactEvent.Mouse.preventDefault(evt)
+ copy()->ignore
+ }
+
+ React.useEffect(() => {
+ switch state {
+ | Copied =>
+ let buttonEl = Nullable.toOption(buttonRef.current)->Option.getOrThrow
+
+ // Note on this imperative DOM nonsense:
+ // For Tailwind transitions to behave correctly, we need to first paint the DOM element in the tree,
+ // and in the next tick, add the opacity-100 class, so the transition animation actually takes place.
+ // If we don't do that, the banner will essentially pop up without any animation
+ let bannerEl = WebAPI.Document.createElement(document, "div")
+ bannerEl.className = "opacity-0 absolute -top-6 right-0 -mt-5 -mr-4 px-4 py-2 w-40 rounded-lg captions text-white bg-gray-100 text-gray-80-tr transition-all duration-1000 ease-in-out "
+
+ let textNode = WebAPI.Document.createTextNode(document, "Copied to clipboard")
+
+ WebAPI.Element.appendChild(bannerEl, textNode)->ignore
+ WebAPI.Element.appendChild(buttonEl, bannerEl)->ignore
+
+ let nextFrameId = WebAPI.Window.requestAnimationFrame(window, _ => {
+ WebAPI.DOMTokenList.toggle(bannerEl.classList, ~token="opacity-0")->ignore
+ WebAPI.DOMTokenList.toggle(bannerEl.classList, ~token="opacity-100")->ignore
+ })
+
+ let timeoutId = setTimeout(~handler=() => {
+ buttonEl->WebAPI.Element.removeChild(bannerEl)->ignore
+ setState(_ => Init)
+ }, ~timeout=3000)
+
+ Some(
+ () => {
+ cancelAnimationFrame(nextFrameId)
+ clearTimeout(timeoutId)
+ bannerEl->WebAPI.Element.remove
+ },
+ )
+ | _ => None
+ }
+ }, [state])
+ //Copy-Button
+
+
+
+}
diff --git a/apps/docs/src/components/CodeExample.res b/apps/docs/src/components/CodeExample.res
index 3736d8da0..2c7eca370 100644
--- a/apps/docs/src/components/CodeExample.res
+++ b/apps/docs/src/components/CodeExample.res
@@ -9,102 +9,6 @@ let langShortname = (lang: string) =>
| rest => rest
}
-module CopyButton = {
- let copyToClipboard: string => bool = %raw(`
- function(str) {
- try {
- const el = document.createElement('textarea');
- el.value = str;
- el.setAttribute('readonly', '');
- el.style.position = 'absolute';
- el.style.left = '-9999px';
- document.body.appendChild(el);
- const selected =
- document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
- el.select();
- document.execCommand('copy');
- document.body.removeChild(el);
- if (selected) {
- document.getSelection().removeAllRanges();
- document.getSelection().addRange(selected);
- }
- return true;
- } catch(e) {
- return false;
- }
- }
- `)
-
- type state =
- | Init
- | Copied
- | Failed
-
- @react.component
- let make = (~code) => {
- let (state, setState) = React.useState(_ => Init)
-
- let buttonRef = React.useRef(Nullable.null)
-
- let onClick = evt => {
- ReactEvent.Mouse.preventDefault(evt)
- if copyToClipboard(code) {
- setState(_ => Copied)
- } else {
- setState(_ => Failed)
- }
- }
-
- React.useEffect(() => {
- switch state {
- | Copied =>
- let buttonEl = Nullable.toOption(buttonRef.current)->Option.getOrThrow
-
- // Note on this imperative DOM nonsense:
- // For Tailwind transitions to behave correctly, we need to first paint the DOM element in the tree,
- // and in the next tick, add the opacity-100 class, so the transition animation actually takes place.
- // If we don't do that, the banner will essentially pop up without any animation
- let bannerEl = WebAPI.Document.createElement(document, "div")
- bannerEl.className = "opacity-0 absolute -top-6 right-0 -mt-5 -mr-4 px-4 py-2 w-40 rounded-lg captions text-white bg-gray-100 text-gray-80-tr transition-all duration-1000 ease-in-out "
-
- let textNode = WebAPI.Document.createTextNode(document, "Copied to clipboard")
-
- WebAPI.Element.appendChild(bannerEl, textNode)->ignore
- WebAPI.Element.appendChild(buttonEl, bannerEl)->ignore
-
- let nextFrameId = WebAPI.Window.requestAnimationFrame(window, _ => {
- WebAPI.DOMTokenList.toggle(bannerEl.classList, ~token="opacity-0")->ignore
- WebAPI.DOMTokenList.toggle(bannerEl.classList, ~token="opacity-100")->ignore
- })
-
- let timeoutId = setTimeout(~handler=() => {
- buttonEl->WebAPI.Element.removeChild(bannerEl)->ignore
- setState(_ => Init)
- }, ~timeout=3000)
-
- Some(
- () => {
- cancelAnimationFrame(nextFrameId)
- clearTimeout(timeoutId)
- },
- )
- | _ => None
- }
- }, [state])
- //Copy-Button
- >))}
- disabled={state === Copied}
- className="relative"
- onClick
- >
-
-
- }
-}
-
@react.component
let make = (
~highlightedLines=[],
@@ -139,7 +43,7 @@ let make = (
let copyButton = if showCopyButton {
-
+
} else {
React.null
@@ -259,7 +163,7 @@ module Toggle = {
: React.null
- let copyButton =
+ let copyButton =
playgroundLinkButton
diff --git a/apps/docs/src/components/Markdown.res b/apps/docs/src/components/Markdown.res
index 8ad1a1436..9840ee22d 100644
--- a/apps/docs/src/components/Markdown.res
+++ b/apps/docs/src/components/Markdown.res
@@ -40,48 +40,29 @@ module Warn = {
}
module UrlBox = {
- open! MdxLegacy.MdxChildren
-
let imgEl =
@react.component
- let make = (~text: string, ~href: string, ~children: MdxLegacy.MdxChildren.t) => {
- let content = switch classify(children) {
- | String(str) =>
-
- imgEl
- {React.string(str)}
-
- | Element(el) =>
- let subChildren = el->getMdxChildren
+ let make = (~text: string, ~href: string, ~children: React.element) => {
+ let elements = React.Children.toArray(children)
+ let content = if typeof(children) == #string || React.isValidElement(children) {
imgEl
- {subChildren->toReactElement}
+ {MdxLegacy.getChildren(children)}
- | Array(arr) =>
- // Scenario: Take the first element, rewrap its children with the hyperlink img
- let length = Array.length(arr)
- if length >= 1 {
- let head = Belt.Array.getExn(arr, 0)
- let headChildren = head->getMdxChildren
-
+ } else {
+ switch elements[0] {
+ | None => React.null
+ | Some(head) =>
+ let rest = elements->Array.slice(~start=1)
<>
imgEl
- {headChildren->toReactElement}
+ {MdxLegacy.getChildren(head)}
- {if length > 1 {
- arr->Array.slice(~start=1, ~end=length)->MdxLegacy.arrToReactElement
- } else {
- React.null
- }}
+ {React.array(rest)}
>
- } else {
- React.null
}
- | Unknown(el) =>
- Console.log2("Received unknown", el)
- React.null
}
let link = if Util.Url.isAbsolute(href) {
@@ -232,10 +213,7 @@ module Code = {
@module("../ffi/parse-numeric-range.js")
external parseNumericRange: string => array
= "parsePart"
- // TODO: Might be refactorable with the new @unboxed feature
- type unknown = MdxLegacy.Components.unknown
-
- external unknownAsString: unknown => string = "%identity"
+ type unknown = React.element
let parseNumericRangeMeta = (metastring: string) =>
String.split(metastring, " ")
@@ -279,7 +257,7 @@ module Code = {
}
}
- let code = children->unknownAsString
+ let code = String.make(children)
let isMultiline = code->String.includes("\n")
switch lang {
@@ -290,35 +268,25 @@ module Code = {
}
module CodeTab = {
- let getMdxMetastring: MdxLegacy.mdxComponent => option = %raw("element => {
- if(element == null || element.props == null) {
- return;
- }
- return element.props.metastring;
- }")
@react.component
- let make = (~children: MdxLegacy.MdxChildren.t, ~labels: array=[]) => {
- let mdxElements = switch MdxLegacy.MdxChildren.classify(children) {
- | Array(mdxElements) => mdxElements
- | Element(el) => [el]
- | _ => []
- }
+ let make = (~children: React.element, ~labels: array=[]) => {
+ let mdxElements = React.Children.toArray(children)
let tabs = Array.reduceWithIndex(mdxElements, [], (acc, mdxElement, i) => {
- let child = mdxElement->MdxLegacy.MdxChildren.getMdxChildren->MdxLegacy.MdxChildren.classify
+ let children = mdxElement->MdxLegacy.getChildren->React.Children.toArray
- switch child {
- | Element(codeEl) =>
- let className = MdxLegacy.getMdxClassName(codeEl)->Option.getOr("")
+ switch children {
+ | [codeEl] if React.isValidElement(codeEl) =>
+ let className = MdxLegacy.getClassName(codeEl)->Option.getOr("")
- let metastring = getMdxMetastring(codeEl)->Option.getOr("")
+ let metastring = MdxLegacy.getMetastring(codeEl)->Option.getOr("")
let lang = switch String.split(className, "-") {
| ["language", lang] => Some(lang)
| _ => None
}
- let code = String.make(MdxLegacy.MdxChildren.getMdxChildren(codeEl))
+ let code = String.make(MdxLegacy.getChildren(codeEl))
let label = labels[i]
let tab = {
CodeExample.Toggle.lang,
@@ -408,73 +376,43 @@ module Ol = {
}
module Li = {
- let typeOf: 'a => string = %raw("thing => { return typeof thing; }")
- let isArray: 'a => bool = %raw("thing => { return thing instanceof Array; }")
- external asArray: 'a => array = "%identity"
+ let getType = element =>
+ if MdxLegacy.isComponent(element, P.make) {
+ "p"
+ } else if MdxLegacy.isComponent(element, Pre.make) {
+ "pre"
+ } else if MdxLegacy.isComponent(element, Ul.make) {
+ "ul"
+ } else if MdxLegacy.isComponent(element, Ol.make) {
+ "ol"
+ } else {
+ MdxLegacy.getType(element)
+ }
@react.component
let make = (~children) => {
- /*
- There are 3 value scenarios for `children`
-
- 1) string (if bullet point is standalone text)
- 2) array(,
|) (if nested list)
- 3) array(,,...,) (if text with nested content)
- 4) array(, , string,...) (if nested content without wrapping )
-
- We are iterating on these here with quite some bailout JS
- */
-
- let elements: React.element = if isArray(children) {
- let arr = children->asArray
- let last: React.element = {
- arr->Belt.Array.getExn(arr->Array.length - 1)
- }
-
- let head = Array.slice(arr, ~start=0, ~end=arr->Array.length - 1)
-
- let first = Belt.Array.getExn(head, 0)
-
- switch {
- open MdxLegacy
- last->fromReactElement->getMdxType
- } {
- | "ul"
- | "li"
- | "pre" =>
- switch {
- open MdxLegacy
- first->fromReactElement->getMdxType
- } {
- | "p" =>
- <>
- {React.array(head)}
- last
- >
- | _ =>
- <>
-
{React.array(head)}
- last
- >
- }
- | _ => children
- /* Scenario 3 */
+ let elements = React.Children.toArray(children)
+ let content = switch elements {
+ | [] => React.null
+ | [child] =>
+ switch getType(child) {
+ | "pre" | "p" => child
+ | _ => child
}
- } else if typeOf(children) === "string" {
- {children->Util.Unsafe.elementAsString->React.string}
- } else {
- switch {
- /* Unknown Scenario */
- open MdxLegacy
- children->fromReactElement->getMdxType
- } {
- | "pre" => children
- | "p" => children
+ | _ =>
+ let first = elements[0]->Option.getOrThrow
+ let last = elements[elements->Array.length - 1]->Option.getOrThrow
+ let head = elements->Array.slice(~start=0, ~end=elements->Array.length - 1)
+ switch getType(last) {
+ | "ul" | "ol" | "li" | "pre" =>
+ <>
+ {getType(first) == "p" ? React.array(head) : {React.array(head)}
}
+ last
+ >
| _ => children
}
}
-
- elements
+ content
}
}
@@ -543,17 +481,3 @@ module Video = {
}
}
-
-// Useful for debugging injected values in props
-// let mdxTestComponent: React.component<{.}> = %raw(`
-// function(children) {
-// console.log(children);
-// return React.createElement("div");
-// }
-// `)
-
-// Used for the MdxJS Provider
-
-/* Sets our preferred branded styles
- We most likely will never need a different ~components
- option on our website. */
diff --git a/apps/docs/src/components/Markdown.resi b/apps/docs/src/components/Markdown.resi
index 4c42ad8ff..51ac8b922 100644
--- a/apps/docs/src/components/Markdown.resi
+++ b/apps/docs/src/components/Markdown.resi
@@ -27,7 +27,7 @@ module Warn: {
module UrlBox: {
@react.component
- let make: (~text: string, ~href: string, ~children: MdxLegacy.MdxChildren.t) => React.element
+ let make: (~text: string, ~href: string, ~children: React.element) => React.element
}
// Used for creating invisible, hoverable anchors for url linking
@@ -93,13 +93,13 @@ module Code: {
let make: (
~className: string=?,
~metastring: option,
- ~children: MdxLegacy.Components.unknown,
+ ~children: React.element,
) => React.element
}
module CodeTab: {
@react.component
- let make: (~children: MdxLegacy.MdxChildren.t, ~labels: array=?) => React.element
+ let make: (~children: React.element, ~labels: array=?) => React.element
}
module Blockquote: {
diff --git a/apps/docs/src/components/MarkdownComponents.res b/apps/docs/src/components/MarkdownComponents.res
index 145d118c7..ebd829c92 100644
--- a/apps/docs/src/components/MarkdownComponents.res
+++ b/apps/docs/src/components/MarkdownComponents.res
@@ -18,12 +18,9 @@ type t = {
@as("Video")
video?: React.componentLike, React.element>,
@as("UrlBox")
- urlBox?: React.componentLike<
- UrlBox.props,
- React.element,
- >,
+ urlBox?: React.componentLike, React.element>,
@as("CodeTab")
- codeTab?: CodeTab.props> => React.element,
+ codeTab?: CodeTab.props> => React.element,
/* Common markdown elements */
p?: P.props => React.element,
li?: Li.props => React.element,
@@ -41,10 +38,7 @@ type t = {
blockquote?: Blockquote.props => React.element,
strong?: Strong.props => React.element,
hr?: Hr.props => React.element,
- code?: React.componentLike<
- Code.props, MdxLegacy.Components.unknown>,
- React.element,
- >,
+ code?: React.componentLike, React.element>, React.element>,
pre?: Pre.props => React.element,
a?: A.props => React.element,
}
diff --git a/apps/docs/src/components/SearchBox.res b/apps/docs/src/components/SearchBox.res
index f99e31c3b..a83ef4f02 100644
--- a/apps/docs/src/components/SearchBox.res
+++ b/apps/docs/src/components/SearchBox.res
@@ -23,8 +23,7 @@ let make = (
onClear()
}
- let focusInput = () =>
- textInput.current->Nullable.forEach(el => el->WebAPI.HTMLInputElement.focus)
+ let focusInput = () => textInput.current->Nullable.forEach(el => el->WebAPI.HTMLElement.focus)
let onAreaFocus = evt => {
let el = ReactEvent.Focus.target(evt)
@@ -52,15 +51,11 @@ let make = (
switch full {
| "Escape" => onClear()
| "Tab" =>
- if Array.length(completionValues) === 1 {
- let targetValue = Belt.Array.getExn(completionValues, 0)
-
- if targetValue !== value {
- ReactEvent.Keyboard.preventDefault(evt)
- onValueChange(targetValue)
- } else {
- ()
- }
+ switch completionValues {
+ | [targetValue] if targetValue !== value =>
+ ReactEvent.Keyboard.preventDefault(evt)
+ onValueChange(targetValue)
+ | _ => ()
}
| _ => ()
}
@@ -84,7 +79,7 @@ let make = (
/>
>))}
+ ref={HtmlRef.domRef(textInput)}
onFocus
onKeyDown
onChange={onChange}
diff --git a/apps/docs/src/data/LandingPagePlaygroundLoader.res b/apps/docs/src/data/LandingPagePlaygroundLoader.res
index c8f946591..60d33b6e1 100644
--- a/apps/docs/src/data/LandingPagePlaygroundLoader.res
+++ b/apps/docs/src/data/LandingPagePlaygroundLoader.res
@@ -41,16 +41,16 @@ export {
let build = (): LandingPagePlayground.playgroundData => {
// Keep prerendering independent of global language registration and other routes.
- let highlighter = HighlightJsBindings.make()
- highlighter->HighlightJsBindings.registerLanguage("rescript", HighlightJsBindings.rescript)
- highlighter->HighlightJsBindings.registerLanguage("javascript", HighlightJsBindings.javascript)
+ let highlighter = HighlightLanguages.make(HighlightLanguages.defaultInstance)
+ highlighter->HighlightLanguages.registerLanguage("rescript", HighlightJsBindings.rescript)
+ highlighter->HighlightLanguages.registerLanguage("javascript", HighlightJsBindings.javascript)
{
rescriptHtml: (
- highlighter->HighlightJsBindings.highlight(example.res, {language: "rescript"})
+ highlighter->HighlightLanguages.highlight(example.res, {language: "rescript"})
).value,
javascriptHtml: (
- highlighter->HighlightJsBindings.highlight(example.js, {language: "javascript"})
+ highlighter->HighlightLanguages.highlight(example.js, {language: "javascript"})
).value,
playgroundHref: `/try?code=${LzString.lzString.compressToEncodedURIComponent(example.res)}`,
}
diff --git a/apps/docs/src/data/MetaTagsApi.res b/apps/docs/src/data/MetaTagsApi.res
index 7f3655d02..93a26d825 100644
--- a/apps/docs/src/data/MetaTagsApi.res
+++ b/apps/docs/src/data/MetaTagsApi.res
@@ -16,14 +16,10 @@ let extractMetaTags = async (url: string) => {
let dom = Jsdom.make(html)
let document = dom.window.document
- let nodeList = document->WebAPI.Document.querySelectorAll("meta")
-
+ let collection = document->WebAPI.Document.getElementsByTagName("meta")
let elements = []
-
- for i in 0 to nodeList.length {
- let node = WebAPI.NodeList.item(nodeList, i)
- // cast Node elements to Element
- elements->Array.push((Obj.magic(node): WebAPI.DOMAPI.element))
+ for i in 0 to collection.length - 1 {
+ elements->Array.push(collection->WebAPI.HTMLCollection.item(i))
}
let metaTags = elements->Array.reduce(Dict.fromArray([]), (tags, meta) => {
diff --git a/apps/docs/src/markdown/MdxLegacy.res b/apps/docs/src/markdown/MdxLegacy.res
index 922232b7e..5ee546bd3 100644
--- a/apps/docs/src/markdown/MdxLegacy.res
+++ b/apps/docs/src/markdown/MdxLegacy.res
@@ -1,72 +1,35 @@
-/*
- Abstract type for representing mdx
- components mostly passed as children to
- the component context API
- */
-/**
- * TODO: POST RR7 The code below is from Next's markdown, and I am not sure it is needed anymore.
- */
-type mdxComponent
-
-external fromReactElement: React.element => mdxComponent = "%identity"
-
-external arrToReactElement: array => React.element = "%identity"
-
-let getMdxClassName: mdxComponent => option = %raw("element => {
- if(element == null || element.props == null) {
- return;
- }
- return element.props.className;
- }")
-
-let getMdxType: mdxComponent => string = %raw("element => {
- if(element == null || element.props == null) {
- return 'unknown';
- }
- return element.props.mdxType;
- }")
+type props = {
+ children?: React.element,
+ className?: string,
+ metastring?: string,
+ mdxType?: string,
+}
-module MdxChildren = {
- type unknown
+@get external props: React.element => props = "props"
- type t
+let getChildren = element =>
+ React.isValidElement(element) ? props(element).children->Option.getOr(React.null) : element
- type case =
- | String(string)
- | Element(mdxComponent)
- | Array(array)
- | Unknown(unknown)
+let getClassName = element => React.isValidElement(element) ? props(element).className : None
- let classify = (v: t): case =>
- if %raw(`function (a) { return a instanceof Array}`)(v) {
- Array((Obj.magic(v): array))
- } else if typeof(v) == #string {
- String((Obj.magic(v): string))
- } else if typeof(v) == #object {
- Element((Obj.magic(v): mdxComponent))
- } else {
- Unknown((Obj.magic(v): unknown))
- }
+let getMetastring = element => React.isValidElement(element) ? props(element).metastring : None
- external toReactElement: t => React.element = "%identity"
+type component
+@get external elementType: React.element => component = "type"
- // Sometimes an mdxComponent element can be a string
- // which means it doesn't have any children.
- // We will return the element as its own child then
- let getMdxChildren: mdxComponent => t = %raw("element => {
- if(typeof element === 'string') {
- return element;
- }
- if(element == null || element.props == null || element.props.children == null) {
- return;
+let getType = element => {
+ if React.isValidElement(element) {
+ switch props(element).mdxType {
+ | Some(tag) => tag
+ | None => {
+ let tag = elementType(element)
+ typeof(tag) == #string ? String.make(tag) : "unknown"
}
- return element.props.children;
- }")
+ }
+ } else {
+ "unknown"
+ }
}
-module Components = {
- // Used for reflection based logic in
- // components such as `code` or `ul`
- // with runtime reflection
- type unknown
-}
+@get external component: React.element => React.componentLike<'props, React.element> = "type"
+let isComponent = (element, make) => React.isValidElement(element) && component(element) === make
diff --git a/apps/guide/__tests__/GuideHome_.test.res b/apps/guide/__tests__/GuideHome_.test.res
index 5833174f4..220dc7bf3 100644
--- a/apps/guide/__tests__/GuideHome_.test.res
+++ b/apps/guide/__tests__/GuideHome_.test.res
@@ -5,7 +5,7 @@ let secondLesson = GuideTestFixtures.secondLesson
let guideLessonsWithFinal = GuideTestFixtures.guideLessonsWithFinal
let renderGuideHome = GuideTestFixtures.renderGuideHome
let renderGuideHomeWithDocsIntroNavigation = GuideTestFixtures.renderGuideHomeWithDocsIntroNavigation
-let renderGuideHomeInBrowser = GuideTestFixtures.renderGuideHomeInBrowser
+let renderGuideHomeWithHistory = GuideTestFixtures.renderGuideHomeWithHistory
test("loads saved guide editor code into the editor", async () => {
await viewport(1440, 900)
@@ -117,11 +117,6 @@ test("navigates to the function argument page", async () => {
GuideLayout.clearExerciseCode(secondLesson.exercise.id)
})
-let guideTestUrl = hash => window.location.pathname ++ window.location.search ++ hash
-
-let resetGuideTestUrl = () =>
- WebAPI.History.replaceState(window.history, ~data=JSON.Null, ~unused="", ~url=guideTestUrl(""))
-
test("shows Back before lesson forward actions and returns to the previous lesson", async () => {
await viewport(1440, 900)
GuideLayout.clearCompletedExercises()
@@ -202,37 +197,23 @@ test("Done on a completed final lesson opens the ReScript docs intro", async ()
GuideLayout.clearExerciseCode(secondLesson.exercise.id)
})
-test("browser back returns to the previous guide lesson", async () => {
+test("history back returns to the previous guide lesson", async () => {
await viewport(1440, 900)
GuideLayout.clearCompletedExercises()
GuideLayout.clearExerciseCode(firstLesson.exercise.id)
GuideLayout.clearExerciseCode(secondLesson.exercise.id)
- WebAPI.History.replaceState(
- window.history,
- ~data=JSON.Null,
- ~unused="",
- ~url=guideTestUrl("#guide-test-start"),
- )
- WebAPI.History.pushState(
- window.history,
- ~data=JSON.Null,
- ~unused="",
- ~url=guideTestUrl("#first-contact"),
- )
-
- let screen = await renderGuideHomeInBrowser()
+ let screen = await renderGuideHomeWithHistory()
await (await screen->getByText("Learn ReScript Guide"))->element->toBeVisible
await (await screen->getByText("Next"))->click
await (await screen->getByText("Call A Function"))->element->toBeVisible
- WebAPI.History.back(window.history)
+ await (await screen->getByText("History back"))->click
await (await screen->getByText("Learn ReScript Guide"))->element->toBeVisible
GuideLayout.clearExerciseCode(firstLesson.exercise.id)
GuideLayout.clearExerciseCode(secondLesson.exercise.id)
- resetGuideTestUrl()
})
test("stretches the output surface to the full output panel", async () => {
diff --git a/apps/guide/__tests__/GuideTestFixtures.res b/apps/guide/__tests__/GuideTestFixtures.res
index b3d199fb9..e92e5f7b4 100644
--- a/apps/guide/__tests__/GuideTestFixtures.res
+++ b/apps/guide/__tests__/GuideTestFixtures.res
@@ -71,9 +71,20 @@ let renderGuideHomeWithDocsIntroNavigation = (goToDocsIntro, ~initialEntries=["/
,
)
-let renderGuideHomeInBrowser = () =>
+module HistoryBack = {
+ @module("react-router") external useNavigate: unit => int => unit = "useNavigate"
+
+ @react.component
+ let make = () => {
+ let navigate = useNavigate()
+ navigate(-1)}> {React.string("History back")}
+ }
+}
+
+let renderGuideHomeWithHistory = () =>
render(
-
+
+
- ,
+ ,
)
diff --git a/apps/guide/app/GuideHome.res b/apps/guide/app/GuideHome.res
index 250e6dbaf..1462ecde6 100644
--- a/apps/guide/app/GuideHome.res
+++ b/apps/guide/app/GuideHome.res
@@ -39,7 +39,7 @@ let make = (
{React.string(lesson.title)}
- lesson.content
+ lesson.content
React.element = "default"
diff --git a/apps/guide/package.json b/apps/guide/package.json
index eda0193b4..c607908ac 100644
--- a/apps/guide/package.json
+++ b/apps/guide/package.json
@@ -24,7 +24,6 @@
"isbot": "^5",
"react": "^19.3.0",
"react-dom": "^19.3.0",
- "react-markdown": "^10.1.0",
"react-router": "^8.3.1",
"remark-comment": "^1.0.0",
"remark-frontmatter": "^5.0.0",
diff --git a/packages/playground/src/EvalIFrame.res b/packages/playground/src/EvalIFrame.res
index 3531edcf1..aaee8db3d 100644
--- a/packages/playground/src/EvalIFrame.res
+++ b/packages/playground/src/EvalIFrame.res
@@ -69,12 +69,14 @@ let srcDoc = `