├── .gitignore ├── README.md ├── app ├── entry.client.res ├── entry.server.res ├── res-routes │ ├── gists.res │ └── index.res └── root.res ├── bsconfig.json ├── jsconfig.json ├── package-lock.json ├── package.json ├── patches └── @remix-run+dev+1.1.1.patch ├── public └── favicon.ico └── remix.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | 8 | /app/**/*.jsx 9 | /app/**/*.js 10 | /lib/ 11 | .bsb.lock 12 | .merlin 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReScript Remix Template 2 | 3 | A starter template for writing Remix apps in [ReScript](https://rescript-lang.org/). 4 | 5 | ## Development 6 | 7 | ``` 8 | npm i 9 | npm run dev:rescript 10 | ``` 11 | 12 | And then in another terminal: 13 | 14 | ``` 15 | npm run dev:remix 16 | ``` 17 | 18 | ## Building 19 | 20 | ``` 21 | npm run build 22 | ``` 23 | -------------------------------------------------------------------------------- /app/entry.client.res: -------------------------------------------------------------------------------- 1 | @val external document: Dom.element = "document" 2 | 3 | ReactDOM.hydrate( , document) 4 | -------------------------------------------------------------------------------- /app/entry.server.res: -------------------------------------------------------------------------------- 1 | module ResponseInit = { 2 | type t 3 | 4 | external make: {..} => t = "%identity" 5 | } 6 | 7 | // TODO: Swap out for Webapi.Fetch.Response when it supports construction 8 | // See https://github.com/tinymce/rescript-webapi/issues/63 9 | @new 10 | external makeResponse: (Webapi.Fetch.BodyInit.t, ResponseInit.t) => Webapi.Fetch.Response.t = 11 | "Response" 12 | 13 | let default = (request, responseStatusCode, responseHeaders, remixContext) => { 14 | open Webapi 15 | 16 | let markup = ReactDOMServer.renderToString( 17 | Fetch.Request.url} />, 18 | ) 19 | 20 | responseHeaders->Fetch.Headers.set("Content-Type", "text/html") 21 | 22 | makeResponse( 23 | Fetch.BodyInit.make("" ++ markup), 24 | ResponseInit.make({ 25 | "status": responseStatusCode, 26 | "headers": responseHeaders, 27 | }), 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /app/res-routes/gists.res: -------------------------------------------------------------------------------- 1 | type gist = { 2 | html_url: string, 3 | id: string, 4 | } 5 | 6 | type loaderData = array 7 | 8 | // Explicit return type for type safety 9 | let loader = (): Promise.t => 10 | Webapi.Fetch.fetch("https://api.github.com/gists") 11 | ->Promise.then(res => res->Webapi.Fetch.Response.json) 12 | ->Promise.thenResolve(json => 13 | json 14 | ->Js.Json.decodeArray 15 | ->Belt.Option.map(gists => 16 | gists->Js.Array2.map(gist => { 17 | let gist = gist->Js.Json.decodeObject->Belt.Option.getUnsafe 18 | 19 | { 20 | html_url: gist 21 | ->Js.Dict.get("html_url") 22 | ->Belt.Option.flatMap(Js.Json.decodeString) 23 | ->Belt.Option.getExn, 24 | id: gist 25 | ->Js.Dict.get("id") 26 | ->Belt.Option.flatMap(Js.Json.decodeString) 27 | ->Belt.Option.getExn, 28 | } 29 | }) 30 | ) 31 | ->Belt.Option.getUnsafe 32 | ) 33 | 34 | let default = () => { 35 | let gists: loaderData = Remix.useLoaderData() 36 | 37 | 44 | } 45 | -------------------------------------------------------------------------------- /app/res-routes/index.res: -------------------------------------------------------------------------------- 1 | @react.component 2 | let default = () => 3 |
4 |

{"Welcome to Remix"->React.string}

5 | 22 |
23 | -------------------------------------------------------------------------------- /app/root.res: -------------------------------------------------------------------------------- 1 | let meta = () => 2 | { 3 | "title": "New Remix App", 4 | } 5 | 6 | @react.component 7 | let default = () => 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {if Remix.process["env"]["NODE_ENV"] === "development" { 20 | 21 | } else { 22 | React.null 23 | }} 24 | 25 | 26 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rescript-project-template", 3 | "version": "0.0.1", 4 | "sources": { 5 | "dir": "app", 6 | "subdirs": true 7 | }, 8 | "package-specs": { 9 | "module": "es6", 10 | "in-source": true 11 | }, 12 | "suffix": ".js", 13 | "reason": { 14 | "react-jsx": 3 15 | }, 16 | "bs-dependencies": [ 17 | "@rescript/react", 18 | "rescript-remix", 19 | "rescript-webapi", 20 | "@ryyppy/rescript-promise" 21 | ], 22 | "warnings": { 23 | "error": "+101" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "baseUrl": ".", 5 | "paths": { 6 | "~/*": ["./app/*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "remix-app-template-js", 4 | "description": "", 5 | "license": "", 6 | "scripts": { 7 | "build": "rescript build && remix build", 8 | "dev:remix": "remix dev", 9 | "dev:rescript": "rescript build -w", 10 | "postinstall": "patch-package && remix setup node", 11 | "start": "remix-serve build" 12 | }, 13 | "dependencies": { 14 | "@remix-run/react": "^1.1.1", 15 | "@remix-run/serve": "^1.1.1", 16 | "@rescript/react": "^0.10.3", 17 | "@ryyppy/rescript-promise": "^2.1.0", 18 | "react": "^17.0.2", 19 | "react-dom": "^17.0.2", 20 | "remix": "^1.1.1", 21 | "rescript-remix": "^0.2.1", 22 | "rescript-webapi": "^0.2.0" 23 | }, 24 | "devDependencies": { 25 | "@remix-run/dev": "^1.1.1", 26 | "patch-package": "^6.4.7", 27 | "rescript": "^9.1.4" 28 | }, 29 | "engines": { 30 | "node": ">=14" 31 | }, 32 | "sideEffects": false 33 | } 34 | -------------------------------------------------------------------------------- /patches/@remix-run+dev+1.1.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@remix-run/dev/compiler.js b/node_modules/@remix-run/dev/compiler.js 2 | index 5dc3ee5..d6daef3 100644 3 | --- a/node_modules/@remix-run/dev/compiler.js 4 | +++ b/node_modules/@remix-run/dev/compiler.js 5 | @@ -348,8 +348,10 @@ async function createServerBuild(config, options) { 6 | if (id === "./assets.json" && importer === "") return true; // Mark all bare imports as external. They will be require()'d (or 7 | // imported if ESM) at runtime from node_modules. 8 | 9 | + 10 | if (isBareModuleId(id)) { 11 | let packageName = getNpmPackageName(id); 12 | + if (config.transpileModules.includes(packageName)) return false; 13 | 14 | if (!/\bnode_modules\b/.test(importer) && !module$1.builtinModules.includes(packageName) && !dependencies$1.includes(packageName)) { 15 | options.onWarning(`The path "${id}" is imported in ` + `${path__namespace.relative(process.cwd(), importer)} but ` + `${packageName} is not listed in your package.json dependencies. ` + `Did you forget to install it?`, packageName); 16 | diff --git a/node_modules/@remix-run/dev/config.js b/node_modules/@remix-run/dev/config.js 17 | index 9705888..398f3e6 100644 18 | --- a/node_modules/@remix-run/dev/config.js 19 | +++ b/node_modules/@remix-run/dev/config.js 20 | @@ -135,7 +135,8 @@ async function readConfig(remixRoot, serverMode = serverModes.ServerMode.Product 21 | serverMode, 22 | serverModuleFormat, 23 | serverPlatform, 24 | - mdx 25 | + mdx, 26 | + transpileModules: appConfig.transpileModules || [], 27 | }; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-sherman/rescript-remix-template/45c1a244413064f126d4f9f59ed12cd55cdbf06f/public/favicon.ico -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | const { registerRoutes } = require('rescript-remix/registerRoutes'); 2 | 3 | /** 4 | * @type {import('@remix-run/dev/config').AppConfig} 5 | */ 6 | module.exports = { 7 | appDirectory: "app", 8 | assetsBuildDirectory: "public/build", 9 | publicPath: "/build/", 10 | serverBuildDirectory: "build", 11 | devServerPort: 8002, 12 | ignoredRouteFiles: [".*", "*.res"], 13 | transpileModules: ["rescript"], 14 | routes(defineRoutes) { 15 | return defineRoutes(route => { 16 | registerRoutes(route); 17 | }); 18 | } 19 | }; 20 | --------------------------------------------------------------------------------