├── .gitignore
├── README.md
├── bin
└── newpage.js
├── bsconfig.json
├── package-lock.json
├── package.json
└── src
├── Next.re
└── NextServer.re
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .merlin
3 | .bsb.lock
4 | npm-debug.log
5 | lib
6 | node_modules
7 | *.bs.js
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Next.js, but in ReasonML and Bucklescript!
2 |
3 | Type definitions for Next.js.
4 |
5 | State:
6 |
7 | - Mostly complete type definitions. Feel free to open a pull-request to add more!
8 | - Wraps API for Next version 9.x.x on NPM
9 | - Only works with Bucklescript 7.x and above.
10 |
11 | # The Basics.
12 |
13 | 0. Have a reason project already set up
14 |
15 | 1. Install:
16 |
17 | ```
18 | npm install --save next@9 reason-nextjs
19 | ```
20 |
21 | (Notice how you installed both next and nextdotbs!)
22 |
23 | 2. Add "reason-nextjs" to your "bs-dependencies" in your "bsconfig.json"
24 |
25 | 3. [Install ReasonReact](https://reasonml.github.io/reason-react/docs/en/installation)
26 |
27 | 4. `mkdir pages`
28 |
29 | 5. Make a file: `src/pages/WelcomePage.re` **that defines a React component named default**.
30 |
31 | ```
32 | open React;
33 | open Next;
34 |
35 | [@react.component]
36 | let default = () => {
37 |
38 |
39 |
"Welcome"->string
40 |
41 | "Welcome to Next"->string
42 | ;
43 | };
44 | ```
45 |
46 | 6. Symlink that file into the pages directory:
47 |
48 | `cd pages && ln -s ../src/pages/WelcomePage.bs.js welcome.js`
49 |
50 | The symlinking allows us to still use the .bs.js postfix for Reason files, and also allows us to use dynamic routing and name files with square brackets inside of the `pages` directory without upsetting `bsb`.
51 |
52 | 7. Launch Next and open the browser, etc. as explained in [the Next.js docs](https://nextjs.org/docs)
53 |
54 | ## Data Fetching
55 |
56 | Next performs data fetching for page components by looking for a static function on the component class called `getInitialProps`. This isn't a pattern that ReasonReact likes, so instead, these wrappings expose a function called `assignPropsFetcher` that you pass your `default` value (from creating the React component) to:
57 |
58 | ```reason
59 | open React;
60 | open Next;
61 |
62 | [@react.component]
63 | let default = (~message: string) => {
64 |
65 |
66 |
"Welcome"->string
67 |
68 | "Welcome to Next"->string
69 | ;
70 | };
71 |
72 | // Notice how I match the props for the component above!
73 | type props = {message: option(Post.t)};
74 |
75 | let fetcher: propsFetcher(props) =
76 | ({req, query}) => {
77 | // ... do some async stuff we don't show here and get a value called "message" in scope. 🧙♂️
78 | {message: message}->Js.Promise.resolve;
79 | };
80 |
81 | default->assignPropsFetcher(fetcher);
82 | ```
83 |
84 | That's most of it! So to re-cap, you can use it just like Next.js except:
85 |
86 | - Symlink your compiled Reason artifacts into the correct location in the `pages` folder.
87 | - Use `assignPropsFetcher` instead of `getInitialProps`.
88 | - The `req` param that gets passed into the props fetcher will be `None` on the client, but will be an object with a `headers` field on the server. If you're making a request from your own server to your own API to populate data, you can get the `host` and use it as the host for your HTTP request.
89 |
90 | Other things to note:
91 |
92 | - A `Head` React component is available for inserting tags into the head of the document.
93 | - A `Link` React component is availalbe for slick in-app navigation.
94 |
95 | ## Usage with Serbet.
96 |
97 | You may want to run Next alongside your own API, and you may be using [Serbet](https://github.com/mrmurphy/serbet) for that. Here's how they can work together:
98 |
99 | ```reason
100 | open Async;
101 |
102 | let nextServer =
103 | NextServer.make({
104 | dev:
105 | !
106 | Node.Process.process##env
107 | ->Js.Dict.get("NODE_ENV")
108 | ->Belt.Option.map(a => a == "production")
109 | ->Belt.Option.getWithDefault(false),
110 | });
111 |
112 | {
113 | let%Async _ = nextServer->NextJSServer.prepare;
114 | let app =
115 | Serbet.application([
116 | // your API routes here
117 | ]);
118 | app.router->Express.Router.use(nextServer->NextServer.getRequestHandler);
119 | async();
120 | };
121 | ```
122 |
--------------------------------------------------------------------------------
/bin/newpage.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const fs = require("fs");
4 | const mkdirp = require("mkdirp");
5 | const path = require("path");
6 |
7 | const pagesDirExists = fs.existsSync("pages");
8 |
9 | let pagePath = process.argv[2];
10 | if (!pagePath) {
11 | console.log(
12 | "Please give me the path to a page you want to create! like: posts/[pid]"
13 | );
14 | process.exit(1);
15 | }
16 |
17 | function capitalize(str) {
18 | return str.charAt(0).toUpperCase() + str.substring(1);
19 | }
20 |
21 | function makeBSBSafe(path) {
22 | let noSquareBrackets = path
23 | .replace(/\[/g, "Dynamic_")
24 | .replace(/\]/g, "")
25 | .replace(/\.\.\./g, "All_");
26 | let splitted = noSquareBrackets.split("/");
27 | let filename = splitted[splitted.length - 1];
28 | splitted[splitted.length - 1] = capitalize(filename);
29 | return splitted.join("/");
30 | }
31 |
32 | let srcPath = makeBSBSafe(`src/pages/${pagePath}`);
33 | let linkPath = `pages/${pagePath}`;
34 |
35 | if (!fs.existsSync(srcPath)) {
36 | let splitted = srcPath.split("/");
37 | mkdirp.sync(splitted.slice(0, splitted.length - 1).join("/"));
38 | }
39 |
40 | if (!fs.existsSync(linkPath)) {
41 | let splitted = linkPath.split("/");
42 | mkdirp.sync(splitted.slice(0, splitted.length - 1).join("/"));
43 | }
44 |
45 | let finalSrcPath = srcPath + ".bs.js";
46 | let finalLinkPath = linkPath + ".js";
47 |
48 | if (!fs.existsSync(srcPath + ".re")) {
49 | // Write the BS file in place
50 | fs.writeFileSync(
51 | srcPath + ".re",
52 | `
53 | open React;
54 | open Next;
55 |
56 | [@react.component]
57 | let default = () => {
58 | "Hello"->string
;
59 | };
60 | `
61 | );
62 | }
63 |
64 | let moduleName = path.basename(finalSrcPath, ".bs.js");
65 |
66 | if (!fs.existsSync(finalLinkPath)) {
67 | fs.writeFileSync(
68 | finalLinkPath,
69 | `
70 | import ${moduleName} from "${path.relative(
71 | path.dirname(finalLinkPath),
72 | finalSrcPath
73 | )}";
74 | export default ${moduleName};
75 | `
76 | );
77 | }
78 |
79 | console.log(`Done! ${finalLinkPath} should now point to ${finalSrcPath}`);
80 |
--------------------------------------------------------------------------------
/bsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextdotbs",
3 | "reason": { "react-jsx": 3 },
4 | "sources": [
5 | {
6 | "dir": "src",
7 | "subdirs": true
8 | }
9 | ],
10 | "package-specs": {
11 | "module": "commonjs",
12 | "in-source": true
13 | },
14 | "suffix": ".bs.js",
15 | "bs-dependencies": ["reason-react"],
16 | "ppx-flags": []
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reason-nextjs",
3 | "version": "0.2.3",
4 | "scripts": {
5 | "clean": "bsb -clean-world",
6 | "build": "bsb -make-world",
7 | "start": "bsb -make-world -w"
8 | },
9 | "bin": {
10 | "newpage": "./bin/newpage.js"
11 | },
12 | "repository": "https://github.com/mrmurphy/reason-nextjs",
13 | "keywords": [
14 | "Reason",
15 | "NextJS",
16 | "Express",
17 | "HTTP"
18 | ],
19 | "author": "Murphy Randle",
20 | "license": "MIT",
21 | "peerDependencies": {
22 | "bs-platform": "^7.x.x",
23 | "reason-react": "^0.7.0"
24 | },
25 | "dependencies": {
26 | "mkdirp": "^1.0.3",
27 | "next": "^9.1.6"
28 | },
29 | "devDependencies": {
30 | "bs-platform": "^7.0.1",
31 | "reason-react": "^0.7.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Next.re:
--------------------------------------------------------------------------------
1 | // These are standard Node.js Request and Response objects, but we wanted to
2 | // allow other libraries to wrap those APIs, and didn't want to bloat the size
3 | // of this library by including one. If you want to do more with the request
4 | // than get the headers, or if you want to do something special with the
5 | // response, we suggest tasteful use of Obj.magic to convert these values to the
6 | // type of choice that fits with the Node.js library you are using.
7 | type request = {headers: Js.Dict.t(string)};
8 | type response;
9 |
10 | type context = {
11 | // Current route. That is the path of the page in /pages
12 | pathname: string,
13 | // Query string section of URL parsed as an object
14 | query: Js.Dict.t(string),
15 | // String of the actual path (including the query) shown in the browser
16 | asPath: string,
17 | // HTTP request object (server only)
18 | req: request,
19 | // HTTP response object (server only)
20 | res: response,
21 | // Error object if any error is encountered during the rendering
22 | err: Js.Exn.t,
23 | };
24 |
25 | [@bs.set]
26 | external assignGetInitialProps:
27 | (React.component('props), context => Js.Promise.t('props)) => unit =
28 | "getInitialProps";
29 |
30 | //
31 | // React Components
32 | //
33 | module Head = {
34 | [@react.component] [@bs.module "next/head"]
35 | external make: (~children: React.element) => React.element = "default";
36 | };
37 |
38 | module Link = {
39 | [@react.component] [@bs.module "next/link"]
40 | external make:
41 | (
42 | ~children: React.element,
43 | ~href: string,
44 | ~as_: string=?,
45 | ~passHref: bool=?,
46 | ~prefetch: bool=?,
47 | ~shallow: bool=?,
48 | ~replace: bool=?,
49 | ~scroll: bool=?
50 | ) =>
51 | React.element =
52 | "default";
53 | };
54 |
55 | //
56 | // Hooks
57 | //
58 |
59 | module Router = {
60 | type t = {
61 | // Current route. That is the path of the page in /pages
62 | pathname: string,
63 | // The query string parsed to an object. Defaults to {}
64 | query: Js.Dict.t(string),
65 | // Actual path (including the query) shown in the browser
66 | asPath: string,
67 | };
68 |
69 | type pushOptions = {
70 | shallow: bool,
71 | getInitialProps: option(bool),
72 | };
73 | [@bs.send]
74 | external push:
75 | (t, ~url: string, ~asUrl: string=?, ~options: pushOptions=?, unit) => unit =
76 | "push";
77 |
78 | [@bs.send]
79 | external replace:
80 | (t, ~url: string, ~asUrl: string=?, ~options: pushOptions=?, unit) => unit =
81 | "replace";
82 |
83 | type popStateContext = {
84 | url: string,
85 | as_: string,
86 | options: pushOptions,
87 | };
88 | [@bs.send]
89 | external beforePopState: (t, popStateContext => bool) => unit =
90 | "beforePopState";
91 |
92 | ();
93 | // Events are not wrapped at the moment. Feel free to contribute!
94 | };
95 |
96 | [@bs.module "next/router"] external useRouter: unit => Router.t = "useRouter";
--------------------------------------------------------------------------------
/src/NextServer.re:
--------------------------------------------------------------------------------
1 | // These are the pieces you need if you want to add Next.js to an existing Express server.
2 |
3 | type server;
4 | type opts = {dev: bool};
5 |
6 | // First, make a Next app.
7 | [@bs.module] external make: opts => server = "next";
8 |
9 | // Then prepare it, and after the preparation is done, you can start your express app.
10 | [@bs.send] external prepare: server => Js.Promise.t(unit) = "prepare";
11 |
12 | // This will return a middleware that you can pass to Express's "App.use" or "Router.use".
13 | [@bs.send]
14 | external getRequestHandler: server => 'expressMiddleware = "getRequestHandler";
--------------------------------------------------------------------------------