├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── MockFs.res └── RouteConventions_spec.res ├── bsconfig.json ├── package-lock.json ├── package.json ├── routing ├── RouteConventions.res └── RouteConventions.resi └── src └── Remix.res /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: PR Checks 2 | on: pull_request 3 | 4 | jobs: 5 | compile-and-test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/setup-node@v2 10 | with: 11 | node-version: '16.x' 12 | registry-url: 'https://registry.npmjs.org' 13 | 14 | - run: npm ci 15 | 16 | - run: npm run build 17 | 18 | - run: npm t 19 | 20 | formatting: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-node@v2 25 | with: 26 | node-version: '16.x' 27 | registry-url: 'https://registry.npmjs.org' 28 | 29 | - run: npm ci 30 | 31 | - run: npm run format-all 32 | 33 | - name: Check for uncommitted changes 34 | id: check-changes 35 | uses: mskri/check-uncommitted-changes-action@v1.0.1 36 | 37 | - name: Print uncommitted changes 38 | if: steps.check-changes.outputs.changes != '' 39 | run: echo "There are uncommitted changes" && git status --porcelain && exit 1 40 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [created] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v2 12 | with: 13 | node-version: '16.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | - run: npm ci 16 | - run: npm publish 17 | env: 18 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | /lib/ 4 | .bsb.lock 5 | .merlin 6 | /registerRoutes.js 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tom Sherman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rescript-remix 2 | 3 | Bindings and helpers for [Remix](https://remix.run/). 4 | 5 | ## Installation 6 | 7 | If you don't already have have a Remix project the easiest way to get started quickly is to use the template found [here](https://github.com/tom-sherman/rescript-remix-template). Otherwise, continue reading this guide if you prefer manual setup or want to understand how the template works. 8 | ### Setup and install 9 | 10 | You should already have a Remix project setup. You can create one by following the [Remix docs](https://remix.run/docs/en/v1/tutorials/blog#creating-the-project). 11 | 12 | Once you have a Remix project, install the necessary dependencies: 13 | 14 | ``` 15 | npm install rescript rescript-remix patch-package @rescript/react rescript-webapi 16 | ``` 17 | 18 |
19 | 🙋 What is each package for? 20 |
27 | 28 | ### Add `bsconfig.json` 29 | 30 | Create a `bsconfig.json` file at the root of your repo. Copy the contents from [the template](https://github.com/tom-sherman/rescript-remix-template/blob/main/bsconfig.json) in and change the "name" to your project name. This can be anything you want but it's recommended to match the one you gave in `package.json` for consistency. 31 | 32 |
33 | 🙋 Why do we need all of these settings? 34 |

You can read about what all the options are for in the ReScript docs. The recommended settings are setup in such a way to be most convenient to ReScript and Remix developers alike while supporting the file-system-based routing of Remix. 35 |

36 | 37 | ### Patch the ReScript compiler 38 | 39 | Copy the patch [from the template](https://github.com/tom-sherman/rescript-remix-template/tree/main/patches) into your project, making sure to place it in the `patches/` directory and matching the file name exactly. The patch in the template works on a specific version of `@remix-run/dev`, you may need to ensure that your version of `@remix-run/dev` matches the one in the template. 40 | 41 | Next, modify (or add) the `postinstall` script in package.json to include the `patch-package` script. 42 | 43 | ```diff 44 | ... 45 | "scripts": { 46 | "build": "rescript build && remix build", 47 | "dev:remix": "remix dev", 48 | "dev:rescript": "rescript build -w", 49 | - "postinstall": "remix setup node", 50 | + "postinstall": "patch-package && remix setup node", 51 | "start": "remix-serve build" 52 | }, 53 | ... 54 | ``` 55 | 56 | Run `npm i` to apply the patch. 57 | 58 |
59 | 🙋 What does the patch do? 60 |

It allows Remix to transpile ES modules (export/import syntax) inside of node_modules. This is important because our ReScript configuration tells the compiler to transpile to ESM, and this includes ReScript dependencies in node_modules eg. this package and the ReScript standard library. 61 |

62 | 63 | ### Enable transpilation of ESM modules 64 | 65 | Add `"rescript"` to the `transpileModules` option in `remix.config.js`. This ensures that our patch installed above will transpile the ReScript standard library. 66 | 67 | ```diff 68 | /** 69 | * @type {import('@remix-run/dev/config').AppConfig} 70 | */ 71 | module.exports = { 72 | appDirectory: "app", 73 | assetsBuildDirectory: "public/build", 74 | publicPath: "/build/", 75 | serverBuildDirectory: "build", 76 | devServerPort: 8002, 77 | ignoredRouteFiles: [".*", "*.res"], 78 | + transpileModules: ["rescript"], 79 | }; 80 | ``` 81 | 82 | You'll need to add more packages to this array whenever you receive a Remix error message that says it failed to compile ESM syntax. This is often the case when you install a new ReScript dependency. 83 | 84 | ### Enable convention based routing for ReScript modules 85 | 86 | Add a `routes` option to `remix.config.js` and inside call the `registerRoutes` function exported by `rescript-remix/registerRoutes`: 87 | 88 | ```diff 89 | + const { registerRoutes } = require('rescript-remix/registerRoutes'); 90 | 91 | /** 92 | * @type {import('@remix-run/dev/config').AppConfig} 93 | */ 94 | module.exports = { 95 | appDirectory: "app", 96 | assetsBuildDirectory: "public/build", 97 | publicPath: "/build/", 98 | serverBuildDirectory: "build", 99 | devServerPort: 8002, 100 | ignoredRouteFiles: [".*", "*.res"], 101 | transpileModules: ["rescript"], 102 | + routes(defineRoutes) { 103 | + return defineRoutes(route => { 104 | + registerRoutes(route); 105 | + }); 106 | + } 107 | }; 108 | ``` 109 | 110 | This allows you to use all of the convention-based routing of Remix with ReScript modules by placing them inside the `app/res-routes` director. See [the usage section](#convention-based-routing) for more details. 111 | 112 | ### (optional) Git ignore compiled JS 113 | 114 | JS is outputted alongside ReScript modules to enable convention based routing. You can opt to not commit these build artifacts by adding the following lines to `.gitignore`: 115 | 116 | ``` 117 | /app/**/*.jsx 118 | /app/**/*.js 119 | ``` 120 | 121 | This step is optional as it's personal preference. Some people prefer to check in the artifacts generated by ReScript to enable teammates that aren't familiar with the language to make quick changes, or to ensure the output from the compiler is as expected. 122 | 123 | ## Adopting into an existing Remix app 124 | 125 | todo 126 | 127 | ## Usage 128 | 129 | todo 130 | 131 | ### Convention based routing 132 | 133 | todo 134 | -------------------------------------------------------------------------------- /__tests__/MockFs.res: -------------------------------------------------------------------------------- 1 | @module external mock: {..} => unit = "mock-fs" 2 | @module external mockWithDict: Js.Dict.t<'a> => unit = "mock-fs" 3 | 4 | @module("mock-fs") external restore: unit => unit = "restore" 5 | -------------------------------------------------------------------------------- /__tests__/RouteConventions_spec.res: -------------------------------------------------------------------------------- 1 | // Disable warning on unused parameter (required by the raw JS) 2 | @@warning("-27") 3 | 4 | open Jest 5 | open Expect 6 | 7 | module Map = Belt.MutableMap.String 8 | 9 | type rec routeDefinition = {file: string, nested: option>} 10 | 11 | let first = (arr: array<'a>): 'a => arr[0] 12 | let last = (arr: array<'a>): 'a => arr[arr->Js.Array2.length - 1] 13 | 14 | module MockRouteDefiner = { 15 | type t = array> 16 | 17 | let make = (): t => [Map.make()] 18 | 19 | let defineChildRoute = (. definer: t, path: string, file: string) => { 20 | definer->last->Map.set(path, {file: file, nested: None}) 21 | } 22 | 23 | let defineParentRoute = (. 24 | definer: t, 25 | path: option, 26 | file: string, 27 | callback: (. unit) => unit, 28 | ) => { 29 | let nestedRoutes = Map.make() 30 | definer 31 | ->last 32 | ->Map.set(path->Belt.Option.getWithDefault(""), {file: file, nested: Some(nestedRoutes)}) 33 | definer->Js.Array2.push(nestedRoutes)->ignore 34 | callback(.) 35 | definer->Js.Array2.pop->ignore 36 | } 37 | 38 | // using raw JS here to model the signature-overloaded `defineRoute` function provided by Remix 39 | let defineRoute = (definer: t) => { 40 | %raw(` 41 | function(path, file, optsOrCallback, opts) { 42 | if (typeof optsOrCallback === "function") { 43 | defineParentRoute(definer, path, file, optsOrCallback) 44 | } else { 45 | defineChildRoute(definer, path, file) 46 | } 47 | } 48 | `) 49 | } 50 | 51 | let routes = (definer: t): Map.t => definer->first 52 | } 53 | 54 | afterEach(() => { 55 | MockFs.restore() 56 | }) 57 | 58 | describe("directory structure to route and view hierarchy", () => { 59 | test("it should map a file into a simple route", () => { 60 | MockFs.mock({"app": {"res-routes": {"blog.js": ""}}}) 61 | 62 | let routeDefiner = MockRouteDefiner.make() 63 | RouteConventions.registerRoutes(routeDefiner->MockRouteDefiner.defineRoute) 64 | 65 | expect(routeDefiner->MockRouteDefiner.routes)->toEqual( 66 | Map.fromArray([("blog", {file: "res-routes/blog.js", nested: None})]), 67 | ) 68 | }) 69 | 70 | test("it should map a deep file into a simple route", () => { 71 | MockFs.mock({"app": {"res-routes": {"blog": {"blog.js": ""}}}}) 72 | 73 | let routeDefiner = MockRouteDefiner.make() 74 | RouteConventions.registerRoutes(routeDefiner->MockRouteDefiner.defineRoute) 75 | 76 | expect(routeDefiner->MockRouteDefiner.routes)->toEqual( 77 | Map.fromArray([("blog/blog", {file: "res-routes/blog/blog.js", nested: None})]), 78 | ) 79 | }) 80 | 81 | test("it should nest routes when a folder and file exist with the same name", () => { 82 | MockFs.mock({"app": {"res-routes": {"blog.js": "", "blog": {"blog.js": ""}}}}) 83 | 84 | let routeDefiner = MockRouteDefiner.make() 85 | RouteConventions.registerRoutes(routeDefiner->MockRouteDefiner.defineRoute) 86 | 87 | expect(routeDefiner->MockRouteDefiner.routes)->toEqual( 88 | Map.fromArray([ 89 | ( 90 | "blog", 91 | { 92 | file: "res-routes/blog.js", 93 | nested: Some( 94 | Map.fromArray([("blog", {file: "res-routes/blog/blog.js", nested: None})]), 95 | ), 96 | }, 97 | ), 98 | ]), 99 | ) 100 | }) 101 | 102 | test("it should not add a route segment when a folder and file start with an underscore", () => { 103 | MockFs.mock({"app": {"res-routes": {"_blog.js": "", "_blog": {"blog.js": ""}}}}) 104 | 105 | let routeDefiner = MockRouteDefiner.make() 106 | RouteConventions.registerRoutes(routeDefiner->MockRouteDefiner.defineRoute) 107 | 108 | expect(routeDefiner->MockRouteDefiner.routes)->toEqual( 109 | Map.fromArray([ 110 | ( 111 | "", 112 | { 113 | file: "res-routes/_blog.js", 114 | nested: Some( 115 | Map.fromArray([("blog", {file: "res-routes/_blog/blog.js", nested: None})]), 116 | ), 117 | }, 118 | ), 119 | ]), 120 | ) 121 | }) 122 | 123 | test("it should ignore non-js files", () => { 124 | MockFs.mock({"app": {"res-routes": {"blog.js": "", "ignoreme.res": ""}}}) 125 | 126 | let routeDefiner = MockRouteDefiner.make() 127 | RouteConventions.registerRoutes(routeDefiner->MockRouteDefiner.defineRoute) 128 | 129 | expect(routeDefiner->MockRouteDefiner.routes)->toEqual( 130 | Map.fromArray([("blog", {file: "res-routes/blog.js", nested: None})]), 131 | ) 132 | }) 133 | }) 134 | 135 | describe("filename to route mappings", () => 136 | [ 137 | ("blog", "blog"), 138 | ("namespaced_blog", "blog"), 139 | ("index", ""), 140 | ("namespaced_index", ""), 141 | ("[]", "*"), 142 | ("namespaced_[]", "*"), 143 | ("[blogId]", ":blogId"), 144 | ("namespaced_[blogId]", ":blogId"), 145 | ("blog[[.]]rss", "blog.rss"), 146 | ("blog[[_]]page", "blog_page"), 147 | ("blog[[_[_]]page", "blog_[_page"), 148 | ("blog[[_]_]]page", "blog_]_page"), 149 | ("blog]page", "blog]page"), 150 | ("namespaced_blog[[.]]rss", "blog.rss"), 151 | ("namespaced_[[_]]_blog", "blog"), 152 | ("blog.about", "blog/about"), 153 | ("namespaced_blog.about", "blog/about"), 154 | ("lots_of_namespaces_blog", "blog"), 155 | ]->Js.Array2.forEach(((input, output)) => 156 | test(`"${input}" -> "${output}"`, () => { 157 | MockFs.mockWithDict( 158 | Js.Dict.fromArray([ 159 | ("app", Js.Dict.fromArray([("res-routes", Js.Dict.fromArray([(`${input}.js`, "")]))])), 160 | ]), 161 | ) 162 | 163 | let routeDefiner = MockRouteDefiner.make() 164 | RouteConventions.registerRoutes(routeDefiner->MockRouteDefiner.defineRoute) 165 | 166 | expect(routeDefiner->MockRouteDefiner.routes)->toEqual( 167 | Map.fromArray([(output, {file: `res-routes/${input}.js`, nested: None})]), 168 | ) 169 | }) 170 | ) 171 | ) 172 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rescript-remix", 3 | "bsc-flags": [ 4 | "-bs-no-version-header" 5 | ], 6 | "sources": [ 7 | { 8 | "dir": "src", 9 | "subdirs": true 10 | }, 11 | { 12 | "dir": "__tests__", 13 | "subdirs": true, 14 | "type": "dev" 15 | }, 16 | { 17 | "dir": "routing", 18 | "subdirs": true, 19 | "type": "dev" 20 | } 21 | ], 22 | "reason": { 23 | "react-jsx": 3 24 | }, 25 | "warnings": { 26 | "number": "+A", 27 | "error": "+A" 28 | }, 29 | "bs-dependencies": [ 30 | "@rescript/react", 31 | "rescript-webapi" 32 | ], 33 | "bs-dev-dependencies": [ 34 | "@glennsl/rescript-jest", 35 | "rescript-nodejs" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rescript-remix", 3 | "version": "0.2.1", 4 | "scripts": { 5 | "build": "rescript", 6 | "postbuild": "cp lib/js/routing/RouteConventions.js registerRoutes.js", 7 | "start": "rescript build -w", 8 | "clean": "rescript clean", 9 | "format-all": "rescript format -all", 10 | "test": "jest", 11 | "prepublishOnly": "npm run build" 12 | }, 13 | "keywords": [ 14 | "rescript", 15 | "remix" 16 | ], 17 | "files": [ 18 | "src", 19 | "bsconfig.json", 20 | "registerRoutes.js" 21 | ], 22 | "author": { 23 | "email": "the.tomsherman@gmail.com", 24 | "name": "Tom Sherman", 25 | "url": "https://github.com/tom-sherman" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/tom-sherman/rescript-remix.git" 30 | }, 31 | "license": "MIT", 32 | "devDependencies": { 33 | "@glennsl/rescript-jest": "^0.9.0", 34 | "@rescript/react": "^0.10.3", 35 | "mock-fs": "^5.1.2", 36 | "rescript": "*", 37 | "rescript-nodejs": "^14.2.0", 38 | "rescript-webapi": "^0.3.2" 39 | }, 40 | "jest": { 41 | "testMatch": [ 42 | "**/*_spec.js" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /routing/RouteConventions.res: -------------------------------------------------------------------------------- 1 | module Map = Belt.MutableMap.String 2 | @module("fs") external statSync: string => NodeJs.Fs.Stats.t = "statSync" 3 | 4 | type defineRoute 5 | type routeOptions = {index: bool} 6 | type defineChildRoute = (. string, string, routeOptions) => unit 7 | type defineParentRoute = (. option, string, unit => unit) => unit 8 | 9 | external toDefineChildRoute: defineRoute => defineChildRoute = "%identity" 10 | external toDefineParentRoute: defineRoute => defineParentRoute = "%identity" 11 | 12 | type rec routeDefinitionNode = { 13 | mutable file: option, 14 | mutable nested: option, 15 | } 16 | and routeDefinition = Map.t 17 | 18 | type segmentAccumulatorState = 19 | Normal | SawOpenBracket | SawCloseBracket | InsideParameter | InsideEscape 20 | type segmentAccumulator = { 21 | segment: string, 22 | state: segmentAccumulatorState, 23 | } 24 | 25 | let filenameToSegment = (name: string): string => { 26 | let segment = (name->Js.String2.split("")->Js.Array2.reduce((acc, char) => 27 | switch (char, acc.state) { 28 | | ("_", Normal) => {...acc, segment: ""} 29 | | (".", Normal) => {...acc, segment: acc.segment ++ "/"} 30 | | ("[", Normal) => {...acc, state: SawOpenBracket} 31 | | ("[", SawOpenBracket) => {...acc, state: InsideEscape} 32 | | ("]", SawOpenBracket) => {segment: acc.segment ++ "*", state: Normal} 33 | | ("]", InsideEscape) => {...acc, state: SawCloseBracket} 34 | | ("]", SawCloseBracket) => {...acc, state: Normal} 35 | | ("]", InsideParameter) => {...acc, state: Normal} 36 | | (_, SawOpenBracket) => {segment: acc.segment ++ ":" ++ char, state: InsideParameter} 37 | | (_, SawCloseBracket) => {segment: acc.segment ++ "]" ++ char, state: InsideEscape} 38 | | (_, Normal) 39 | | (_, InsideEscape) 40 | | (_, InsideParameter) => {...acc, segment: acc.segment ++ char} 41 | } 42 | , {segment: "", state: Normal})).segment 43 | 44 | if name->Js.String2.startsWith("_") { 45 | "_" ++ segment 46 | } else if segment == "index" { 47 | "" 48 | } else { 49 | segment 50 | } 51 | } 52 | 53 | let rec buildRoutesForDir = (path: string) => { 54 | let routes = Map.make() 55 | 56 | let files = NodeJs.Fs.readdirSync(NodeJs.Path.join(["app", path])) 57 | Js.Array2.forEach(files, file => { 58 | let fileInfo = file->NodeJs.Path.parse 59 | let isDirectory = ["app", path, file]->NodeJs.Path.join->statSync->NodeJs.Fs.Stats.isDirectory 60 | 61 | if isDirectory || fileInfo.ext === ".js" { 62 | let segment = (isDirectory ? fileInfo.base : fileInfo.name)->filenameToSegment 63 | let mapping = routes->Map.getWithDefault(segment, {file: None, nested: None}) 64 | 65 | if isDirectory { 66 | mapping.nested = Some(buildRoutesForDir(NodeJs.Path.join([path, segment]))) 67 | } else { 68 | mapping.file = Some(NodeJs.Path.join([path, file])) 69 | } 70 | 71 | routes->Map.set(segment, mapping) 72 | } 73 | }) 74 | 75 | routes 76 | } 77 | 78 | let rec registerBuiltRoutes = ( 79 | routes: routeDefinition, 80 | defineRoute: defineRoute, 81 | ~segments=[], 82 | (), 83 | ) => { 84 | routes->Map.forEach((segment, definition) => 85 | switch (definition.file, definition.nested) { 86 | | (Some(file), None) => 87 | (defineRoute->toDefineChildRoute)(. 88 | segments->Js.Array2.concat([segment])->Js.Array2.joinWith("/"), 89 | file, 90 | {index: segment == ""}, 91 | ) 92 | | (None, Some(nested)) => 93 | registerBuiltRoutes(nested, defineRoute, ~segments=segments->Js.Array2.concat([segment]), ()) 94 | | (Some(file), Some(nested)) => 95 | let isPathlessRoute = segment->Js.String2.startsWith("_") 96 | (defineRoute->toDefineParentRoute)(. 97 | isPathlessRoute 98 | ? None 99 | : Some(segments->Js.Array2.concat([segment])->Js.Array2.joinWith("/")), 100 | file, 101 | () => registerBuiltRoutes(nested, defineRoute, ()), 102 | ) 103 | | (None, None) => Js.Exn.raiseError("Invariant error") 104 | } 105 | ) 106 | } 107 | 108 | let registerRoutes = (defineRoute: defineRoute) => { 109 | buildRoutesForDir("res-routes")->registerBuiltRoutes(defineRoute, ()) 110 | } 111 | -------------------------------------------------------------------------------- /routing/RouteConventions.resi: -------------------------------------------------------------------------------- 1 | type defineRoute 2 | let registerRoutes: defineRoute => unit 3 | -------------------------------------------------------------------------------- /src/Remix.res: -------------------------------------------------------------------------------- 1 | @val external process: 'a = "process" 2 | 3 | module RemixBrowser = { 4 | @module("remix") @react.component external make: unit => React.element = "RemixBrowser" 5 | } 6 | 7 | type entryContext 8 | 9 | module RemixServer = { 10 | @module("remix") @react.component 11 | external make: (~context: entryContext, ~url: string) => React.element = "RemixServer" 12 | } 13 | 14 | module Meta = { 15 | @module("remix") @react.component 16 | external make: unit => React.element = "Meta" 17 | } 18 | 19 | module Links = { 20 | @module("remix") @react.component 21 | external make: unit => React.element = "Links" 22 | } 23 | 24 | module Outlet = { 25 | @module("remix") @react.component 26 | external make: (~context: 'a=?) => React.element = "Outlet" 27 | } 28 | 29 | module ScrollRestoration = { 30 | @module("remix") @react.component 31 | external make: unit => React.element = "ScrollRestoration" 32 | } 33 | 34 | module Scripts = { 35 | @module("remix") @react.component 36 | external make: unit => React.element = "Scripts" 37 | } 38 | 39 | module LiveReload = { 40 | @module("remix") @react.component 41 | external make: (~port: int=?) => React.element = "LiveReload" 42 | } 43 | 44 | module Link = { 45 | @module("remix") @react.component 46 | external make: ( 47 | ~prefetch: [#intent | #render | #none]=?, 48 | ~to: string, 49 | ~reloadDocument: bool=?, 50 | ~replace: bool=?, 51 | ~state: 'a=?, 52 | ~children: React.element, 53 | ) => React.element = "Link" 54 | } 55 | 56 | module Form = { 57 | @module("remix") @react.component 58 | external make: ( 59 | ~method: [#get | #post | #put | #patch | #delete]=?, 60 | ~action: string=?, 61 | ~encType: [#"application/x-www-form-urlencoded" | #"multipart/form-data"]=?, 62 | ~reloadDocument: bool=?, 63 | ~replace: bool=?, 64 | ~onSubmit: @uncurry ReactEvent.Form.t => unit=?, 65 | ) => React.element = "Form" 66 | } 67 | 68 | @module("remix") external json: {..} => Webapi.Fetch.Response.t = "json" 69 | 70 | @module("remix") external redirect: string => Webapi.Fetch.Response.t = "redirect" 71 | 72 | @module("remix") external useBeforeUnload: (@uncurry unit => unit) => unit = "useBeforeUnload" 73 | 74 | @module("remix") external useLoaderData: unit => 'a = "useLoaderData" 75 | 76 | module Cookie = { 77 | type t 78 | 79 | @get external name: t => string = "name" 80 | @get external isSigned: t => bool = "isSigned" 81 | @get @return(undefined_to_opt) external expires: t => option = "isSigned" 82 | @send external serialize: (t, {..}) => Js.Promise.t = "serialize" 83 | @module("remix") external isCookie: 'a => bool = "isCookie" 84 | 85 | type parseOptions = {decode: string => string} 86 | @send external parse: (t, option) => {..} = "parse" 87 | @send external parseWithOptions: (t, option, parseOptions) => {..} = "parse" 88 | } 89 | 90 | module CreateCookieOptions = { 91 | type t 92 | 93 | @obj 94 | external make: ( 95 | ~decode: string => string=?, 96 | ~encode: string => string=?, 97 | ~domain: string=?, 98 | ~expires: Js.Date.t=?, 99 | ~httpOnly: bool=?, 100 | ~maxAge: int=?, 101 | ~path: string=?, 102 | ~sameSite: [#lax | #strict | #none]=?, 103 | ~secure: bool=?, 104 | ~secrets: array=?, 105 | unit, 106 | ) => t = "" 107 | } 108 | 109 | @module("remix") external createCookie: string => Cookie.t = "createCookie" 110 | @module("remix") 111 | external createCookieWithOptions: (string, CreateCookieOptions.t) => Cookie.t = "createCookie" 112 | --------------------------------------------------------------------------------