├── .github ├── FUNDING.yml └── workflows │ └── node.yml ├── .gitignore ├── HISTORY.md ├── MIT-LICENSE ├── README.md ├── bsconfig.json ├── package.json ├── server └── server.mjs ├── src ├── App.res ├── App.resi ├── ErrorPage.res ├── ErrorPage.resi ├── Footer.res ├── Footer.resi ├── Header.res ├── Header.resi ├── Home.res ├── Home.resi ├── Robots.res ├── Robots.resi ├── index.html └── shared │ ├── CssReset.res │ ├── CssReset.resi │ ├── Emotion.res │ ├── Head.res │ ├── Head.resi │ ├── Link.res │ ├── Link.resi │ ├── Router.res │ ├── Router.resi │ ├── Spacer.res │ └── Spacer.resi ├── statics └── robots.txt ├── test ├── Home__test.res ├── Robots__test.res └── utils │ ├── Assert.res │ └── ReactTest.res ├── webpack.config.js └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ["bloodyowl"] 4 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Install 28 | run: yarn 29 | 30 | - name: Build 31 | run: yarn build 32 | env: 33 | NODE_ENV: production 34 | 35 | - name: Test 36 | run: yarn test 37 | 38 | - name: Bundle 39 | run: yarn bundle 40 | env: 41 | NODE_ENV: production 42 | PUBLIC_PATH: ${{ secrets.PUBLIC_PATH }} 43 | 44 | - name: Deploy 45 | uses: peaceiris/actions-gh-pages@v3 46 | # remove the following line to host your project on GitHub 47 | if: ${{ github.repository == 'bloodyowl/rescript-react-starter-kit'}} 48 | with: 49 | github_token: ${{ secrets.GITHUB_TOKEN }} 50 | publish_dir: ./build 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DS_Store? 3 | node_modules/ 4 | lib/ 5 | .bsb.lock 6 | .merlin 7 | build/ 8 | src/**/*.mjs 9 | test/**/*.mjs -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 0.7.2 2 | 3 | Changes: 4 | 5 | - Externalize dev server features (de695c6) 6 | 7 | ## 0.7.1 8 | 9 | Changes: 10 | 11 | - Update ReScript CLI (280126b) 12 | - Improve dev server (87db62f) 13 | 14 | ## 0.7.0 15 | 16 | Changes: 17 | 18 | - Move to ReScript CLI (4d401fe) 19 | 20 | ## 0.6.0 21 | 22 | Features: 23 | 24 | - Add support for title/meta management (77b83cb) 25 | 26 | ## 0.5.0 27 | 28 | Features: 29 | 30 | - Handle publicPaths automatically (ceaff4e) 31 | 32 | ## 0.4.0 33 | 34 | Features: 35 | 36 | - Add live reload (c547412) 37 | 38 | ## 0.3.0 39 | 40 | Features: 41 | 42 | - Add useful buidling blocks like `Link` & `Spacer` (e4ec015) 43 | 44 | Changes: 45 | 46 | - Better docs (2523d24) 47 | 48 | ## 0.2.0 49 | 50 | Changes: 51 | 52 | - Auto open Belt for convenience (7207b67) 53 | 54 | ## 0.1.1 55 | 56 | Features: 57 | 58 | - Add bundle command (5328460) 59 | 60 | ## 0.1.0 61 | 62 | Initial version -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016-2018 Various Authors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReScript React Starter Kit 2 | 3 | > An opinionated starter kit for ReScript React 4 | 5 | Screen Shot 2021-02-27 at 23 45 09 6 | 7 | ## What's inside 8 | 9 | ### Familiar standard library 10 | 11 | The configuration automatically gives you [Belt](https://rescript-lang.org/docs/manual/latest/api/belt) and [ReScriptJs.Js](https://github.com/bloodyowl/rescript-js) in scope. 12 | 13 | This makes your code always default to JavaScript APIs if available, while giving you good manipulation functions for ReScript-specific types (like `Option` & `Result`) 14 | 15 | This means that by default, the following code: 16 | 17 | ```rescript 18 | let x = [1, 2, 3] 19 | ->Array.map(x => x * 2) 20 | ->Array.forEach(Console.log) 21 | ``` 22 | 23 | will compile to the following JS (no additional runtime cost!): 24 | 25 | ```js 26 | [1, 2, 3] 27 | .map(function (x) { 28 | return x << 1; 29 | }) 30 | .forEach(function (prim) { 31 | console.log(prim); 32 | }); 33 | ``` 34 | 35 | If you need a specific data-structure from Belt, you can prefix with `Belt`'s scope: 36 | 37 | ```rescript 38 | let x = Belt.Map.String.fromArray([("a", 1), ("b", 2)]) 39 | ``` 40 | 41 | ### Ready-to-go requests 42 | 43 | This starter kit gives you three building blocks to handle API calls from the get go. 44 | 45 | #### AsyncData 46 | 47 | [AsyncData](https://github.com/bloodyowl/rescript-asyncdata) is a great way to represent asynchronous data in React component state. It's a variant type that can be either `NotAsked`, `Loading` or `Done(payload)`, leaving no room for the errors you get when managing those in different state cells. 48 | 49 | #### Future 50 | 51 | Promises don't play really well with React's effect cancellation model, [Future](https://github.com/bloodyowl/rescript-future) gives you a performant equivalent that has built-in cancellation and leaves error management to the [Result](https://rescript-lang.org/docs/manual/latest/api/belt/result) type. 52 | 53 | #### Request 54 | 55 | [Request](https://github.com/bloodyowl/rescript-request) gives you a simple API to perform API calls in a way that's easy to store in React component state. 56 | 57 | ### Dev server 58 | 59 | Once your project grows, having the compiler output files and webpack watching it can lead to long waiting times. Here, the development server waits for BuckleScript to be ready before it triggers a compilation. 60 | 61 | The dev server supports basic **live reload**. 62 | 63 | ### Testing library 64 | 65 | With [ReScriptTest](https://github.com/bloodyowl/rescript-test), you get a light testing framework that plays nicely with React & lets you mock HTTP call responses. 66 | 67 | The assertion part is on your side, the library simply runs and renders the tests. 68 | 69 | ```rescript 70 | open ReactTest 71 | 72 | testWithReact("Robots renders", container => { 73 | let (future, resolve) = Deferred.make() 74 | 75 | let fetchRobotsTxt = () => future 76 | 77 | act(() => ReactDOM.render(, container)) 78 | Assert.elementContains(container, "Loading") 79 | 80 | act(() => resolve(Ok({ok: true, status: 200, response: Some("My mock response")}))) 81 | 82 | Assert.elementContains(container, "My mock response") 83 | }) 84 | ``` 85 | 86 | Check the example output in [this repo's GitHub Actions](https://github.com/bloodyowl/rescript-react-starter-kit/actions) 87 | 88 | ### Styling with Emotion 89 | 90 | With [some zero-cost bindings to Emotion](https://github.com/bloodyowl/rescript-react-starter-kit/blob/main/src/shared/Emotion.res), you get CSS-in-ReScript right away. 91 | 92 | ```rescript 93 | module Styles = { 94 | open Emotion 95 | let actionButton = css({ 96 | "borderStyle": "none", 97 | "background": "hotpink", 98 | "fontFamily": "inherit", 99 | "color": "#fff", 100 | "fontSize": 20, 101 | "padding": 10, 102 | "cursor": "pointer", 103 | "borderRadius": 10, 104 | "alignSelf": "center", 105 | }) 106 | let disabledButton = cx([actionButton, css({"opacity": "0.3"})]) 107 | } 108 | ``` 109 | 110 | ## Routing 111 | 112 | Provide a `PUBLIC_PATH` environment variable (defaults to `/`), the boilerplate takes care of the rest. Manage your routing using the `Router` & `` modules. 113 | 114 | ## Titles & metadata 115 | 116 | Call `` with the metadata you like for a given route, this binds to [react-helmet](https://github.com/nfl/react-helmet). 117 | 118 | ## Getting started 119 | 120 | ```console 121 | $ yarn 122 | $ yarn start 123 | # And in a second terminal tab 124 | $ yarn server 125 | ``` 126 | 127 | ## Commands 128 | 129 | ### yarn start 130 | 131 | Starts ReScript compiler in watch mode 132 | 133 | ### yarn server 134 | 135 | Starts the development server 136 | 137 | ### yarn build 138 | 139 | Builds the project 140 | 141 | ### yarn bundle 142 | 143 | Bundles the project in `build` 144 | 145 | ### yarn test 146 | 147 | Runs the test suite 148 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rescript-react-starter-kit", 3 | "reason": { 4 | "react-jsx": 3 5 | }, 6 | "sources": [ 7 | { 8 | "dir": "src", 9 | "subdirs": true 10 | }, 11 | { 12 | "dir": "test", 13 | "subdirs": true, 14 | "type": "dev" 15 | } 16 | ], 17 | "package-specs": [ 18 | { 19 | "module": "es6", 20 | "in-source": true 21 | } 22 | ], 23 | "suffix": ".mjs", 24 | "bs-dependencies": [ 25 | "@rescript/react", 26 | "rescript-future", 27 | "rescript-request", 28 | "rescript-asyncdata", 29 | "@ryyppy/rescript-promise", 30 | "rescript-js" 31 | ], 32 | "bs-dev-dependencies": ["rescript-test"], 33 | "bsc-flags": ["-open Belt", "-open ReScriptJs__Js"] 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rescript-react-starter-kit", 3 | "version": "0.7.2", 4 | "description": "A starter kit for ReScript React applications", 5 | "main": "src/App.bs.js", 6 | "repository": "git@github.com:bloodyowl/rescript-react-starter-kit.git", 7 | "author": "bloodyowl ", 8 | "license": "MIT", 9 | "private": false, 10 | "engines": { 11 | "node": ">=14.0.0" 12 | }, 13 | "scripts": { 14 | "start": "rescript build -with-deps -w -ws 9999", 15 | "build": "rescript", 16 | "bundle": "webpack", 17 | "server": "node server/server.mjs", 18 | "clean": "rescript clean", 19 | "test": "retest --with-dom test/*_test.mjs" 20 | }, 21 | "devDependencies": { 22 | "chalk": "^4.1.0", 23 | "copy-webpack-plugin": "^7.0.0", 24 | "etag": "^1.8.1", 25 | "express": "^4.17.1", 26 | "html-webpack-plugin": "^5.2.0", 27 | "mime": "^2.5.2", 28 | "rescript": "^9.1.4", 29 | "rescript-devserver-tools": "^1.0.9", 30 | "webpack": "^5.24.2", 31 | "webpack-cli": "^4.5.0" 32 | }, 33 | "dependencies": { 34 | "@emotion/css": "^11.1.3", 35 | "@rescript/react": "^0.10.1", 36 | "react": "^17.0.1", 37 | "react-dom": "^17.0.1", 38 | "react-helmet": "^6.1.0", 39 | "rescript-asyncdata": "^2.0.0", 40 | "rescript-future": "^2.0.0", 41 | "rescript-js": "^0.5.6", 42 | "rescript-request": "^3.0.1", 43 | "rescript-test": "^3.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/server.mjs: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import path from "path"; 3 | import chalk from "chalk"; 4 | import etag from "etag"; 5 | import mime from "mime"; 6 | import webpack from "webpack"; 7 | 8 | import config from "../webpack.config.js"; 9 | 10 | let fs = await import("fs"); 11 | 12 | let { name } = JSON.parse( 13 | fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8") 14 | ); 15 | 16 | let app = express(); 17 | 18 | app.disable("x-powered-by"); 19 | 20 | import createRescriptDevserverTools from "rescript-devserver-tools"; 21 | 22 | let { virtualFs, middleware, getLiveReloadAppendix } = 23 | createRescriptDevserverTools(webpack(config)); 24 | 25 | fs = virtualFs; 26 | 27 | app.use(middleware); 28 | 29 | let publicPath = process.env.PUBLIC_PATH || "/"; 30 | 31 | app.use(publicPath, (req, res, next) => { 32 | let url = req.path; 33 | let filePath = url.startsWith("/") ? url.slice(1) : url; 34 | let normalizedFilePath = path.join(process.cwd(), "build", filePath); 35 | fs.stat(normalizedFilePath, (err, stat) => { 36 | if (err) { 37 | next(); 38 | } else { 39 | if (stat.isFile()) { 40 | fs.readFile(normalizedFilePath, (err, data) => { 41 | if (err) { 42 | next(); 43 | } else { 44 | setMime(filePath, res); 45 | res.status(200).set("Etag", etag(data)).end(data); 46 | } 47 | }); 48 | } else { 49 | next(); 50 | } 51 | } 52 | }); 53 | }); 54 | 55 | function setMime(path, res) { 56 | if (res.getHeader("Content-Type")) { 57 | return; 58 | } 59 | let type = mime.getType(path); 60 | if (!type) { 61 | return; 62 | } 63 | res.setHeader("Content-Type", type); 64 | } 65 | 66 | function readFileIfExists(filePath, req, res, appendix) { 67 | fs.stat(filePath, (err, data) => { 68 | if (err) { 69 | res.status(404).end(""); 70 | } else { 71 | fs.readFile(filePath, (err, data) => { 72 | if (err) { 73 | res.status(404).end(""); 74 | } else { 75 | setMime(filePath, res); 76 | res 77 | .status(200) 78 | .set("Etag", etag(data)) 79 | .end(appendix ? data + appendix : data); 80 | } 81 | }); 82 | } 83 | }); 84 | } 85 | 86 | app.get(`${publicPath}*`, (req, res) => { 87 | res.set("Cache-control", `public, max-age=0`); 88 | readFileIfExists( 89 | path.join(process.cwd(), "build/index.html"), 90 | req, 91 | res, 92 | getLiveReloadAppendix() 93 | ); 94 | }); 95 | 96 | let port = process.env.PORT || 3000; 97 | 98 | app.listen(port); 99 | 100 | console.log(`${chalk.white("---")}`); 101 | console.log(`${chalk.green(`${name}`)}`); 102 | console.log(`${chalk.white("---")}`); 103 | console.log(`${chalk.cyan("Development server started")}`); 104 | console.log(``); 105 | console.log(`${chalk.magenta("URL")} -> http://localhost:${port}${publicPath}`); 106 | console.log(``); 107 | -------------------------------------------------------------------------------- /src/App.res: -------------------------------------------------------------------------------- 1 | Emotion.injectGlobal(` 2 | html { 3 | padding: 0; 4 | margin: 0; 5 | height: -webkit-fill-available; 6 | font-family: sans-serif; 7 | } 8 | body { 9 | padding: 0; 10 | margin: 0; 11 | display: flex; 12 | flex-direction: column; 13 | min-height: 100vh; 14 | min-height: -webkit-fill-available; 15 | } 16 | #root { 17 | display: flex; 18 | flex-direction: column; 19 | flex-grow: 1 20 | }`) 21 | 22 | module App = { 23 | @react.component 24 | let make = () => { 25 | let url = Router.useUrl() 26 | 27 | React.useEffect1(() => { 28 | let () = window["scrollTo"](. 0, 0) 29 | None 30 | }, [url.path]) 31 | 32 | <> 33 | 36 |
37 | {switch url.path { 38 | | list{} => 39 | | list{"robots"} => 40 | | _ => 41 | }} 42 |