├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public ├── img │ └── pwa │ │ ├── icons-192.png │ │ └── icons-512.png ├── manifest.webmanifest └── sw.js ├── src ├── client │ └── home.hydrate.tsx ├── colors.scss ├── components │ ├── LazyExample │ │ ├── LazyExample.client.module.scss │ │ ├── LazyExample.client.scss │ │ └── LazyExample.tsx │ ├── TodoList.scss │ └── TodoList.tsx ├── layouts │ ├── MainLayout.scss │ └── MainLayout.tsx ├── pages │ ├── AboutPage.tsx │ └── HomePage.tsx ├── server │ ├── makeHtml.tsx │ └── server.tsx └── styles.scss ├── tsconfig.json ├── typings └── modules.d.ts └── webpack ├── const.cjs ├── webpack.client.common.cjs ├── webpack.client.dev.cjs ├── webpack.client.prod.cjs ├── webpack.common.cjs ├── webpack.server.cjs └── webpack.server.prod.cjs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: yandeu 2 | open_collective: yandeu 3 | patreon: yandeu 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Have a question?** 14 | Join the [discussions](https://github.com/nanojsx/nano/discussions) instead. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/nanojsx/nano/discussions 5 | about: Ask questions and discuss with other community members 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # read: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions 2 | 3 | name: CI 4 | 5 | on: 6 | push: 7 | paths: 8 | - '.github/**' 9 | - 'src/**' 10 | - 'test/**' 11 | - 'webpack/**' 12 | pull_request: 13 | paths: 14 | - '.github/**' 15 | - 'src/**' 16 | - 'test/**' 17 | - 'webpack/**' 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | matrix: 25 | node-version: [16.x, 18.x] 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v3 30 | 31 | - name: Use Node.js ${{ matrix.node-version }} 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | 36 | - name: Install Dependencies 37 | run: npm install 38 | 39 | - name: Build Packages 40 | run: npm run build 41 | 42 | - name: Run Prettier 43 | run: npm run format 44 | 45 | # - name: Run ESLint 46 | # run: npm run lint 47 | 48 | # - name: Upload coverage to Codecov 49 | # uses: codecov/codecov-action@v2 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | "@yandeu/prettier-config" -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Yannick Deubel (https://github.com/yandeu); Project Url: https://github.com/nanojsx/template 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 | # Nano JSX Template 2 | 3 | A [Nano JSX](https://nanojsx.github.io/) Template for building Isomorphic JSX Apps. 4 | 5 | ## Download 6 | 7 | ```bash 8 | # download 9 | npx degit nanojsx/template nano 10 | 11 | # directory 12 | cd nano 13 | 14 | # install 15 | npm i 16 | 17 | # development (on http://localhost:3000/) 18 | npm start 19 | 20 | # production 21 | npm run build 22 | 23 | # serve (on http://localhost:3000/) 24 | npm run serve 25 | ``` 26 | 27 | ## Structure 28 | 29 | ```bash 30 | root 31 | ├── public # all your static files 32 | ├── src 33 | │ ├── client # bundles for hydration 34 | │ ├── components # your custom components 35 | │ ├── layouts # your app's layouts 36 | │ ├── pages # your pages 37 | │ └── server # all server-side code 38 | ``` 39 | 40 | Every file in `/client` will be bundles separately. 41 | 42 | ## TODOs 43 | 44 | All the things below will hopefully be implemented soon. 45 | 46 | - Auto refresh browser on changes 47 | - Improve Service Worker cache strategy 48 | - Pre-Render to static HTML 49 | 50 | ## LazyLoading ChunkLoadError on localhost 51 | 52 | On localhost you may experience the following error when LazyLoading while switching routes: 53 | 54 | ``` 55 | Uncaught (in promise) ChunkLoadError: Loading chunk 56 | ``` 57 | 58 | This is related to the disabled browser option: 59 | 60 | ``` 61 | Allow invalid certificates for resources loaded from localhost. 62 | ``` 63 | 64 | **Fix for Google Chrome** 65 | 66 | In the Chrome address bar, type [chrome://flags/#allow-insecure-localhost](chrome://flags/#allow-insecure-localhost) and **enable** the option. 67 | 68 | **Fix for Firefox** 69 | 70 | No supported option to disable for localhost only. 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nano-jsx-template", 3 | "version": "0.0.8", 4 | "description": "Nano JSX Template using Isomorphic JSX.", 5 | "scripts": { 6 | "start": "npm run clean && npm run dev", 7 | "serve": "node dist/server.bundle.js", 8 | "build": "npm run clean && npm-run-all --parallel prod:*", 9 | "dev": "webpack --config webpack/webpack.client.dev.cjs && webpack --config webpack/webpack.server.cjs && npm-run-all --parallel dev:*", 10 | "dev:nodemon": "nodemon --watch src --ext css,scss,sass,js,ts,tsx,webmanifest --watch dist dist/server.bundle.js", 11 | "dev:webpack-client": "webpack --config webpack/webpack.client.dev.cjs --watch", 12 | "dev:webpack-server": "webpack --config webpack/webpack.server.cjs --watch", 13 | "prod:webpack-client": "webpack --config webpack/webpack.client.prod.cjs", 14 | "prod:webpack-server": "webpack --config webpack/webpack.server.prod.cjs", 15 | "postinstall": "npm run build", 16 | "clean": "rimraf dist", 17 | "format:check": "prettier --check src/**/*", 18 | "format": "prettier --write src/**/*" 19 | }, 20 | "keywords": [], 21 | "author": "Yannick Deubel (https://github.com/yandeu)", 22 | "license": "MIT", 23 | "engines": { 24 | "node": ">=14" 25 | }, 26 | "dependencies": { 27 | "compression": "^1.7.4", 28 | "express": "^4.17.2", 29 | "nano-jsx": "^0.0.34" 30 | }, 31 | "devDependencies": { 32 | "@types/compression": "^1.7.2", 33 | "@types/node-fetch": "^2.5.8", 34 | "@yandeu/prettier-config": "^0.0.3", 35 | "autoprefixer": "^10.4.0", 36 | "copy-webpack-plugin": "^11.0.0", 37 | "css-loader": "^6.5.1", 38 | "nodemon": "^2.0.15", 39 | "npm-run-all": "^4.1.5", 40 | "null-loader": "^4.0.1", 41 | "postcss-loader": "^7.0.1", 42 | "prettier": "^2.5.1", 43 | "resolve-typescript-plugin": "^1.2.0", 44 | "rimraf": "^3.0.2", 45 | "sass": "^1.45.1", 46 | "sass-loader": "^13.1.0", 47 | "style-loader": "^3.3.1", 48 | "ts-loader": "^9.2.6", 49 | "typescript": "^4.8.4", 50 | "webpack": "^5.65.0", 51 | "webpack-cli": "^4.9.1", 52 | "webpack-manifest-plugin": "^5.0.0", 53 | "webpack-merge": "^5.8.0", 54 | "webpack-node-externals": "^3.0.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer')] 3 | } 4 | -------------------------------------------------------------------------------- /public/img/pwa/icons-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanojsx/template/94dcbabafe23a0c676a410a3a3d7313f563bdf88/public/img/pwa/icons-192.png -------------------------------------------------------------------------------- /public/img/pwa/icons-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanojsx/template/94dcbabafe23a0c676a410a3a3d7313f563bdf88/public/img/pwa/icons-512.png -------------------------------------------------------------------------------- /public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Nano JSX", 3 | "name": "Nano JSX Application", 4 | "description": "A simple PWA using nano-jsx", 5 | "icons": [ 6 | { 7 | "src": "/public/img/pwa/icons-192.png", 8 | "type": "image/png", 9 | "sizes": "192x192" 10 | }, 11 | { 12 | "src": "/public/img/pwa/icons-512.png", 13 | "type": "image/png", 14 | "sizes": "512x512" 15 | } 16 | ], 17 | "start_url": "/", 18 | "background_color": "#3367D6", 19 | "display": "standalone", 20 | "scope": "/", 21 | "theme_color": "#3367D6" 22 | } 23 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | importScripts( 2 | 'https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js' 3 | ) 4 | 5 | const { registerRoute } = workbox.routing 6 | const { CacheFirst, StaleWhileRevalidate } = workbox.strategies 7 | const { ExpirationPlugin } = workbox.expiration 8 | 9 | registerRoute( 10 | ({ request }) => request.destination === 'style', 11 | new StaleWhileRevalidate({ 12 | cacheName: 'css-cache' 13 | }) 14 | ) 15 | 16 | registerRoute( 17 | ({ request }) => request.destination === 'document', 18 | new StaleWhileRevalidate({ 19 | cacheName: 'document-cache' 20 | }) 21 | ) 22 | 23 | registerRoute( 24 | ({ request }) => request.destination === 'script', 25 | new StaleWhileRevalidate({ 26 | cacheName: 'script-cache' 27 | }) 28 | ) 29 | 30 | registerRoute( 31 | // Cache image files. 32 | ({ request }) => request.destination === 'image', 33 | // Use the cache if it's available. 34 | new CacheFirst({ 35 | // Use a custom cache name. 36 | cacheName: 'image-cache', 37 | plugins: [ 38 | new ExpirationPlugin({ 39 | // Cache only 20 images. 40 | maxEntries: 20, 41 | // Cache for a maximum of a week. 42 | maxAgeSeconds: 7 * 24 * 60 * 60 43 | }) 44 | ] 45 | }) 46 | ) 47 | -------------------------------------------------------------------------------- /src/client/home.hydrate.tsx: -------------------------------------------------------------------------------- 1 | import { h, hydrate } from 'nano-jsx/lib/core.js' 2 | import { printVersion } from 'nano-jsx/lib/helpers.js' 3 | import TodoList from '../components/TodoList.js' 4 | 5 | const main = async () => { 6 | hydrate(, document.getElementById('todo-list')) 7 | 8 | // example of a lazy loaded module 9 | window.addEventListener( 10 | 'click', 11 | () => 12 | import('../components/LazyExample/LazyExample.js').then(({ default: LazyComponent }) => { 13 | const html = hydrate() 14 | const homePage = document.getElementById('homePage') 15 | homePage?.appendChild(html) 16 | }), 17 | { once: true } 18 | ) 19 | 20 | // print the nano-jsx version in the console 21 | printVersion() 22 | } 23 | 24 | main() 25 | -------------------------------------------------------------------------------- /src/colors.scss: -------------------------------------------------------------------------------- 1 | $padding: 1em 2em; 2 | $primary-color: #ff4e6a; 3 | $font: #363636; 4 | -------------------------------------------------------------------------------- /src/components/LazyExample/LazyExample.client.module.scss: -------------------------------------------------------------------------------- 1 | .lazy-example { 2 | margin-top: 3em; 3 | border: 1px red solid; 4 | 5 | i { 6 | color: #2e19ab; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/LazyExample/LazyExample.client.scss: -------------------------------------------------------------------------------- 1 | #lazy-example { 2 | p { 3 | color: blue; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/LazyExample/LazyExample.tsx: -------------------------------------------------------------------------------- 1 | import { h } from 'nano-jsx/lib/core.js' 2 | 3 | // you don't need "withStyles" if you include your css in the client bundle 4 | // import { withStyles } from 'nano-jsx/lib/withStyles' 5 | 6 | // import normal styling (ending with .client.scss) 7 | import './LazyExample.client.scss' 8 | 9 | // import css modules styling (ending with .client.module.scss) 10 | import className from './LazyExample.client.module.scss' 11 | 12 | // see home.hydrate.tsx to see how to lazy load (code splitting with dynamic Imports) this component 13 | const LazyExample = () => { 14 | console.log('I am LazyExample') 15 | return ( 16 |
17 |

18 | LazyExample 19 |
20 | 21 | 22 | This text and its styling are lazy loaded (in another .js file) and will appear once you click on the page. 23 | 24 | 25 |

26 |
27 | ) 28 | } 29 | 30 | export default LazyExample 31 | -------------------------------------------------------------------------------- /src/components/TodoList.scss: -------------------------------------------------------------------------------- 1 | @import '../colors'; 2 | 3 | #todo-list { 4 | li { 5 | padding: 6px; 6 | } 7 | form { 8 | input { 9 | margin-top: 4px; 10 | padding: 2px 4px; 11 | &:focus { 12 | outline: none; 13 | } 14 | } 15 | button { 16 | background: $primary-color; 17 | border: none; 18 | color: white; 19 | padding: 4px 12px; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import { h } from 'nano-jsx/lib/core.js' 2 | import { Component } from 'nano-jsx/lib/component.js' 3 | 4 | import styles from './TodoList.scss' 5 | import { withStyles } from 'nano-jsx/lib/withStyles.js' 6 | 7 | class TodoList extends Component<{}, { todos: string[] }> { 8 | constructor(props: any) { 9 | super(props) 10 | 11 | // set a unique id if you use this component more than 12 | // once across your app and it is using a state 13 | this.id = 'TodoList' 14 | 15 | // this state now has the id 'TodoList' and can be accessed 16 | // by any other component using that id and the useState() hook 17 | this.state = { todos: [] } 18 | } 19 | 20 | didMount() { 21 | // get todos from localStorage 22 | const t = localStorage.getItem('todos') 23 | if (t) { 24 | this.state.todos = JSON.parse(t) 25 | this.saveAndUpdate() 26 | } 27 | } 28 | 29 | saveAndUpdate() { 30 | // save todos to localStorage 31 | localStorage.setItem('todos', JSON.stringify(this.state.todos)) 32 | // update component 33 | this.update() 34 | // set focus to input element 35 | document.getElementById('input')?.focus() 36 | } 37 | 38 | submitHandler(e: Event) { 39 | e.preventDefault() 40 | const input = document.getElementById('input') as HTMLInputElement 41 | if (input.value.length > 0) { 42 | this.state.todos.push(input.value) 43 | this.saveAndUpdate() 44 | } 45 | } 46 | 47 | removeHandler(i: number) { 48 | this.state.todos.splice(i, 1) 49 | this.saveAndUpdate() 50 | } 51 | 52 | render() { 53 | return ( 54 |
this.submitHandler(e)}> 55 | 56 |
57 | 58 | 59 |
    60 | {this.state.todos.map((todo: any, index: number) => ( 61 |
  • 62 | {todo}{' '} 63 | this.removeHandler(index)} style={{ color: 'red', cursor: 'pointer' }}> 64 | x 65 | 66 |
  • 67 | ))} 68 |
69 |
70 | ) 71 | } 72 | } 73 | 74 | export default withStyles(styles)(TodoList) 75 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.scss: -------------------------------------------------------------------------------- 1 | @import '../colors'; 2 | 3 | @mixin link { 4 | &:hover { 5 | text-decoration: underline; 6 | text-decoration-color: $primary-color; 7 | } 8 | } 9 | 10 | #root { 11 | max-width: 550px; 12 | margin: 0 auto; 13 | 14 | h1 { 15 | color: $primary-color; 16 | } 17 | 18 | header { 19 | padding: $padding; 20 | nav { 21 | margin-left: -16px; 22 | a { 23 | padding: 16px; 24 | &:hover { 25 | text-decoration: underline; 26 | text-decoration-color: $primary-color; 27 | } 28 | } 29 | } 30 | } 31 | 32 | #content { 33 | padding: $padding; 34 | opacity: 1; 35 | animation: fadeIn 0.4s; 36 | min-height: calc(100vh - 200px); 37 | } 38 | 39 | footer { 40 | padding: $padding; 41 | a { 42 | @include link(); 43 | } 44 | } 45 | } 46 | 47 | @keyframes fadeIn { 48 | 0% { 49 | opacity: 0; 50 | } 51 | 100% { 52 | opacity: 1; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import Nano, { h, withStyles, Link } from 'nano-jsx' 2 | import { VERSION } from 'nano-jsx/lib/version.js' 3 | 4 | import styles from '../styles.scss' 5 | import mainStyles from './MainLayout.scss' 6 | 7 | const MainLayout = (props: any) => { 8 | const { path } = props 9 | 10 | const isBold = (href: string) => (href === path ? { fontWeight: 'bold' } : {}) 11 | 12 | return ( 13 |
14 |
15 | 23 |
24 |
{props.children}
25 | 35 |
36 | ) 37 | } 38 | 39 | export default withStyles(styles, mainStyles)(MainLayout) 40 | -------------------------------------------------------------------------------- /src/pages/AboutPage.tsx: -------------------------------------------------------------------------------- 1 | import { h, Helmet } from 'nano-jsx' 2 | import MainLayout from '../layouts/MainLayout' 3 | 4 | export const AboutPage = (props: any) => { 5 | return ( 6 | 7 | 8 | About Page 9 | 10 |

About page

11 |

This is the Nano JSX Template.

12 |

13 | Although, this App is completely build using Isomorphic JSX, it contains only very little JavaScript. 14 |

15 |

It is a MPA but feels like an SPA.

16 |

17 | Repository:{' '} 18 | 19 | github.com/nanojsx/template 20 | 21 |

22 | 23 |

24 | Author:{' '} 25 | 26 | @yandeu 27 | 28 |

29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import Nano, { h, Helmet } from 'nano-jsx' 2 | import MainLayout from '../layouts/MainLayout' 3 | import TodoList from '../components/TodoList' 4 | 5 | export const HomePage = (props: any) => { 6 | return ( 7 | 8 | 9 | Home Page 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

Home Page

18 |
19 | 20 |
21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/server/makeHtml.tsx: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | // get the manifest file 4 | const manifest = JSON.parse( 5 | fs.readFileSync(__dirname + '/public/js/manifest.json', { 6 | encoding: 'utf-8' 7 | }) 8 | ) 9 | 10 | // prepare html markup 11 | export const makeHtml = (body: any, head: string[] = [], footer: string[] = []) => { 12 | let html = ` 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ${head.join('\n')} 22 | 23 | 24 | 25 | ${body} 26 | ${footer.join('\n')} 27 | 28 | 35 | 36 | 37 | 38 | ` 39 | const addChunkHash = () => { 40 | for (const [key, value] of Object.entries(manifest)) { 41 | if (html.includes(key)) html = html.replace(key, value as string) 42 | } 43 | } 44 | 45 | const minify = () => { 46 | html = html.replace(/(?=/gm, '') // replace html comments 47 | html = html.replace(/\s+/gm, ' ') // remove unnecessary spaces 48 | } 49 | 50 | addChunkHash() 51 | // minify() 52 | 53 | return html 54 | } 55 | -------------------------------------------------------------------------------- /src/server/server.tsx: -------------------------------------------------------------------------------- 1 | import compression from 'compression' 2 | import Nano, { h, Helmet } from 'nano-jsx' 3 | 4 | import { makeHtml } from './makeHtml' 5 | 6 | import { HomePage } from '../pages/HomePage' 7 | import { AboutPage } from '../pages/AboutPage' 8 | 9 | // express 10 | const express = require('express') 11 | const app = express() 12 | const port = process.env.PORT || 3000 13 | 14 | // compression 15 | app.use(compression()) 16 | 17 | app.get('/', async (req: any, res: any) => { 18 | /** 19 | * You could fetch some data here and pass the result to your component. 20 | * 21 | * const data = await fetch('website.com/api/posts/1234') 22 | * 23 | */ 24 | 25 | // render the app 26 | const ssr = Nano.renderSSR() 27 | 28 | // extract body, head, and footer 29 | const { body, head, footer } = Helmet.SSR(ssr) 30 | 31 | // make and send the html 32 | res.send(makeHtml(body, head, footer)) 33 | }) 34 | 35 | app.get('/about', async (req: any, res: any) => { 36 | // render the app 37 | const ssr = Nano.renderSSR() 38 | 39 | // extract body, head, and footer 40 | const { body, head, footer } = Helmet.SSR(ssr) 41 | 42 | // make and send the html 43 | res.send(makeHtml(body, head, footer)) 44 | }) 45 | 46 | // serve service worker file 47 | app.get('/sw.js', (req: any, res: any) => { 48 | res.sendFile(__dirname + '/public/sw.js') 49 | }) 50 | 51 | // serve manifest.webmanifest file 52 | app.get('/manifest.webmanifest', (req: any, res: any) => { 53 | res.sendFile(__dirname + '/public/manifest.webmanifest') 54 | }) 55 | 56 | // serve public directory as a static folder 57 | app.use('/public', express.static('./dist/public')) // , { maxAge: 60 * 60 * 1000 })) 58 | 59 | // start the server 60 | app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`)) 61 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'colors'; 2 | 3 | // remove outline on press (chrome) 4 | * { 5 | -webkit-tap-highlight-color: transparent; 6 | -webkit-touch-callout: none; 7 | } 8 | 9 | // remove outline on press (firefox) 10 | a:focus { 11 | outline: none; 12 | } 13 | 14 | // remove input field outline on focus 15 | input:focus { 16 | outline: none; 17 | } 18 | 19 | // change default a style 20 | a { 21 | color: inherit; 22 | text-decoration: inherit; 23 | } 24 | 25 | // change default list style 26 | ol, 27 | ul { 28 | padding-left: 0px; 29 | } 30 | li { 31 | list-style-type: none; 32 | } 33 | 34 | // some styles for the body 35 | body { 36 | // user-select: none; 37 | margin: 0px; 38 | color: $font; 39 | font-family: sans-serif; 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | 5 | "target": "ES2015", 6 | "module": "ES2020", 7 | "moduleResolution": "node", 8 | 9 | "jsx": "react", 10 | "jsxFactory": "h", 11 | "jsxFragmentFactory": "Fragment", 12 | 13 | "strict": true, 14 | "esModuleInterop": true, 15 | "skipLibCheck": true, 16 | "skipDefaultLibCheck": false, 17 | "forceConsistentCasingInFileNames": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /typings/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const styles: any 3 | export default styles 4 | } 5 | 6 | declare module '*.scss' { 7 | const styles: any 8 | export default styles 9 | } 10 | 11 | declare module '*.sass' { 12 | const styles: any 13 | export default styles 14 | } 15 | -------------------------------------------------------------------------------- /webpack/const.cjs: -------------------------------------------------------------------------------- 1 | exports.REGEX = { 2 | STYLES: /\.s[ac]ss$/i, 3 | CLIENT_STYLES: /\.client(\.module)?\.s?[ac]ss$/i 4 | } 5 | 6 | const LOADERS = [ 7 | { 8 | loader: 'css-loader', 9 | options: { 10 | url: false 11 | } 12 | }, 13 | 'postcss-loader', 14 | 'sass-loader' 15 | ] 16 | 17 | exports.LOADER = { 18 | STYLES: LOADERS, 19 | CLIENT_STYLES: ['style-loader', ...LOADERS], 20 | NULL: ['null-loader'] 21 | } 22 | -------------------------------------------------------------------------------- /webpack/webpack.client.common.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const glob = require('glob') 3 | const CopyPlugin = require('copy-webpack-plugin') 4 | const ResolveTypeScriptPlugin = require('resolve-typescript-plugin') 5 | const { WebpackManifestPlugin } = require('webpack-manifest-plugin') 6 | const { REGEX, LOADER } = require('./const.cjs') 7 | 8 | module.exports = { 9 | // create one bundle for each file in /src/client 10 | entry: glob 11 | .sync(path.resolve(__dirname, '../src/client/') + '/**.ts*', { 12 | absolute: true 13 | }) 14 | .reduce((acc, path) => { 15 | const entry = path.match(/[^\/]+\.tsx?$/gm)[0].replace(/\.tsx?$/, '') 16 | acc[entry] = path 17 | return acc 18 | }, {}), 19 | output: { 20 | filename: '[name].js', 21 | chunkFilename: '[id].js', 22 | path: path.resolve(__dirname, '../dist/public/js') 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: REGEX.CLIENT_STYLES, 28 | use: LOADER.CLIENT_STYLES 29 | }, 30 | { 31 | test: REGEX.STYLES, 32 | exclude: REGEX.CLIENT_STYLES, 33 | use: LOADER.NULL 34 | } 35 | ] 36 | }, 37 | resolve: { 38 | plugins: [new ResolveTypeScriptPlugin()] 39 | }, 40 | plugins: [ 41 | new CopyPlugin({ 42 | patterns: [{ from: 'public', to: '../' }] 43 | }), 44 | new WebpackManifestPlugin({ 45 | publicPath: '', 46 | basePath: '', 47 | filter: file => { 48 | return /\.hydrate\.js$/.test(file.name) 49 | } 50 | }) 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /webpack/webpack.client.dev.cjs: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.common.cjs') 2 | const hydrate = require('./webpack.client.common.cjs') 3 | const { merge } = require('webpack-merge') 4 | 5 | const dev = { 6 | mode: 'development', 7 | devtool: 'inline-cheap-source-map' 8 | } 9 | 10 | module.exports = merge(common, hydrate, dev) 11 | -------------------------------------------------------------------------------- /webpack/webpack.client.prod.cjs: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.common.cjs') 2 | const hydrate = require('./webpack.client.common.cjs') 3 | const { merge } = require('webpack-merge') 4 | 5 | const prod = { 6 | mode: 'production', 7 | output: { 8 | filename: '[name].[chunkhash].js', 9 | chunkFilename: '[id].[chunkhash].js' 10 | } 11 | } 12 | 13 | module.exports = merge(common, hydrate, prod) 14 | -------------------------------------------------------------------------------- /webpack/webpack.common.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stats: 'errors-warnings', 3 | resolve: { 4 | extensions: ['.ts', '.tsx', '.js'] 5 | }, 6 | module: { 7 | rules: [{ test: /\.tsx?$/, loader: 'ts-loader' }] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /webpack/webpack.server.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const common = require('./webpack.common.cjs') 3 | const { merge } = require('webpack-merge') 4 | const nodeExternals = require('webpack-node-externals') 5 | const { REGEX, LOADER } = require('./const.cjs') 6 | 7 | const server = { 8 | mode: 'development', 9 | target: 'node', 10 | entry: path.resolve(__dirname, '../src/server/server.tsx'), 11 | output: { 12 | filename: 'server.bundle.js', 13 | path: path.resolve(__dirname, '../dist') 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: REGEX.STYLES, 19 | exclude: REGEX.CLIENT_STYLES, 20 | use: LOADER.STYLES 21 | } 22 | ] 23 | }, 24 | externals: [nodeExternals()] 25 | } 26 | 27 | module.exports = merge(common, server) 28 | -------------------------------------------------------------------------------- /webpack/webpack.server.prod.cjs: -------------------------------------------------------------------------------- 1 | const devServer = require('./webpack.server.cjs') 2 | const { merge } = require('webpack-merge') 3 | 4 | const server = { 5 | mode: 'production' 6 | } 7 | 8 | module.exports = merge(devServer, server) 9 | --------------------------------------------------------------------------------