├── .gitignore
├── Makefile
├── Readme.md
├── _bundler.js
├── _middlewares
├── error.js
├── log.js
├── notFound.js
├── publicAssets.js
├── router.js
└── timming.js
├── _react
├── base.jsx
├── browser.jsx
├── components
│ ├── Error.jsx
│ └── Link.jsx
└── renderToString.jsx
├── _routes.js
├── config.js
├── importmap.json
├── mod.js
└── src
├── components
└── Menu.jsx
└── pages
├── _error.jsx
├── api
└── test.js
├── hello
├── index.jsx
└── world.jsx
└── index.jsx
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/linux,macos,windows
3 | # Edit at https://www.gitignore.io/?templates=linux,macos,windows
4 |
5 | ### Linux ###
6 | *~
7 |
8 | # temporary files which can be created if a process still has a handle open of a deleted file
9 | .fuse_hidden*
10 |
11 | # KDE directory preferences
12 | .directory
13 |
14 | # Linux trash folder which might appear on any partition or disk
15 | .Trash-*
16 |
17 | # .nfs files are created when an open file is removed but is still being accessed
18 | .nfs*
19 |
20 | ### macOS ###
21 | # General
22 | .DS_Store
23 | .AppleDouble
24 | .LSOverride
25 |
26 | # Icon must end with two \r
27 | Icon
28 |
29 | # Thumbnails
30 | ._*
31 |
32 | # Files that might appear in the root of a volume
33 | .DocumentRevisions-V100
34 | .fseventsd
35 | .Spotlight-V100
36 | .TemporaryItems
37 | .Trashes
38 | .VolumeIcon.icns
39 | .com.apple.timemachine.donotpresent
40 |
41 | # Directories potentially created on remote AFP share
42 | .AppleDB
43 | .AppleDesktop
44 | Network Trash Folder
45 | Temporary Items
46 | .apdisk
47 |
48 | ### Windows ###
49 | # Windows thumbnail cache files
50 | Thumbs.db
51 | Thumbs.db:encryptable
52 | ehthumbs.db
53 | ehthumbs_vista.db
54 |
55 | # Dump file
56 | *.stackdump
57 |
58 | # Folder config file
59 | [Dd]esktop.ini
60 |
61 | # Recycle Bin used on file shares
62 | $RECYCLE.BIN/
63 |
64 | # Windows Installer files
65 | *.cab
66 | *.msi
67 | *.msix
68 | *.msm
69 | *.msp
70 |
71 | # Windows shortcuts
72 | *.lnk
73 |
74 | # End of https://www.gitignore.io/api/linux,macos,windows
75 |
76 | # Deno React Server
77 | public/.src
78 | public/importmap.json
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | run:
2 | deno run --allow-read --allow-write --allow-net --importmap=importmap.json --unstable --reload mod.js
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # Nextjs-ish Routing with Deno
2 | Project with the objective to copy Next.js functionalities to Deno.js.
3 |
4 | By the moment we only have the router based on pages, but more things will be added in the near future.
5 |
6 | ## Requirements
7 | Deno 1.0.0
8 |
9 | Go to https://deno.land to see how to install Deno.
10 |
11 | ## Run
12 | Type `make` on command line
13 |
14 | ## Current folder public and src are only for testing purposes!
15 |
16 | ## Routing
17 | Deno will walk through the folder `/src/pages` and create the routes using Oak.
18 |
19 | ## Public Assets folder
20 | The folder `/public` in the root of the application will host you static assets.
21 |
22 | ## Initial Props
23 | You can get props from server and use on your application.
24 |
25 | ``` javascript
26 | import React from 'react';
27 |
28 | function Page({ props }) {
29 | return
JSON.stringify(props, null, 2) ;
30 | }
31 |
32 | Page.getInitialProps = (context) => {
33 | return {
34 | hello: 'world!'
35 | }
36 | }
37 |
38 | export default Page;
39 | ```
40 |
41 | On this example, you can get the object returned from function getInitialProps and use as you may like.
42 |
43 | The context parameter that you get it's the default context that you get from Oak (https://oakserver.github.io/oak/) module.
44 |
45 | ## Customizable error page
46 | Just create a page at root of `/src/public` with the name `_error.jsx`. The content inside that page, will be your new error page.
47 |
48 | When creating an error page, you will get an error object inside props, see example:
49 |
50 | ``` javascript
51 | import React from 'react';
52 |
53 | function CustomError({ props }) {
54 | return { props.error.status } - { props.error.message }
55 | }
56 |
57 | export default CustomError;
58 | ```
59 |
60 | ## Dynamic API routes
61 | The folder `/src/pages/api` will return an nom component, you can use it as a simple API creation method.
62 | ``` javascript
63 | // index.js
64 | export default function (context) {
65 | return context.response.body = {
66 | Hello: "World!",
67 | method: context.request.method
68 | };
69 | }
70 | ```
71 |
72 | When you make GET a request at /api it will return
73 | ``` json
74 | {
75 | "Hello": "World!",
76 | "method": "GET"
77 | }
78 | ```
79 |
80 | ## API Context
81 | The context param that is received from the handler, is the same that Oak (https://github.com/oakserver/oak) is using.
82 |
83 | ## Missing pieces:
84 | - [ ] Fix React Hooks (Error from different versions for React, ReactDOM and ReactDOMServer);
85 | - [X] API routes;
86 | - [ ] Styling;
87 | - [X] Get initial props;
88 | - [ ] Get static props;
89 | - [ ] Get server side props;
90 | - [ ] Client side nav (react-router and fix React versions using esm);
91 | - [X] Serve static files;
92 | - [X] Customizable error page;
93 | - [ ] Helpers components (Head, Link);
94 | - [ ] Helper functions (Router, useRouter, etc…)
95 | - [ ] Remove folders `/public` and `/src` (These files are examples to show how this project works);
96 |
97 | ## Future plans
98 | In the future this project seeks to create a React application with only two folders, and the rest will be under the hood with this repository, example:
99 | You'll only need the folders that you will use, like the commons:
100 | - `/public`
101 | - `/src/pages`
102 | - `/src/pages/api`
103 | - `/src/components`
104 | - `importmap.json`
105 |
106 | Add and with only these folders, run the command `react-server`, and then, Deno will run this repository and serve your content, without configuring anything else.
107 |
--------------------------------------------------------------------------------
/_bundler.js:
--------------------------------------------------------------------------------
1 | import { exists } from "fs";
2 | import { pageRoutes } from "./_routes.js";
3 |
4 | const [browserDiagnostics, browserOutput] = await Deno.bundle(
5 | "./_react/browser.jsx",
6 | );
7 |
8 | let ErrorPage;
9 | const customError = await exists("src/pages/_error.jsx");
10 |
11 | if (customError) {
12 | const [customErrorPageDiagnostics, customErrorPageOutput] = await Deno.bundle(
13 | "src/pages/_error.jsx",
14 | );
15 | ErrorPage = customErrorPageOutput;
16 | } else {
17 | const [errorPageDiagnostics, errorPageOutput] = await Deno.bundle(
18 | "_react/components/Error.jsx",
19 | );
20 | ErrorPage = errorPageOutput;
21 | }
22 |
23 | const encoder = new TextEncoder();
24 |
25 | await Deno.mkdir("public/.src/pages", { recursive: true });
26 |
27 | await Deno.writeFile("public/.src/bundle.js", encoder.encode(browserOutput));
28 | await Deno.writeFile(
29 | "public/.src/pages/_error.js",
30 | encoder.encode(ErrorPage),
31 | );
32 |
33 | pageRoutes.forEach(async (page) => {
34 | let importPath = page.path;
35 | let exportPath = page.path;
36 |
37 | if (!page.path.endsWith(page.name)) {
38 | if (page.path.endsWith("/")) {
39 | importPath = importPath + "index." + page.extension;
40 | exportPath = exportPath + "index";
41 | } else {
42 | importPath = importPath + "/index." + page.extension;
43 | exportPath = exportPath + "/index";
44 | }
45 | } else {
46 | importPath = importPath + "." + page.extension;
47 | }
48 |
49 | const [pageDiagnostics, pageOutput] = await Deno.bundle(
50 | `./src/pages${importPath}`,
51 | );
52 |
53 | const exportFolder = page.origin.split("/");
54 | exportFolder.shift();
55 | exportFolder.pop();
56 |
57 | await Deno.mkdir(`public/.${exportFolder.join("/")}`, { recursive: true });
58 |
59 | await Deno.writeFile(
60 | `./public/.src/pages${exportPath}.js`,
61 | encoder.encode(pageOutput),
62 | );
63 |
64 | console.log(`Generated ${page.path} at public/${exportFolder.join("/")}`);
65 | });
66 |
--------------------------------------------------------------------------------
/_middlewares/error.js:
--------------------------------------------------------------------------------
1 | import Error from "../_react/components/error.jsx";
2 | import renderToString from "../_react/renderToString.jsx";
3 |
4 | export default async (context, next) => {
5 | try {
6 | await next();
7 | } catch (err) {
8 | let props;
9 |
10 | if (Error.getInitialProps) {
11 | await file.default.getInitialProps(context)
12 | .then((res) => props = res)
13 | .catch((error) => err = error);
14 | } else {
15 | props = {};
16 | }
17 |
18 | const customError = await import("../src/pages/_error.jsx")
19 | .then((res) => res.default)
20 | .catch((err) => {
21 | console.log(err);
22 | return null;
23 | });
24 |
25 | context.response.status = err.status;
26 | context.response.body = await renderToString(
27 | customError || Error,
28 | {
29 | props: {
30 | ...props,
31 | error: {
32 | status: err.status,
33 | message: err.message,
34 | },
35 | },
36 | route: {
37 | name: "error",
38 | path: "/_error",
39 | },
40 | routes: [],
41 | },
42 | );
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/_middlewares/log.js:
--------------------------------------------------------------------------------
1 | export default async (context, next) => {
2 | await next();
3 | const responseTime = context.response.headers.get("X-Response-Time");
4 | console.log(
5 | `${context.request.method} ${context.request.url} - ${responseTime}`,
6 | );
7 | };
8 |
--------------------------------------------------------------------------------
/_middlewares/notFound.js:
--------------------------------------------------------------------------------
1 | export default ({ response }) => {
2 | response.status = 404;
3 | response.body = "Not Found";
4 | };
5 |
--------------------------------------------------------------------------------
/_middlewares/publicAssets.js:
--------------------------------------------------------------------------------
1 | import { send } from "oak";
2 |
3 | export default async (context) => {
4 | await send(
5 | context,
6 | context.request.url.pathname,
7 | {
8 | root: `${Deno.cwd()}/public`,
9 | },
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/_middlewares/router.js:
--------------------------------------------------------------------------------
1 | import { Router } from "oak";
2 | import { pageRoutes, apiRoutes } from "../_routes.js";
3 | import renderToString from "../_react/renderToString.jsx";
4 |
5 | const router = new Router();
6 |
7 | pageRoutes.forEach(async (route) => {
8 | const file = await import(".." + route.origin);
9 |
10 | router
11 | .get(route.path, async (context) => {
12 | let props;
13 | if (file.default.getInitialProps) {
14 | props = await file.default.getInitialProps(context);
15 | } else {
16 | props = {};
17 | }
18 |
19 | context.response.body = await renderToString(
20 | file.default,
21 | {
22 | props,
23 | route,
24 | routes: pageRoutes,
25 | },
26 | );
27 | });
28 | });
29 |
30 | apiRoutes.forEach(async (route) => {
31 | const file = await import(".." + route.origin);
32 | router
33 | .all(route.path, file.default);
34 | });
35 |
36 | export default router;
37 |
--------------------------------------------------------------------------------
/_middlewares/timming.js:
--------------------------------------------------------------------------------
1 | export default async (context, next) => {
2 | const start = Date.now();
3 | await next();
4 | const ms = Date.now() - start;
5 | context.response.headers.set("X-Response-Time", `${ms}ms`);
6 | };
7 |
--------------------------------------------------------------------------------
/_react/base.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function Base({ children, props, routes, route }) {
4 | const pageData = {
5 | __html: [
6 | `window.__initialProps = ${JSON.stringify(props)};`,
7 | `window.__routes = ${JSON.stringify(routes)};`,
8 | `window.__route = ${JSON.stringify(route)};`,
9 | ].join(""),
10 | };
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 | {children}
20 |
21 |
22 | );
23 | }
24 |
25 | export default Base;
26 |
--------------------------------------------------------------------------------
/_react/browser.jsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom";
2 | // import { BrowserRouter } from "react-router-dom";
3 |
4 | window.addEventListener("DOMContentLoaded", async () => {
5 | let routeUrl = `./pages${__route.path}`;
6 | if (__route.path.endsWith("/")) {
7 | routeUrl = routeUrl + __route.name;
8 | }
9 |
10 | routeUrl = routeUrl + ".js";
11 |
12 | const { default: Page } = await import(`${routeUrl}`);
13 | ReactDOM.hydrate(
14 | //
15 | // {__routes.map((route) => {
16 | // return ;
17 | // })}
18 | // ,
19 | // ,
20 | Page({ props: __initialProps }),
21 | document.getElementById("root"),
22 | );
23 | });
24 |
--------------------------------------------------------------------------------
/_react/components/Error.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function Page({ props }) {
4 | return (
5 | <>
6 | Error :(
7 | {props.error.message}
8 | >
9 | );
10 | }
11 |
12 | export default Page;
13 |
--------------------------------------------------------------------------------
/_react/components/Link.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function Link({ children, href }) {
4 | function goTo(e) {
5 | e.preventDefault();
6 | alert("Click on fragment");
7 | }
8 |
9 | return (
10 |
11 | {children}
12 |
13 | );
14 | }
15 |
16 | export default Link;
17 |
--------------------------------------------------------------------------------
/_react/renderToString.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDomServer from "react-dom/server";
3 |
4 | import Base from "./base.jsx";
5 |
6 | function renderToString(Page, { props, route, routes }) {
7 | // const context = {};
8 | return ReactDomServer.renderToString(
9 |
10 | {/* */}
11 |
12 | {/* */}
13 | ,
14 | );
15 | }
16 |
17 | export default renderToString;
18 |
--------------------------------------------------------------------------------
/_routes.js:
--------------------------------------------------------------------------------
1 | import { walk } from "fs";
2 |
3 | function formatRoute(origin) {
4 | console.log(origin);
5 | const pathsToIgnore = [
6 | "src/pages/_error.jsx",
7 | ];
8 |
9 | if (pathsToIgnore.includes(origin)) {
10 | return null;
11 | }
12 |
13 | const paths = origin.split("/");
14 | let [name, extension] = paths[paths.length - 1].split(".");
15 |
16 | paths.shift(); // Remove /src
17 | paths.shift(); // Remove /pages
18 | paths.pop(); // Remove file
19 |
20 | if (name !== "index") paths.push(name); // Add name to path without index if isn't index
21 |
22 | const path = "/" + paths.join("/");
23 | const type = paths[0] === "api" ? "api" : "page";
24 |
25 | origin = "/" + origin;
26 |
27 | return {
28 | name,
29 | path,
30 | extension,
31 | origin,
32 | type,
33 | };
34 | }
35 |
36 | const pageRoutes = [];
37 | const apiRoutes = [];
38 |
39 | // Walk thought pages
40 | for await (const file of walk("./src/pages")) {
41 | if (file.isFile) {
42 | const routeObject = formatRoute(file.path);
43 | if (routeObject) {
44 | if (routeObject.type === "page") {
45 | pageRoutes.push(routeObject);
46 | } else {
47 | apiRoutes.push(routeObject);
48 | }
49 | }
50 | }
51 | }
52 |
53 | export { pageRoutes, apiRoutes };
54 | export default { pageRoutes, apiRoutes };
55 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | APP_HOST: "http://127.0.0.1", // Only show in console after running project
3 | APP_PORT: 8080,
4 | };
5 |
6 | export default config;
7 |
--------------------------------------------------------------------------------
/importmap.json:
--------------------------------------------------------------------------------
1 | {
2 | "imports": {
3 | "fs": "https://deno.land/std@v0.51.0/fs/mod.ts",
4 | "oak": "https://deno.land/x/oak@v4.0.0/mod.ts",
5 | "react": "https://dev.jspm.io/react@16.13.1",
6 | "react-dom": "https://dev.jspm.io/react-dom@16.13.1",
7 | "react-dom/server": "https://dev.jspm.io/react-dom@16.13.1/server"
8 | }
9 | }
--------------------------------------------------------------------------------
/mod.js:
--------------------------------------------------------------------------------
1 | import { Application } from "oak";
2 |
3 | import config from "./config.js";
4 | import error from "./_middlewares/error.js";
5 | import log from "./_middlewares/log.js";
6 | import timming from "./_middlewares/timming.js";
7 | import router from "./_middlewares/router.js";
8 | import publicAssets from "./_middlewares/publicAssets.js";
9 | // import notFound from "./_middlewares/notFound.js";
10 | import "./_bundler.js";
11 |
12 | const app = new Application();
13 |
14 | // Middlewares
15 | app.use(log);
16 | app.use(timming);
17 | app.use(error);
18 |
19 | // Router
20 | app.use(router.routes());
21 | app.use(router.allowedMethods());
22 |
23 | // Public
24 | app.use(publicAssets);
25 |
26 | // Not found
27 | // app.use(notFound);
28 |
29 | console.log(`Listening on ${config.APP_HOST}:${config.APP_PORT}`);
30 | await app.listen({ port: config.APP_PORT });
31 |
--------------------------------------------------------------------------------
/src/components/Menu.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function Menu() {
4 | return (
5 |
6 | Page Routes
7 |
8 | - /
9 |
10 |
11 | - /hello
12 |
13 |
14 | - /hello/world
15 |
16 | Api Routes
17 |
18 | - /api/test
19 |
20 |
21 | );
22 | }
23 |
24 | export default Menu;
25 |
--------------------------------------------------------------------------------
/src/pages/_error.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function CustomError({ props }) {
4 | return (
5 | <>
6 | My custom error :D
7 | {props.error.message}
8 | >
9 | );
10 | }
11 |
12 | export default CustomError;
13 |
--------------------------------------------------------------------------------
/src/pages/api/test.js:
--------------------------------------------------------------------------------
1 | export default function (context) {
2 | return context.response.body = {
3 | hello: "world!",
4 | method: context.request.method,
5 | };
6 | }
7 |
--------------------------------------------------------------------------------
/src/pages/hello/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Menu from "../../components/Menu.jsx";
3 |
4 | function Page() {
5 | return (
6 | <>
7 |
8 | /hello
9 | >
10 | );
11 | }
12 |
13 | export default Page;
14 |
--------------------------------------------------------------------------------
/src/pages/hello/world.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Menu from "../../components/Menu.jsx";
3 |
4 | function Page() {
5 | return (
6 | <>
7 |
8 | /hello world
9 | >
10 | );
11 | }
12 |
13 | export default Page;
14 |
--------------------------------------------------------------------------------
/src/pages/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Menu from "../components/Menu.jsx";
3 |
4 | function Page({ props }) {
5 | const [counter, setCounter] = React.useState(0);
6 |
7 | return (
8 | <>
9 |
10 | Hello world!
11 |
12 | Lorem ipsum dolor, sit amet consectetur adipisicing elit. Veritatis
13 | nulla ullam facilis nisi, possimus distinctio! Ipsa, sapiente sequi
14 | libero minus quas, iure doloremque quos, vitae hic autem voluptates amet
15 | doloribus!
16 |
17 | Get Initial Props
18 | In the function add getInitialProps to your page component, like:
19 |
20 | {`import React from 'react';
21 |
22 | function Page({ props }) {
23 | return { JSON.stringify(props, null, 2) } ;
24 | }
25 |
26 | Page.getInitialProps = (context) => {
27 | return {
28 | hello: 'world!'
29 | };
30 | }
31 |
32 | export default Page;`}
33 |
34 |
35 | This will give you the props from the server that you returned on
36 | getInitialProps function.
37 |
38 | {JSON.stringify(props, null, 2)}
39 | useState hook test
40 |
41 | Clicked {counter} times.
42 |
43 |
44 | setCounter(counter + 1)}>
45 | Add clicks
46 |
47 |
48 | >
49 | );
50 | }
51 |
52 | Page.getInitialProps = (context) => {
53 | return {
54 | hello: "world!",
55 | };
56 | };
57 |
58 | export default Page;
59 |
--------------------------------------------------------------------------------