From ef36f3d3b6fcc66a8a4fa53c3287f9f8e0ae08d9 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Sat, 19 Sep 2026 14:17:44 -0400 Subject: [PATCH 1/4] perf(homepage): scope syntax grammars to content routes Remove runtime grammar registration from the shared root. Register content languages at a pathless layout boundary and JavaScript at the lazy playground boundary, with cold-navigation and direct-load coverage. --- .../__tests__/ContentHighlighting_.test.res | 88 ++++++++++++++ .../__tests__/HighlightLanguages_.test.res | 51 ++++++++ .../JavaScriptHighlighting_.test.res | 35 ++++++ apps/docs/app/DocsRoot.res | 29 ----- apps/docs/app/DocsRoot.resi | 4 +- apps/docs/app/DocsRoutes.res | 51 ++++---- apps/docs/app/DocsRoutes.resi | 1 + apps/docs/app/layouts/ContentLayoutRoute.res | 5 + apps/docs/app/layouts/ContentLayoutRoute.resi | 3 + .../homepage-highlighting.spec.mjs | 109 ++++++++++++++++++ apps/docs/e2e/Playground.cy.res | 7 ++ apps/docs/src/common/ContentHighlighting.res | 44 +++++++ apps/docs/src/common/ContentHighlighting.resi | 2 + packages/playground/src/Playground.res | 3 + packages/shared/src/HighlightLanguages.res | 18 +++ packages/shared/src/HighlightLanguages.resi | 7 ++ .../shared/src/JavaScriptHighlighting.res | 5 + .../shared/src/JavaScriptHighlighting.resi | 2 + 18 files changed, 412 insertions(+), 52 deletions(-) create mode 100644 apps/docs/__tests__/ContentHighlighting_.test.res create mode 100644 apps/docs/__tests__/HighlightLanguages_.test.res create mode 100644 apps/docs/__tests__/JavaScriptHighlighting_.test.res create mode 100644 apps/docs/app/layouts/ContentLayoutRoute.res create mode 100644 apps/docs/app/layouts/ContentLayoutRoute.resi create mode 100644 apps/docs/e2e-playwright/homepage-highlighting.spec.mjs create mode 100644 apps/docs/src/common/ContentHighlighting.res create mode 100644 apps/docs/src/common/ContentHighlighting.resi create mode 100644 packages/shared/src/HighlightLanguages.res create mode 100644 packages/shared/src/HighlightLanguages.resi create mode 100644 packages/shared/src/JavaScriptHighlighting.res create mode 100644 packages/shared/src/JavaScriptHighlighting.resi diff --git a/apps/docs/__tests__/ContentHighlighting_.test.res b/apps/docs/__tests__/ContentHighlighting_.test.res new file mode 100644 index 000000000..32157e953 --- /dev/null +++ b/apps/docs/__tests__/ContentHighlighting_.test.res @@ -0,0 +1,88 @@ +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" + +@send +external getLanguage: (HighlightLanguages.highlighter, string) => option = + "getLanguage" + +@send +external highlight: (HighlightLanguages.highlighter, string, highlightOptions) => highlightResult = + "highlight" + +let aliases = [ + "rescript", + "res", + "resi", + "javascript", + "js", + "jsx", + "mjs", + "cjs", + "css", + "ts", + "tsx", + "mts", + "cts", + "typescript", + "sh", + "bash", + "zsh", + "toml", + "json", + "jsonc", + "text", + "txt", + "html", + "xhtml", + "rss", + "atom", + "xjb", + "xsd", + "xsl", + "plist", + "wsf", + "svg", + "diff", + "patch", +] + +test("content registration preserves every established language name and alias", async () => { + let highlighter = createHighlighter() + highlighter->ContentHighlighting.register + + let missingAliases = + aliases->Array.filter(alias => highlighter->getLanguage(alias)->Option.isNone) + expect(missingAliases)->toEqual([]) +}) + +test("content registration produces highlighted ReScript and escaped plain text", async () => { + let highlighter = createHighlighter() + highlighter->ContentHighlighting.register + + let rescript = highlighter->highlight("let count = 1", {language: "res"}) + let text = highlighter->highlight("", {language: "text"}) + + expect(rescript.value->String.includes("let"))->toBe(true) + expect(rescript.value->String.includes("1"))->toBe(true) + expect(text.value)->toBe("<button>Example & code</button>") +}) + +test("repeated content initialization leaves registered grammars unchanged", async () => { + let highlighter = createHighlighter() + highlighter->ContentHighlighting.register + let original = highlighter->getLanguage("javascript") + let before = highlighter->highlight("const count: number = 1;", {language: "ts"}) + + highlighter->ContentHighlighting.register + + let after = highlighter->highlight("const count: number = 1;", {language: "ts"}) + expect(highlighter->getLanguage("javascript"))->toBe(original) + expect(highlighter->getLanguage("js"))->toBe(original) + expect(after.value)->toBe(before.value) +}) diff --git a/apps/docs/__tests__/HighlightLanguages_.test.res b/apps/docs/__tests__/HighlightLanguages_.test.res new file mode 100644 index 000000000..6b1179bde --- /dev/null +++ b/apps/docs/__tests__/HighlightLanguages_.test.res @@ -0,0 +1,51 @@ +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() + + expect(highlighter->getLanguage("javascript"))->toEqual(None) + expect(highlighter->getLanguage("js"))->toEqual(None) + highlighter->HighlightLanguages.ensureRegistered("javascript", javascript) + + let result = highlighter->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") + + highlighter->HighlightLanguages.ensureRegistered("javascript", plaintext) + highlighter->HighlightLanguages.ensureRegistered("js", plaintext) + + expect(highlighter->getLanguage("javascript"))->toBe(original) + expect(highlighter->getLanguage("js"))->toBe(original) + let result = highlighter->highlight("const answer = 42;", {language: "js"}) + expect(result.value)->toBe( + "const answer = 42;", + ) +}) diff --git a/apps/docs/__tests__/JavaScriptHighlighting_.test.res b/apps/docs/__tests__/JavaScriptHighlighting_.test.res new file mode 100644 index 000000000..78e5b3454 --- /dev/null +++ b/apps/docs/__tests__/JavaScriptHighlighting_.test.res @@ -0,0 +1,35 @@ +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" + +@send +external listLanguages: HighlightLanguages.highlighter => array = "listLanguages" + +@send +external getLanguage: (HighlightLanguages.highlighter, string) => option = + "getLanguage" + +@send +external highlight: (HighlightLanguages.highlighter, string, highlightOptions) => highlightResult = + "highlight" + +test("playground registration loads only JavaScript and its working aliases", async () => { + let highlighter = createHighlighter() + highlighter->JavaScriptHighlighting.register + + let missingAliases = + ["javascript", "js", "jsx", "mjs", "cjs"]->Array.filter(alias => + highlighter->getLanguage(alias)->Option.isNone + ) + expect(highlighter->listLanguages)->toEqual(["javascript"]) + expect(missingAliases)->toEqual([]) + let result = highlighter->highlight("const answer = 42;", {language: "js"}) + expect(result.value)->toBe( + "const answer = 42;", + ) +}) diff --git a/apps/docs/app/DocsRoot.res b/apps/docs/app/DocsRoot.res index 5baf7427e..f45def69d 100644 --- a/apps/docs/app/DocsRoot.res +++ b/apps/docs/app/DocsRoot.res @@ -4,35 +4,6 @@ external mainCss: string = "default" @module("../styles/_hljs.css?url") external hljsCss: string = "default" -%%raw(` - import hljs from 'highlight.js/lib/core'; - import bash from 'highlight.js/lib/languages/bash'; - import css from 'highlight.js/lib/languages/css'; - import diff from 'highlight.js/lib/languages/diff'; - import javascript from 'highlight.js/lib/languages/javascript'; - import typescript from 'highlight.js/lib/languages/typescript'; - import json from 'highlight.js/lib/languages/json'; - import text from 'highlight.js/lib/languages/plaintext'; - import html from 'highlight.js/lib/languages/xml'; - import yaml from 'highlight.js/lib/languages/yaml'; - import toml from 'highlight.js/lib/languages/ini'; - import rescript from 'highlightjs-rescript'; - - hljs.registerLanguage('rescript', rescript) - hljs.registerLanguage('javascript', javascript) - hljs.registerLanguage('css', css) - hljs.registerLanguage('ts', typescript) - hljs.registerLanguage('sh', bash) - hljs.registerLanguage('bash', bash) - hljs.registerLanguage('toml', toml) - hljs.registerLanguage('json', json) - hljs.registerLanguage('text', text) - hljs.registerLanguage('html', html) - hljs.registerLanguage('diff', diff) - hljs.registerLanguage('typescript', typescript) - hljs.registerLanguage('yaml', yaml) -`) - open ReactRouter @react.component diff --git a/apps/docs/app/DocsRoot.resi b/apps/docs/app/DocsRoot.resi index 1ddeb3433..98c027984 100644 --- a/apps/docs/app/DocsRoot.resi +++ b/apps/docs/app/DocsRoot.resi @@ -1,4 +1,4 @@ -/** Shared shell styles and the stable Cypress bootstrap slot. -Feature-specific styles load with their components. */ +/** Shared shell, token styles, and the stable Cypress bootstrap slot. +Runtime grammars belong to content routes and the playground. */ @react.component let default: unit => Jsx.element diff --git a/apps/docs/app/DocsRoutes.res b/apps/docs/app/DocsRoutes.res index 2992c859f..326a19350 100644 --- a/apps/docs/app/DocsRoutes.res +++ b/apps/docs/app/DocsRoutes.res @@ -74,30 +74,39 @@ let syntaxLookupDetailRoutes = let default = [ index("./routes/LandingPageRoute.jsx"), - route("packages", "./routes/PackagesRoute.jsx"), route("try", "./routes/TryRoute.jsx"), - route("brand", "./routes/BrandRoute.jsx"), - route("blog", "./routes/BlogRoute.jsx", ~options={id: "blog-index"}), - route("blog/archived", "./routes/BlogRoute.jsx", ~options={id: "blog-archived"}), - ...blogArticleRoutes, - ...communityRoutes, - route("docs", "./routes/DocsOverview.jsx", ~options={id: "docs-overview"}), layout( - "./layouts/DocsLayoutRoute.jsx", + "./layouts/ContentLayoutRoute.jsx", [ - route("docs/manual/api", "./routes/ApiOverviewRoute.jsx", ~options={id: "api-overview"}), - route("docs/manual/api/stdlib", "./routes/ApiRoute.jsx", ~options={id: "api-stdlib"}), - route("docs/manual/api/introduction", "./routes/ApiRoute.jsx", ~options={id: "api-intro"}), - route("docs/manual/api/belt", "./routes/ApiRoute.jsx", ~options={id: "api-belt"}), - route("docs/manual/api/dom", "./routes/ApiRoute.jsx", ~options={id: "api-dom"}), - ...stdlibRoutes, - ...beltRoutes, - ...domRoutes, - ...docsManualRoutes, - ...docsGuidesRoutes, - ...docsReactRoutes, - route("syntax-lookup", "./routes/SyntaxLookupRoute.jsx", ~options={id: "syntax-lookup"}), - ...syntaxLookupDetailRoutes, + route("packages", "./routes/PackagesRoute.jsx"), + route("brand", "./routes/BrandRoute.jsx"), + route("blog", "./routes/BlogRoute.jsx", ~options={id: "blog-index"}), + route("blog/archived", "./routes/BlogRoute.jsx", ~options={id: "blog-archived"}), + ...blogArticleRoutes, + ...communityRoutes, + route("docs", "./routes/DocsOverview.jsx", ~options={id: "docs-overview"}), + layout( + "./layouts/DocsLayoutRoute.jsx", + [ + route("docs/manual/api", "./routes/ApiOverviewRoute.jsx", ~options={id: "api-overview"}), + route("docs/manual/api/stdlib", "./routes/ApiRoute.jsx", ~options={id: "api-stdlib"}), + route( + "docs/manual/api/introduction", + "./routes/ApiRoute.jsx", + ~options={id: "api-intro"}, + ), + route("docs/manual/api/belt", "./routes/ApiRoute.jsx", ~options={id: "api-belt"}), + route("docs/manual/api/dom", "./routes/ApiRoute.jsx", ~options={id: "api-dom"}), + ...stdlibRoutes, + ...beltRoutes, + ...domRoutes, + ...docsManualRoutes, + ...docsGuidesRoutes, + ...docsReactRoutes, + route("syntax-lookup", "./routes/SyntaxLookupRoute.jsx", ~options={id: "syntax-lookup"}), + ...syntaxLookupDetailRoutes, + ], + ), ], ), route("*", "./routes/NotFoundRoute.jsx"), diff --git a/apps/docs/app/DocsRoutes.resi b/apps/docs/app/DocsRoutes.resi index 51f6d36ee..c60e74bfd 100644 --- a/apps/docs/app/DocsRoutes.resi +++ b/apps/docs/app/DocsRoutes.resi @@ -10,4 +10,5 @@ let docsReactRoutes: array let docsGuidesRoutes: array let communityRoutes: array let syntaxLookupDetailRoutes: array +/** Keep home, the lazy playground, and not-found outside the content highlighting boundary. */ let default: array diff --git a/apps/docs/app/layouts/ContentLayoutRoute.res b/apps/docs/app/layouts/ContentLayoutRoute.res new file mode 100644 index 000000000..5d0015195 --- /dev/null +++ b/apps/docs/app/layouts/ContentLayoutRoute.res @@ -0,0 +1,5 @@ +// Route-module initialization runs before any content children render. +let () = ContentHighlighting.register(HighlightLanguages.defaultInstance) + +@react.component +let default = () => diff --git a/apps/docs/app/layouts/ContentLayoutRoute.resi b/apps/docs/app/layouts/ContentLayoutRoute.resi new file mode 100644 index 000000000..a5c43f841 --- /dev/null +++ b/apps/docs/app/layouts/ContentLayoutRoute.resi @@ -0,0 +1,3 @@ +/** Own runtime highlighting for content routes without adding a DOM wrapper. */ +@react.component +let default: unit => React.element diff --git a/apps/docs/e2e-playwright/homepage-highlighting.spec.mjs b/apps/docs/e2e-playwright/homepage-highlighting.spec.mjs new file mode 100644 index 000000000..76a9ce260 --- /dev/null +++ b/apps/docs/e2e-playwright/homepage-highlighting.spec.mjs @@ -0,0 +1,109 @@ +import { expect, test } from "playwright/test"; +import { JSDOM } from "jsdom"; + +test("initial homepage scripts exclude example preparation and the syntax runtime", async ({ + request, +}) => { + const response = await request.get("/"); + const { document } = new JSDOM(await response.text()).window; + const scripts = [ + ...document.querySelectorAll('link[rel="modulepreload"][href]'), + ...document.querySelectorAll("script[src]"), + ].map( + (element) => element.getAttribute("href") ?? element.getAttribute("src"), + ); + + expect(response.ok()).toBe(true); + expect(scripts.length).toBeGreaterThan(0); + for (const asset of scripts) { + const script = await request.get(asset); + expect(script.ok()).toBe(true); + const source = await script.text(); + expect(source.includes("compressToEncodedURIComponent"), asset).toBe(false); + expect(source.includes("registerLanguage"), asset).toBe(false); + expect(source.includes("function Playground$Button(props)"), asset).toBe( + false, + ); + } +}); + +test("content routes highlight JSON on direct loads and cold homepage navigation", async ({ + page, + request, +}) => { + const path = "/docs/manual/build-configuration"; + const response = await request.get(`${path}/`); + const { document } = new JSDOM(await response.text()).window; + expect(response.ok()).toBe(true); + expect(document.querySelector("code.lang-json .hljs-attr")?.textContent).toBe( + '"sources"', + ); + + await page.goto(`${path}/`); + const property = page.locator("code.lang-json .hljs-attr").first(); + await expect(property).toHaveText('"sources"'); + await page.goto("/"); + await expect(page).toHaveURL("/"); + await expect( + page.getByRole("heading", { + level: 1, + name: "JavaScript Made Simple for Humans and AI", + }), + ).toBeVisible(); + await page.getByRole("link", { name: "Docs", exact: true }).click(); + await expect( + page.getByRole("heading", { level: 1, name: "ReScript", exact: true }), + ).toBeVisible(); + await page.getByRole("link", { name: "Configuration", exact: true }).click(); + await expect(page).toHaveURL(path); + await expect(property).toHaveText('"sources"'); +}); + +test("prepared examples survive hydration and navigation back from documentation", async ({ + page, + request, +}) => { + const response = await request.get("/"); + const { document } = new JSDOM(await response.text()).window; + const examples = ["res", "js"].map((language) => ({ + selector: `code.lang-${language}`, + html: document.querySelector(`code.lang-${language}`)?.innerHTML, + })); + const playgroundHref = document + .querySelector('a[href^="/try?code="]') + ?.getAttribute("href"); + const runtimeErrors = []; + page.on("pageerror", (error) => runtimeErrors.push(error.message)); + page.on("console", (message) => { + if (message.type() === "error") runtimeErrors.push(message.text()); + }); + + expect(response.ok()).toBe(true); + expect(playgroundHref).toMatch(/^\/try\?code=.+/); + for (const example of examples) { + expect(example.html).toContain(' { waitForHydration() }) + it("should highlight JavaScript after a direct playground load", () => { + visit("/try") + get(".cm-editor")->shouldBeVisible->ignore + get("pre.whitespace-pre-wrap")->shouldContainText("react/jsx-runtime")->ignore + get("pre code.lang-js span[class^='hljs-']")->should("exist")->ignore + }) + 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") diff --git a/apps/docs/src/common/ContentHighlighting.res b/apps/docs/src/common/ContentHighlighting.res new file mode 100644 index 000000000..c69239bb8 --- /dev/null +++ b/apps/docs/src/common/ContentHighlighting.res @@ -0,0 +1,44 @@ +@module("highlight.js/lib/languages/bash") +external bash: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/css") +external css: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/diff") +external diff: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/javascript") +external javascript: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/typescript") +external typescript: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/json") +external json: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/plaintext") +external text: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/xml") +external html: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/ini") +external toml: HighlightLanguages.definition = "default" + +@module("highlightjs-rescript") +external rescript: HighlightLanguages.definition = "default" + +let register = highlighter => { + highlighter->HighlightLanguages.ensureRegistered("rescript", rescript) + highlighter->HighlightLanguages.ensureRegistered("javascript", javascript) + highlighter->HighlightLanguages.ensureRegistered("css", css) + highlighter->HighlightLanguages.ensureRegistered("ts", typescript) + highlighter->HighlightLanguages.ensureRegistered("sh", bash) + highlighter->HighlightLanguages.ensureRegistered("bash", bash) + highlighter->HighlightLanguages.ensureRegistered("toml", toml) + highlighter->HighlightLanguages.ensureRegistered("json", json) + highlighter->HighlightLanguages.ensureRegistered("text", text) + highlighter->HighlightLanguages.ensureRegistered("html", html) + highlighter->HighlightLanguages.ensureRegistered("diff", diff) + highlighter->HighlightLanguages.ensureRegistered("typescript", typescript) +} diff --git a/apps/docs/src/common/ContentHighlighting.resi b/apps/docs/src/common/ContentHighlighting.resi new file mode 100644 index 000000000..db03a979d --- /dev/null +++ b/apps/docs/src/common/ContentHighlighting.resi @@ -0,0 +1,2 @@ +/** Register the content routes' grammars and their established aliases. */ +let register: HighlightLanguages.highlighter => unit diff --git a/packages/playground/src/Playground.res b/packages/playground/src/Playground.res index 37f210e68..70038bc9e 100644 --- a/packages/playground/src/Playground.res +++ b/packages/playground/src/Playground.res @@ -1,6 +1,9 @@ open CompilerManagerHook module Api = RescriptCompilerApi +// The lazy playground must highlight direct visits without loading a content route first. +let () = JavaScriptHighlighting.register(HighlightLanguages.defaultInstance) + type layout = Column | Row type tab = JavaScript | Output | Problems | Settings diff --git a/packages/shared/src/HighlightLanguages.res b/packages/shared/src/HighlightLanguages.res new file mode 100644 index 000000000..60250770b --- /dev/null +++ b/packages/shared/src/HighlightLanguages.res @@ -0,0 +1,18 @@ +type highlighter +type definition +type registeredLanguage + +@module("highlight.js/lib/core") +external defaultInstance: highlighter = "default" + +@send +external getLanguage: (highlighter, string) => option = "getLanguage" + +@send +external registerLanguage: (highlighter, string, definition) => unit = "registerLanguage" + +let ensureRegistered = (highlighter, name, definition) => + switch highlighter->getLanguage(name) { + | Some(_) => () + | None => highlighter->registerLanguage(name, definition) + } diff --git a/packages/shared/src/HighlightLanguages.resi b/packages/shared/src/HighlightLanguages.resi new file mode 100644 index 000000000..13ebcccba --- /dev/null +++ b/packages/shared/src/HighlightLanguages.resi @@ -0,0 +1,7 @@ +type highlighter +type definition + +let defaultInstance: highlighter + +/** Preserve an existing language or alias when route modules initialize more than once. */ +let ensureRegistered: (highlighter, string, definition) => unit diff --git a/packages/shared/src/JavaScriptHighlighting.res b/packages/shared/src/JavaScriptHighlighting.res new file mode 100644 index 000000000..7bfa6de35 --- /dev/null +++ b/packages/shared/src/JavaScriptHighlighting.res @@ -0,0 +1,5 @@ +@module("highlight.js/lib/languages/javascript") +external javascript: HighlightLanguages.definition = "default" + +let register = highlighter => + highlighter->HighlightLanguages.ensureRegistered("javascript", javascript) diff --git a/packages/shared/src/JavaScriptHighlighting.resi b/packages/shared/src/JavaScriptHighlighting.resi new file mode 100644 index 000000000..b69289ac6 --- /dev/null +++ b/packages/shared/src/JavaScriptHighlighting.resi @@ -0,0 +1,2 @@ +/** Register only the playground's JavaScript grammar and its aliases. */ +let register: HighlightLanguages.highlighter => unit From c6d7f3313552a28119e75ae63e9b82b554fdf596 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Sat, 19 Sep 2026 18:15:58 -0400 Subject: [PATCH 2/4] test(highlighting): port content boundary checks to Cypress Keep syntax-runtime bundle exclusion and direct/cold-navigation JSON highlighting coverage for PR #1355 comment 4054821928. --- .../homepage-highlighting.spec.mjs | 109 ------------------ apps/docs/e2e/bindings/Cypress.res | 3 + .../e2e/homepage/HomepageHighlighting.cy.res | 33 +++++- 3 files changed, 35 insertions(+), 110 deletions(-) delete mode 100644 apps/docs/e2e-playwright/homepage-highlighting.spec.mjs diff --git a/apps/docs/e2e-playwright/homepage-highlighting.spec.mjs b/apps/docs/e2e-playwright/homepage-highlighting.spec.mjs deleted file mode 100644 index 76a9ce260..000000000 --- a/apps/docs/e2e-playwright/homepage-highlighting.spec.mjs +++ /dev/null @@ -1,109 +0,0 @@ -import { expect, test } from "playwright/test"; -import { JSDOM } from "jsdom"; - -test("initial homepage scripts exclude example preparation and the syntax runtime", async ({ - request, -}) => { - const response = await request.get("/"); - const { document } = new JSDOM(await response.text()).window; - const scripts = [ - ...document.querySelectorAll('link[rel="modulepreload"][href]'), - ...document.querySelectorAll("script[src]"), - ].map( - (element) => element.getAttribute("href") ?? element.getAttribute("src"), - ); - - expect(response.ok()).toBe(true); - expect(scripts.length).toBeGreaterThan(0); - for (const asset of scripts) { - const script = await request.get(asset); - expect(script.ok()).toBe(true); - const source = await script.text(); - expect(source.includes("compressToEncodedURIComponent"), asset).toBe(false); - expect(source.includes("registerLanguage"), asset).toBe(false); - expect(source.includes("function Playground$Button(props)"), asset).toBe( - false, - ); - } -}); - -test("content routes highlight JSON on direct loads and cold homepage navigation", async ({ - page, - request, -}) => { - const path = "/docs/manual/build-configuration"; - const response = await request.get(`${path}/`); - const { document } = new JSDOM(await response.text()).window; - expect(response.ok()).toBe(true); - expect(document.querySelector("code.lang-json .hljs-attr")?.textContent).toBe( - '"sources"', - ); - - await page.goto(`${path}/`); - const property = page.locator("code.lang-json .hljs-attr").first(); - await expect(property).toHaveText('"sources"'); - await page.goto("/"); - await expect(page).toHaveURL("/"); - await expect( - page.getByRole("heading", { - level: 1, - name: "JavaScript Made Simple for Humans and AI", - }), - ).toBeVisible(); - await page.getByRole("link", { name: "Docs", exact: true }).click(); - await expect( - page.getByRole("heading", { level: 1, name: "ReScript", exact: true }), - ).toBeVisible(); - await page.getByRole("link", { name: "Configuration", exact: true }).click(); - await expect(page).toHaveURL(path); - await expect(property).toHaveText('"sources"'); -}); - -test("prepared examples survive hydration and navigation back from documentation", async ({ - page, - request, -}) => { - const response = await request.get("/"); - const { document } = new JSDOM(await response.text()).window; - const examples = ["res", "js"].map((language) => ({ - selector: `code.lang-${language}`, - html: document.querySelector(`code.lang-${language}`)?.innerHTML, - })); - const playgroundHref = document - .querySelector('a[href^="/try?code="]') - ?.getAttribute("href"); - const runtimeErrors = []; - page.on("pageerror", (error) => runtimeErrors.push(error.message)); - page.on("console", (message) => { - if (message.type() === "error") runtimeErrors.push(message.text()); - }); - - expect(response.ok()).toBe(true); - expect(playgroundHref).toMatch(/^\/try\?code=.+/); - for (const example of examples) { - expect(example.html).toContain(', @as("have.css") _, string) => chai @send external shouldInt: (chain<'a>, string, int) => chain<'a> = "should" @send external shouldValue: (chain, @as("have.value") _, string) => chain = "should" +@send +external shouldText: (chain, @as("have.text") _, string) => chain = "should" @send external shouldEqual: (chain<'a>, @as("equal") _, 'a) => chain<'a> = "should" @send external shouldDeepEqual: (chain<'a>, @as("deep.equal") _, 'a) => chain<'a> = "should" @send external shouldMatch: (chain<'a>, @as("match") _, RegExp.t) => chain<'a> = "should" @@ -95,6 +97,7 @@ external shouldProperty: (chain, @as("have.prop") _, string, string) = @send external propertyInt: (chain<'a>, string) => chain = "its" @send external shouldSatisfy: (chain<'a>, 'a => unit) => chain<'a> = "should" @send external click: chain => chain = "click" +@send external first: chain => chain = "first" @send external typeText: (chain, string) => chain = "type" @send external containsChildRegex: (chain, string, RegExp.t) => chain = "contains" diff --git a/apps/docs/e2e/homepage/HomepageHighlighting.cy.res b/apps/docs/e2e/homepage/HomepageHighlighting.cy.res index fa6a82b36..e16db0687 100644 --- a/apps/docs/e2e/homepage/HomepageHighlighting.cy.res +++ b/apps/docs/e2e/homepage/HomepageHighlighting.cy.res @@ -1,7 +1,7 @@ open Cypress open HomepageHelpers -it("initial homepage scripts do not include example preparation or compression", () => { +it("initial homepage scripts exclude example preparation and the syntax runtime", () => { homepageDocument(document => { let scripts = document->initialScriptUrls expect(scripts->Array.length)->greaterThan(0) @@ -12,6 +12,7 @@ it("initial homepage scripts do not include example preparation or compression", response => { expect(response.status, ~message=asset)->equal(200) expect(response.body, ~message=asset)->notInclude("compressToEncodedURIComponent") + expect(response.body, ~message=asset)->notInclude("registerLanguage") expect(response.body, ~message=asset)->notInclude("function Playground$Button(props)") }, ) @@ -20,6 +21,36 @@ it("initial homepage scripts do not include example preparation or compression", }) }) +it("content routes highlight JSON on direct loads and cold homepage navigation", () => { + let path = "/docs/manual/build-configuration" + request(`${path}/`) + ->then(response => { + expect(response.status)->equal(200) + let document = parser()->parseHtml(response.body) + let text = + document + ->querySelector("code.lang-json .hljs-attr") + ->Null.toOption + ->Option.flatMap(element => element->textContent->Null.toOption) + expect(text)->equal(Some(`"sources"`)) + }) + ->ignore + visit(`${path}/`) + get("code.lang-json .hljs-attr")->first->shouldText(`"sources"`)->ignore + visit("/") + cyLocation("pathname")->shouldEqual("/")->ignore + containsIn("h1", headline)->should("be.visible")->ignore + containsInRegex("a", /^Docs$/)->click->ignore + containsInRegex("h1", /^ReScript$/)->should("be.visible")->ignore + get("aside:visible") + ->containsChildRegex("a", /^Configuration$/) + ->scrollIntoView + ->click + ->ignore + cyLocation("pathname")->shouldEqual(path)->ignore + get("code.lang-json .hljs-attr")->first->shouldText(`"sources"`)->ignore +}) + it("prepared examples survive hydration and navigation back from documentation", () => { homepageDocument(document => { let examples = ["res", "js"]->Array.map( From 418d201497c4d877f957c31e42b2d592db096cd8 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Sat, 19 Sep 2026 18:38:42 -0400 Subject: [PATCH 3/4] fix(highlighting): register YAML for documentation Load the existing YAML grammar at the content boundary and verify YAML/yml aliases and installation-example highlighting. This removes application console errors exposed by the strict Cypress navigation tests. --- apps/docs/__tests__/ContentHighlighting_.test.res | 14 ++++++++++++++ apps/docs/src/common/ContentHighlighting.res | 4 ++++ apps/docs/vitest.config.mjs | 1 + 3 files changed, 19 insertions(+) diff --git a/apps/docs/__tests__/ContentHighlighting_.test.res b/apps/docs/__tests__/ContentHighlighting_.test.res index 32157e953..a86f1c881 100644 --- a/apps/docs/__tests__/ContentHighlighting_.test.res +++ b/apps/docs/__tests__/ContentHighlighting_.test.res @@ -50,6 +50,8 @@ let aliases = [ "svg", "diff", "patch", + "yaml", + "yml", ] test("content registration preserves every established language name and alias", async () => { @@ -86,3 +88,15 @@ test("repeated content initialization leaves registered grammars unchanged", asy expect(highlighter->getLanguage("js"))->toBe(original) expect(after.value)->toBe(before.value) }) + +test("content registration highlights YAML installation examples and their alias", async () => { + let highlighter = createHighlighter() + highlighter->ContentHighlighting.register + let code = "packages:\n - apps/*" + + let yaml = highlighter->highlight(code, {language: "yaml"}) + let alias = highlighter->highlight(code, {language: "yml"}) + + expect(yaml.value->String.includes("packages:"))->toBe(true) + expect(alias.value)->toBe(yaml.value) +}) diff --git a/apps/docs/src/common/ContentHighlighting.res b/apps/docs/src/common/ContentHighlighting.res index c69239bb8..5d2943d11 100644 --- a/apps/docs/src/common/ContentHighlighting.res +++ b/apps/docs/src/common/ContentHighlighting.res @@ -22,6 +22,9 @@ external text: HighlightLanguages.definition = "default" @module("highlight.js/lib/languages/xml") external html: HighlightLanguages.definition = "default" +@module("highlight.js/lib/languages/yaml") +external yaml: HighlightLanguages.definition = "default" + @module("highlight.js/lib/languages/ini") external toml: HighlightLanguages.definition = "default" @@ -41,4 +44,5 @@ let register = highlighter => { highlighter->HighlightLanguages.ensureRegistered("html", html) highlighter->HighlightLanguages.ensureRegistered("diff", diff) highlighter->HighlightLanguages.ensureRegistered("typescript", typescript) + highlighter->HighlightLanguages.ensureRegistered("yaml", yaml) } diff --git a/apps/docs/vitest.config.mjs b/apps/docs/vitest.config.mjs index c39aeb9fa..0d24589ec 100644 --- a/apps/docs/vitest.config.mjs +++ b/apps/docs/vitest.config.mjs @@ -27,6 +27,7 @@ const setupDeps = [ "highlight.js/lib/languages/plaintext", "highlight.js/lib/languages/typescript", "highlight.js/lib/languages/xml", + "highlight.js/lib/languages/yaml", "highlightjs-rescript", ]; From f4c30a5534d2036fbd78638de51eccfec50a7475 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Sat, 19 Sep 2026 23:54:28 -0400 Subject: [PATCH 4/4] refactor(highlighting): centralize content bindings Keep npm language imports in the dedicated Highlight.js binding module and leave content highlighting responsible only for registration policy. --- .../docs/src/bindings/HighlightJsBindings.res | 44 +++++++++++--- apps/docs/src/common/ContentHighlighting.res | 59 ++++--------------- 2 files changed, 50 insertions(+), 53 deletions(-) diff --git a/apps/docs/src/bindings/HighlightJsBindings.res b/apps/docs/src/bindings/HighlightJsBindings.res index cbfe747c0..f793d247f 100644 --- a/apps/docs/src/bindings/HighlightJsBindings.res +++ b/apps/docs/src/bindings/HighlightJsBindings.res @@ -1,19 +1,49 @@ -type t -type language type highlightOptions = {language: string} type highlightResult = {value: string} @module("highlight.js/lib/core") @scope("default") -external make: unit => t = "newInstance" +external make: unit => HighlightLanguages.highlighter = "newInstance" + +@module("highlight.js/lib/languages/bash") +external bash: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/css") +external css: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/diff") +external diff: HighlightLanguages.definition = "default" @module("highlight.js/lib/languages/javascript") -external javascript: language = "default" +external javascript: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/typescript") +external typescript: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/json") +external json: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/plaintext") +external text: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/xml") +external html: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/yaml") +external yaml: HighlightLanguages.definition = "default" + +@module("highlight.js/lib/languages/ini") +external toml: HighlightLanguages.definition = "default" @module("highlightjs-rescript") -external rescript: language = "default" +external rescript: HighlightLanguages.definition = "default" @send -external registerLanguage: (t, string, language) => unit = "registerLanguage" +external registerLanguage: ( + HighlightLanguages.highlighter, + string, + HighlightLanguages.definition, +) => unit = "registerLanguage" @send -external highlight: (t, string, highlightOptions) => highlightResult = "highlight" +external highlight: (HighlightLanguages.highlighter, string, highlightOptions) => highlightResult = + "highlight" diff --git a/apps/docs/src/common/ContentHighlighting.res b/apps/docs/src/common/ContentHighlighting.res index 5d2943d11..faae2e2c3 100644 --- a/apps/docs/src/common/ContentHighlighting.res +++ b/apps/docs/src/common/ContentHighlighting.res @@ -1,48 +1,15 @@ -@module("highlight.js/lib/languages/bash") -external bash: HighlightLanguages.definition = "default" - -@module("highlight.js/lib/languages/css") -external css: HighlightLanguages.definition = "default" - -@module("highlight.js/lib/languages/diff") -external diff: HighlightLanguages.definition = "default" - -@module("highlight.js/lib/languages/javascript") -external javascript: HighlightLanguages.definition = "default" - -@module("highlight.js/lib/languages/typescript") -external typescript: HighlightLanguages.definition = "default" - -@module("highlight.js/lib/languages/json") -external json: HighlightLanguages.definition = "default" - -@module("highlight.js/lib/languages/plaintext") -external text: HighlightLanguages.definition = "default" - -@module("highlight.js/lib/languages/xml") -external html: HighlightLanguages.definition = "default" - -@module("highlight.js/lib/languages/yaml") -external yaml: HighlightLanguages.definition = "default" - -@module("highlight.js/lib/languages/ini") -external toml: HighlightLanguages.definition = "default" - -@module("highlightjs-rescript") -external rescript: HighlightLanguages.definition = "default" - let register = highlighter => { - highlighter->HighlightLanguages.ensureRegistered("rescript", rescript) - highlighter->HighlightLanguages.ensureRegistered("javascript", javascript) - highlighter->HighlightLanguages.ensureRegistered("css", css) - highlighter->HighlightLanguages.ensureRegistered("ts", typescript) - highlighter->HighlightLanguages.ensureRegistered("sh", bash) - highlighter->HighlightLanguages.ensureRegistered("bash", bash) - highlighter->HighlightLanguages.ensureRegistered("toml", toml) - highlighter->HighlightLanguages.ensureRegistered("json", json) - highlighter->HighlightLanguages.ensureRegistered("text", text) - highlighter->HighlightLanguages.ensureRegistered("html", html) - highlighter->HighlightLanguages.ensureRegistered("diff", diff) - highlighter->HighlightLanguages.ensureRegistered("typescript", typescript) - highlighter->HighlightLanguages.ensureRegistered("yaml", yaml) + highlighter->HighlightLanguages.ensureRegistered("rescript", HighlightJsBindings.rescript) + highlighter->HighlightLanguages.ensureRegistered("javascript", HighlightJsBindings.javascript) + highlighter->HighlightLanguages.ensureRegistered("css", HighlightJsBindings.css) + highlighter->HighlightLanguages.ensureRegistered("ts", HighlightJsBindings.typescript) + highlighter->HighlightLanguages.ensureRegistered("sh", HighlightJsBindings.bash) + highlighter->HighlightLanguages.ensureRegistered("bash", HighlightJsBindings.bash) + highlighter->HighlightLanguages.ensureRegistered("toml", HighlightJsBindings.toml) + highlighter->HighlightLanguages.ensureRegistered("json", HighlightJsBindings.json) + highlighter->HighlightLanguages.ensureRegistered("text", HighlightJsBindings.text) + highlighter->HighlightLanguages.ensureRegistered("html", HighlightJsBindings.html) + highlighter->HighlightLanguages.ensureRegistered("diff", HighlightJsBindings.diff) + highlighter->HighlightLanguages.ensureRegistered("typescript", HighlightJsBindings.typescript) + highlighter->HighlightLanguages.ensureRegistered("yaml", HighlightJsBindings.yaml) }