Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add community blog and first blog post #946

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions _blogposts/community/2025-01-01-what-can-i-do-with-rescript.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
author: josh-derocher-vlk
date: "2025-01-01"
previewImg: /static/blog/compiler_release_11_1.jpg
title: What can I do with ReScript?
description: |
Can I use Vite, or Next.js? Is it only for React? Can I use Node or Deno?
---

You've taken a look and ReScript and you want to try it out, but how do you get started? There's the [installation](https://rescript-lang.org/docs/manual/latest/installation) page in the docs,
which is great if you want to set up a new React app using [create-rescript-app](https://github.com/rescript-lang/create-rescript-app). There's instructions on how to add it to an existing project or set it up manually.
But that doesn't really answer the question "Can I use this with X?".

## You can use ReScript anywhere you can use JavaScript
ReScript is just a language that compiles to JavaScript. Unlike other language like [Elm](https://elm-lang.org/) or [PureScript](https://www.purescript.org/) ReScript doesn't have a recommended framework or independent ecosystem, it's just part of the normal JavaScript world.

Here's a really basic example that you can run in Node after compiling:

```res
// index.res
Console.log("Hello")
```

Just run `node index.res.js` and you'll see "Hello" logged to the console. You can import compiled ReScript into any project that could import JavaScript.
If you can use `.js` or `.mjs` files, you can use ReScript. This does mean that languages with different file formats like Vue or Svelte require you to import the compiled JavaScript instead of writing it directly in the `.vue` or `.svelte` files.
But real world projects aren't just JavaScript; they use libraries and frameworks. This is where [bindings](https://rescript-lang.org/docs/manual/latest/external) come into play.
A binding is a way to tell ReScript the types and imports from external JavaScript. You can think of bindings in the same way that you need to create a `*.d.ts` file to add types to a JavaScript library that doesn't use TypeScript.

ReScript has great integration with [React](https://rescript-lang.org/docs/react/latest/introduction) and those bindings are kept up to date by the core team, but that doesn't mean you don't have other options!

## Using existing bindings
While ReScript isn't as large as TypeScript it has a small but growing list of bindings you can find on NPM. The website has a [package explorer](https://rescript-lang.org/packages) you can use to find official and community maintained bindings.
Many major libraries have existing bindings. Here's a small set of what you can find.

- [Node](https://github.com/TheSpyder/rescript-nodejs)
- [Material UI](https://github.com/cca-io/rescript-mui)
- [Bun](https://github.com/zth/rescript-bun)
- [Deno](https://github.com/tsirysndr/rescript-deno)
- [Deno's Fresh](https://github.com/jderochervlk/rescript-fresh)
- [Vitest](https://github.com/cometkim/rescript-vitest)
- [Rxjs](https://github.com/noble-ai/rescript-rxjs)
- [React Helmet](https://github.com/MoOx/rescript-react-helmet)
- [Jotai](https://github.com/Fattafatta/rescript-jotai)
- [Headless UI](https://github.com/cbowling/rescript-headlessui)


## Using libraries and frameworks created for ReScript
Bindings are great if you want to work with libraries written with JavaScript, but there are great options for libraries and frameworks written with ReScript, which means you don't need bindings.

- [ReScript Schema](https://github.com/DZakh/rescript-schema) - The fastest parser in the entire JavaScript ecosystem with a focus on small bundle size and top-notch DX.
- [rescript-relay](https://github.com/zth/rescript-relay) - This is an amazing way to connect React to Relay and GraphQL
- [rescript-rest](https://github.com/DZakh/rescript-rest) - Fully typed RPC-like client, with no need for code generation!
- [rescript-edgedb](https://github.com/zth/rescript-edgedb) - Use EdgeDB fully type safe in ReScript. Embed EdgeQL right in your ReScript source code.
- [ResX](https://github.com/zth/res-x) - A ReScript framework for building server-driven web sites and applications.

## Creating your own bindings
At some point you will probably have to use a library that doesn't have bindings available. Asking on the [forum](https://forum.rescript-lang.org/) is a great place to start. Someone else might have bindings already in a project that they just haven't published to NPM.
You can also get help and guidance on how to write bindings for what you need. Usually you can figure out what you need from looking at a libraries official docs.
You don't need to write bindings for an entire library, or even for all of a functions arguments. Just write what you need as you go.

Let's take a look at the `format` function from [date-fns](https://date-fns.org/). We can see the [arguments in the docs](https://date-fns.org/v4.1.0/docs/format#arguments), and how it should be imported and used.
```res
// type signature
function format(
date: string | number | Date,
formatStr: string,
options?: FormatOptions
): string

// how it's imported
import { format } from "date-fns";

// how it's used
const result = format(new Date(2014, 1, 11), 'MM/dd/yyyy')
```

That's all we need to know to write bindings to use this function in ReScript.
The first thing we need to figure out is how to handle the type for what `date-fns` considers to be a `date`, which is `Date | string | number`. In ReScript things can't just be of different types like they can in JavaScript or TypeScript. There are a couple options here; you can make a function for each type such as `formatString` and `formatDate`, or you can create a [variant type](https://rescript-lang.org/docs/manual/latest/variant) to map to the possible input types.
Creating a function for each type is simpler, and it's most likely how you will use the library in your project. You probably have a standard type for Dates already. We'll also need a type for `FormatDateOptions` in case we want to pass options. We'll use [labled arguments](https://rescript-lang.org/docs/manual/latest/function#labeled-arguments) for our binding.
```res
// DateFns.res - you might want to put this in a folder called "bindings" or "external"
type formatDateOptions // we're not even going to add anything to this yet until we need something

@module("date-fns") // this is the import path for the module
external formatString: (
~date: string, // the date string
~formatStr: string, // how we want it formatted
~options: formatDateOptions=?, // =? means the argument is optional
) => string = "format" // "format" is the name of the function we are importing from the module
```

Now we can use the function!
<CodeTab labels={["ReScript", "JS Output"]}>
```res
let formattedDate = DateFns.formatString(~date="2021-09-01", ~formatStr="MMMM dd, yyyy")
```

```js
import * as DateFns from "date-fns";

var formattedDate = DateFns.format("2021-09-01", "MMMM dd, yyyy");
```
</CodeTab>

If we need to use `FormatDateOptions` we can add to our type definition as needed. The first option is `firstWeekContainsDate` which can either be `1` or `4`.
Here's how we could write bindings for that.
```res
@unboxed
type firstWeekContainsDate =
| @as(1) One
| @as(4) Four

type formatDateOptions = {firstWeekContainsDate: firstWeekContainsDate}
```

And when we use it it will output either `1` or `4`.
<CodeTab labels={["ReScript", "JS Output"]}>
```res
let formattedDate = formatString(
~date="2021-09-01",
~formatStr="MMMM dd, yyyy",
~options={firstWeekContainsDate: Four},
)
```

```js
import * as DateFns from "date-fns";

var formattedDate = DateFns.format("2021-09-01", "MMMM dd, yyyy", {
firstWeekContainsDate: 4
});
```
</CodeTab>

You can write new bindings and extend existing types as you need.

## How can I get started?
You can [follow this guide](https://rescript-lang.org/docs/manual/v11.0.0/converting-from-js) to add ReScript to an existing JavaScript project to get a feel for how the language works and interacts with JavaScript.
The forum is also a great place to ask questions! Feel free to drop by and ask how to get started with a specific framework or project that you want to work on,
and you'll probably get great advice and information from users who have already used ReScript for something similar.

Happy coding!
7 changes: 7 additions & 0 deletions pages/blog/community.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import BlogRes from "src/Blog.mjs";

export { getStaticProps_Community as getStaticProps } from "src/Blog.mjs";

export default function Blog(props) {
return <BlogRes {...props} />
}
3 changes: 2 additions & 1 deletion pages/community/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ News are broadcasted on this site's blog, on Bluesky and X. Some extra, less imp

## Articles

- [Getting rid of your dead code in ReScript](https://dev.to/zth/getting-rid-of-your-dead-code-in-rescript-3mba)
- [Community Blog](https://rescript-lang.org/blog/community)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make that link relative?

- [Getting rid of your dead code in ReScript](https://dev.to/zth/getting-rid-of-your-dead-code-in-rescript-3mba)
- [Speeding up ReScript compilation using interface files](https://dev.to/zth/speeding-up-rescript-compilation-using-interface-files-4fgn)
- Articles in [awesome-rescript](https://github.com/fhammerschmidt/awesome-rescript#readme)

Expand Down
26 changes: 20 additions & 6 deletions src/Blog.res
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,14 @@ module Badge = {
}

type category =
| /** Actually only unarchived */ All
| Official
| Community
| Archived

module CategorySelector = {
@react.component
let make = (~selected: category) => {
let tabs = [All, Archived]
let tabs = [Official, Community, Archived]

<div className="text-16 w-full flex items-center justify-between text-gray-60">
{tabs
Expand All @@ -56,7 +57,8 @@ module CategorySelector = {
let isActive = selected == tab
let text = (tab :> string)
let href = switch tab {
| All => "/blog"
| Official => "/blog"
| Community => "/blog/community"
| Archived => "/blog/archived"
}
let className =
Expand Down Expand Up @@ -170,7 +172,10 @@ module FeatureCard = {
<div>
<a
className="hover:text-gray-60"
href={"https://x.com/" ++ author.xHandle}
href={switch author.social {
| X(handle) => "https://x.com/" ++ handle
| Bluesky(handle) => "https://bsky.app/profile/" ++ handle
}}
rel="noopener noreferrer">
{React.string(author.fullname)}
</a>
Expand Down Expand Up @@ -297,17 +302,26 @@ let default = (props: props): React.element => {
let getStaticProps_All: Next.GetStaticProps.t<props, params> = async _ctx => {
let props = {
posts: BlogApi.getLivePosts(),
category: All,
category: Official,
}

{"props": props}
}

let getStaticProps_Archived: Next.GetStaticProps.t<props, params> = async _ctx => {
let props = {
posts: BlogApi.getArchivedPosts(),
posts: BlogApi.getSpecialPosts("archive"),
category: Archived,
}

{"props": props}
}

let getStaticProps_Community: Next.GetStaticProps.t<props, params> = async _ctx => {
let props = {
posts: BlogApi.getSpecialPosts("community"),
category: Community,
}

{"props": props}
}
3 changes: 2 additions & 1 deletion src/Blog.resi
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ let defaultPreviewImg: string
type params
type props

type category = All | Archived
type category = Official | Community | Archived

let default: props => React.element

let getStaticProps_All: Next.GetStaticProps.t<props, params>
let getStaticProps_Archived: Next.GetStaticProps.t<props, params>
let getStaticProps_Community: Next.GetStaticProps.t<props, params>
33 changes: 31 additions & 2 deletions src/BlogArticle.res
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ module AuthorBox = {
<div className="w-10 h-10 bg-berry-40 block rounded-full mr-3"> authorImg </div>
<div className="body-sm">
<a
href={"https://x.com/" ++ author.xHandle}
href={switch author.social {
| X(handle) => "https://x.com/" ++ handle
| Bluesky(handle) => "https://bsky.app/profile/" ++ handle
}}
className="hover:text-gray-80"
rel="noopener noreferrer">
{React.string(author.fullname)}
Expand All @@ -60,6 +63,7 @@ module BlogHeader = {
~category: option<string>=?,
~description: option<string>,
~articleImg: option<string>,
~originalSrc: option<(string, string)>,
) => {
let date = DateStr.toDate(date)

Expand Down Expand Up @@ -88,6 +92,17 @@ module BlogHeader = {
</div>
}
)}
{switch originalSrc {
| None => React.null
| Some("", _) => React.null
| Some(_, "") => React.null
| Some(url, title) =>
<div className="mt-1 mb-8">
<a className="body-sm no-underline text-fire hover:underline" href=url>
{React.string(`Originally posted on ${title}`)}
</a>
</div>
}}
<div className="flex flex-col md:flex-row mb-12">
{Array.map(authors, author =>
<div
Expand Down Expand Up @@ -147,7 +162,17 @@ let default = (props: props) => {
: React.null

let content = switch fm {
| Ok({date, author, co_authors, title, description, articleImg, previewImg}) =>
| Ok({
date,
author,
co_authors,
title,
description,
articleImg,
previewImg,
originalSrc,
originalSrcUrl,
}) =>
<div className="w-full">
<Meta
siteName="ReScript Blog"
Expand All @@ -163,6 +188,10 @@ let default = (props: props) => {
title
description={description->Null.toOption}
articleImg={articleImg->Null.toOption}
originalSrc={switch (originalSrcUrl->Null.toOption, originalSrc->Null.toOption) {
| (Some(url), Some(title)) => Some(url, title)
| _ => None
}}
/>
</div>
<div className="flex justify-center">
Expand Down
32 changes: 23 additions & 9 deletions src/common/BlogApi.res
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type post = {
}

let blogPathToSlug = path => {
path->String.replaceRegExp(%re(`/^(archive\/)?\d\d\d\d-\d\d-\d\d-(.+)\.mdx$/`), "$2")
path->String.replaceRegExp(%re(`/^(archive|community\/)?\d\d\d\d-\d\d-\d\d-(.+)\.mdx$/`), "$2")
}

let mdxFiles = dir => {
Expand All @@ -49,6 +49,7 @@ let mdxFiles = dir => {
let getAllPosts = () => {
let postsDirectory = Node.Path.join2(Node.Process.cwd(), "_blogposts")
let archivedPostsDirectory = Node.Path.join2(postsDirectory, "archive")
let communityPostsDirectory = Node.Path.join2(postsDirectory, "community")

let nonArchivedPosts = mdxFiles(postsDirectory)->Array.map(path => {
let {GrayMatter.data: data} =
Expand Down Expand Up @@ -76,7 +77,20 @@ let getAllPosts = () => {
}
})

Array.concat(nonArchivedPosts, archivedPosts)->Array.toSorted((a, b) =>
let communityPosts = mdxFiles(communityPostsDirectory)->Array.map(path => {
let {GrayMatter.data: data} =
Node.Path.join2(communityPostsDirectory, path)->Node.Fs.readFileSync->GrayMatter.matter
switch BlogFrontmatter.decode(data) {
| Error(msg) => Exn.raiseError(msg)
| Ok(d) => {
path: Node.Path.join2("community", path),
frontmatter: d,
archived: false,
}
}
})

Array.concatMany(nonArchivedPosts, [archivedPosts, communityPosts])->Array.toSorted((a, b) =>
String.compare(Node.Path.basename(b.path), Node.Path.basename(a.path))
)
}
Expand All @@ -102,24 +116,24 @@ let getLivePosts = () => {
)
}

let getArchivedPosts = () => {
let getSpecialPosts = directory => {
let postsDirectory = Node.Path.join2(Node.Process.cwd(), "_blogposts")
let archivedPostsDirectory = Node.Path.join2(postsDirectory, "archive")
let specialPostsDirectory = Node.Path.join2(postsDirectory, directory)

let archivedPosts = mdxFiles(archivedPostsDirectory)->Array.map(path => {
let specialPosts = mdxFiles(specialPostsDirectory)->Array.map(path => {
let {GrayMatter.data: data} =
Node.Path.join2(archivedPostsDirectory, path)->Node.Fs.readFileSync->GrayMatter.matter
Node.Path.join2(specialPostsDirectory, path)->Node.Fs.readFileSync->GrayMatter.matter
switch BlogFrontmatter.decode(data) {
| Error(msg) => Exn.raiseError(msg)
| Ok(d) => {
path: Node.Path.join2("archive", path),
path: Node.Path.join2(directory, path),
frontmatter: d,
archived: true,
archived: directory === "archive",
}
}
})

archivedPosts->Array.toSorted((a, b) =>
specialPosts->Array.toSorted((a, b) =>
String.compare(Node.Path.basename(b.path), Node.Path.basename(a.path))
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/common/BlogApi.resi
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type post = {

let getAllPosts: unit => array<post>
let getLivePosts: unit => array<post>
let getArchivedPosts: unit => array<post>
let getSpecialPosts: string => array<post>
let blogPathToSlug: string => string

module RssFeed: {
Expand Down
Loading
Loading