Skip to content

Commit e877771

Browse files
CopilotjderochervlkCopilot
authored
Merge master into vlk/split-out-api-overview (PR #1226) (#1240)
* ci: allow dependabot PRs to skip deployments to cloudflare (#1235) * ci: allow dependabot PRs to deploy via pull_request_target GitHub restricts secrets for pull_request events triggered by dependabot[bot]. Switch dependabot PRs to pull_request_target, which runs in the base branch context and has access to secrets. - Add pull_request_target trigger - Route dependabot PRs through pull_request_target only - Route all other PRs through pull_request only (no double runs) - Checkout PR head SHA for pull_request_target events * Update .github/workflows/deploy.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * bypass cloudflare * Simplify deploy job condition in workflow file * Fix conditional syntax in deploy workflow steps --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: patch marked ReDoS vulnerability (CVE-2022-21681) via Yarn resolution override (#1236) * Initial plan * fix: upgrade marked to 4.0.10 via resolutions to fix ReDoS (GHSA-5v2h-r2cx-5xgj) Agent-Logs-Url: https://github.com/rescript-lang/rescript-lang.org/sessions/9eb986e6-cb64-40d5-ac83-ff5bdd72d561 Co-authored-by: jderochervlk <60623931+jderochervlk@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jderochervlk <60623931+jderochervlk@users.noreply.github.com> * feat: split community pages out of MdxRoute into CommunityRoute (#1223) - Create CommunityRoute.res with dedicated loader and community sidebar - Register communityRoutes in routes.res, filter community from mdxRoutes - Remove communityTableOfContents, community branches from MdxRoute * fix: Add .resi file for CommunityRoute.jsx (#1238) * Add Cypress E2E testing with ReScript bindings and CI integration (#1239) * Add Cypress E2E testing with ReScript bindings and CI integration - Add Cypress config and support files for E2E tests - Add ReScript bindings for Cypress in e2e/bindings - Add navigation E2E test in e2e/Navigation_.cy.res - Update .gitignore for e2e artifacts - Add Cypress and E2E scripts to package.json - Add e2e to rescript.json dev sources - Update GitHub Actions to run E2E tests after deploy * Use Cypress GitHub Action for E2E tests in deploy workflow * pr feedback * wait again * configure retries * change type name * Initial plan --------- Co-authored-by: Josh Vlk <josh@vlkpack.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: jderochervlk <60623931+jderochervlk@users.noreply.github.com>
1 parent 8fda350 commit e877771

13 files changed

Lines changed: 1627 additions & 46 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020
contents: read
2121
deployments: write
2222
pull-requests: write
23+
outputs:
24+
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
2325
steps:
2426
- uses: actions/checkout@v6.0.2
2527
- name: Setup Node.js environment
@@ -53,6 +55,7 @@ jobs:
5355
env:
5456
VITE_DEPLOYMENT_URL: ${{ env.VITE_DEPLOYMENT_URL }}
5557
- name: Deploy
58+
if: ${{ github.actor != 'dependabot[bot]' }}
5659
id: deploy
5760
uses: cloudflare/wrangler-action@v3
5861
with:
@@ -64,6 +67,7 @@ jobs:
6467
env:
6568
FORCE_COLOR: 0
6669
- name: Comment PR with deployment link
70+
if: ${{ github.actor != 'dependabot[bot]' }}
6771
uses: marocchino/sticky-pull-request-comment@v2
6872
with:
6973
recreate: true
@@ -74,3 +78,28 @@ jobs:
7478
Deployment Environment: ${{ steps.deploy.outputs.pages-environment }}
7579
7680
${{ steps.deploy.outputs.command-output }}
81+
82+
e2e:
83+
runs-on: ubuntu-latest
84+
name: E2E Tests
85+
needs: deploy
86+
if: ${{ github.actor != 'dependabot[bot]' }}
87+
steps:
88+
- uses: actions/checkout@v6.0.2
89+
- name: Setup Node.js environment
90+
uses: actions/setup-node@v6.3.0
91+
with:
92+
node-version-file: ".node-version"
93+
cache: "yarn"
94+
- name: Enable Corepack
95+
run: corepack enable
96+
- name: Install dependencies
97+
run: yarn install
98+
- name: Build ReScript
99+
run: yarn build:res
100+
- name: Cypress E2E tests
101+
uses: cypress-io/github-action@v7
102+
with:
103+
install: false
104+
browser: chrome
105+
config: baseUrl=${{ needs.deploy.outputs.deployment-url }}

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ functions/**/*.mjs
5050
functions/**/*.jsx
5151
__tests__/**/*.mjs
5252
__tests__/**/*.jsx
53+
e2e/**/*.mjs
54+
e2e/**/*.jsx
5355
!_shims.mjs
5456
!_shims.jsx
5557

@@ -72,4 +74,4 @@ _scripts
7274

7375
# Vitest screenshots
7476
!__tests__/__screenshots__/**/*
75-
.vitest-attachments
77+
.vitest-attachments

app/routes.res

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,20 @@ let blogArticleRoutes =
3333
route(path, "./routes/BlogArticleRoute.jsx", ~options={id: path})
3434
)
3535

36+
let communityRoutes =
37+
MdxFile.scanPaths(~dir="markdown-pages/community", ~alias="community")->Array.map(path =>
38+
route(path, "./routes/CommunityRoute.jsx", ~options={id: path})
39+
)
40+
3641
let mdxRoutes = mdxRoutes("./routes/MdxRoute.jsx")->Array.filter(r =>
3742
!(
3843
r.path
39-
->Option.map(path => path === "blog" || String.startsWith(path, "blog/"))
44+
->Option.map(path =>
45+
path === "blog" ||
46+
String.startsWith(path, "blog/") ||
47+
path === "community" ||
48+
String.startsWith(path, "community/")
49+
)
4050
->Option.getOr(false)
4151
)
4252
)
@@ -57,6 +67,7 @@ let default = [
5767
...stdlibRoutes,
5868
...beltRoutes,
5969
...blogArticleRoutes,
70+
...communityRoutes,
6071
...mdxRoutes,
6172
route("*", "./routes/NotFoundRoute.jsx"),
6273
]

app/routes/CommunityRoute.res

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
type loaderData = {
2+
compiledMdx: CompiledMdx.t,
3+
entries: array<TableOfContents.entry>,
4+
title: string,
5+
description: string,
6+
filePath: string,
7+
categories: array<SidebarLayout.Sidebar.Category.t>,
8+
}
9+
10+
let convertToNavItems = (items, rootPath) =>
11+
Array.map(items, (item): SidebarLayout.Sidebar.NavItem.t => {
12+
let href = switch item.Mdx.slug {
13+
| Some(slug) => `${rootPath}/${slug}`
14+
| None => rootPath
15+
}
16+
{
17+
name: item.title,
18+
href,
19+
}
20+
})
21+
22+
let getGroup = (groups, groupName): SidebarLayout.Sidebar.Category.t => {
23+
{
24+
name: groupName,
25+
items: groups
26+
->Dict.get(groupName)
27+
->Option.getOr([]),
28+
}
29+
}
30+
31+
let getAllGroups = (groups, groupNames): array<SidebarLayout.Sidebar.Category.t> =>
32+
groupNames->Array.map(item => getGroup(groups, item))
33+
34+
let communityTableOfContents = async () => {
35+
let groups =
36+
(await Mdx.allMdx(~filterByPaths=["markdown-pages/community"]))
37+
->Mdx.filterMdxPages("community")
38+
->Mdx.groupBySection
39+
->Dict.mapValues(values => values->Mdx.sortSection->convertToNavItems("/community"))
40+
41+
getAllGroups(groups, ["Resources"])
42+
}
43+
44+
let loader: ReactRouter.Loader.t<loaderData> = async ({request}) => {
45+
let {pathname} = WebAPI.URL.make(~url=request.url)
46+
let filePath = MdxFile.resolveFilePath(
47+
(pathname :> string),
48+
~dir="markdown-pages/community",
49+
~alias="community",
50+
)
51+
52+
let raw = await Node.Fs.readFile(filePath, "utf-8")
53+
let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw)
54+
55+
let description = switch frontmatter {
56+
| Object(dict) =>
57+
switch dict->Dict.get("description") {
58+
| Some(String(s)) => s
59+
| _ => ""
60+
}
61+
| _ => ""
62+
}
63+
64+
let title = switch frontmatter {
65+
| Object(dict) =>
66+
switch dict->Dict.get("title") {
67+
| Some(String(s)) => s
68+
| _ => ""
69+
}
70+
| _ => ""
71+
}
72+
73+
let compiledMdx = await MdxFile.compileMdx(raw, ~filePath, ~remarkPlugins=Mdx.plugins)
74+
75+
let markdownTree = Mdast.fromMarkdown(raw)
76+
let tocResult = Mdast.toc(markdownTree, {maxDepth: 2})
77+
78+
let headers = Dict.make()
79+
Mdast.reduceHeaders(tocResult.map, headers)
80+
81+
let entries =
82+
headers
83+
->Dict.toArray
84+
->Array.map(((header, url)): TableOfContents.entry => {
85+
header,
86+
href: (url :> string),
87+
})
88+
->Array.slice(~start=2)
89+
90+
let categories = await communityTableOfContents()
91+
92+
{
93+
compiledMdx,
94+
entries,
95+
title: `${title} | ReScript Community`,
96+
description,
97+
filePath,
98+
categories,
99+
}
100+
}
101+
102+
let default = () => {
103+
let {compiledMdx, entries, filePath, categories} = ReactRouter.useLoaderData()
104+
105+
let editHref = `https://github.com/rescript-lang/rescript-lang.org/blob/master/${filePath}`
106+
107+
<>
108+
<CommunityLayout categories entries>
109+
<div className="markdown-body">
110+
<MdxContent compiledMdx />
111+
</div>
112+
<a
113+
href=editHref className="inline text-14 hover:underline text-fire" rel="noopener noreferrer"
114+
>
115+
{React.string("Edit")}
116+
</a>
117+
</CommunityLayout>
118+
</>
119+
}

app/routes/CommunityRoute.resi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
type loaderData = {
2+
compiledMdx: CompiledMdx.t,
3+
entries: array<TableOfContents.entry>,
4+
title: string,
5+
description: string,
6+
filePath: string,
7+
categories: array<SidebarLayout.Sidebar.Category.t>,
8+
}
9+
10+
let loader: ReactRouter.Loader.t<loaderData>
11+
12+
let default: unit => React.element

app/routes/MdxRoute.res

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -115,19 +115,6 @@ let reactTableOfContents = async () => {
115115
categories
116116
}
117117

118-
let communityTableOfContents = async () => {
119-
let groups =
120-
(await allMdx(~filterByPaths=["markdown-pages/community"]))
121-
->filterMdxPages("community")
122-
->groupBySection
123-
->Dict.mapValues(values => values->sortSection->convertToNavItems("/community"))
124-
125-
// these are the categories that appear in the sidebar
126-
let categories: array<SidebarLayout.Sidebar.Category.t> = getAllGroups(groups, ["Resources"])
127-
128-
categories
129-
}
130-
131118
let loader: ReactRouter.Loader.t<loaderData> = async ({request}) => {
132119
let {pathname} = WebAPI.URL.make(~url=request.url)
133120

@@ -163,8 +150,6 @@ let loader: ReactRouter.Loader.t<loaderData> = async ({request}) => {
163150
await manualTableOfContents()
164151
} else if pathname->String.includes("docs/react") {
165152
await reactTableOfContents()
166-
} else if pathname->String.includes("community") {
167-
await communityTableOfContents()
168153
} else {
169154
[]
170155
}
@@ -236,8 +221,6 @@ let loader: ReactRouter.Loader.t<loaderData> = async ({request}) => {
236221
"ReScript React"
237222
} else if path->String.includes("docs/manual") {
238223
"ReScript Language Manual"
239-
} else if path->String.includes("community") {
240-
"ReScript Community"
241224
} else {
242225
"ReScript"
243226
}
@@ -352,10 +335,6 @@ let default = () => {
352335
</>
353336
}
354337
</>
355-
} else if (pathname :> string)->String.includes("community") {
356-
<CommunityLayout categories entries>
357-
<div className="markdown-body"> {component()} </div>
358-
</CommunityLayout>
359338
} else {
360339
switch loaderData.mdxSources {
361340
| Some(mdxSources) =>

cypress.config.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { defineConfig } from "cypress";
2+
3+
export default defineConfig({
4+
allowCypressEnv: false,
5+
retries: {
6+
runMode: 2,
7+
openMode: 0,
8+
},
9+
e2e: {
10+
baseUrl: "http://localhost:8080",
11+
specPattern: "e2e/**/*.cy.jsx",
12+
supportFile: "cypress/support/e2e.js",
13+
video: false,
14+
screenshotOnRunFailure: false,
15+
defaultCommandTimeout: 10000,
16+
pageLoadTimeout: 30000,
17+
},
18+
});

cypress/support/e2e.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const knownHydrationErrors = [
2+
/Hydration failed because the initial UI does not match what was rendered on the server\.?/,
3+
/Text content does not match server-rendered HTML\.?/,
4+
/There was an error while hydrating\.?/,
5+
/Minified React error #418\b/,
6+
/Minified React error #423\b/,
7+
/Minified React error #425\b/,
8+
];
9+
10+
Cypress.on("uncaught:exception", (err) => {
11+
const message = err && err.message ? err.message : "";
12+
const isKnownHydrationError = knownHydrationErrors.some((pattern) =>
13+
pattern.test(message),
14+
);
15+
16+
if (isKnownHydrationError) {
17+
console.warn("Suppressing known React hydration exception in Cypress:", {
18+
message,
19+
error: err,
20+
});
21+
return false;
22+
}
23+
});

0 commit comments

Comments
 (0)