├── .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 |