From 3cb3286b713c9ee5378bee33f780797d1ef9c8a8 Mon Sep 17 00:00:00 2001 From: Sean Thomas Sullivan Date: Thu, 23 Sep 2021 22:37:47 -0400 Subject: [PATCH] Initial commit --- .gitignore | 0 .vscode/settings.json | 6 ++ Procfile | 1 + README.md | 25 ++++++ assets/example.png | Bin 0 -> 18838 bytes client/App.tsx | 33 ++++++++ client/api/auth.ts | 37 ++++++++ client/app/useSocket.ts | 83 ++++++++++++++++++ client/components/GithubIcon.tsx | 25 ++++++ client/components/Loading.tsx | 7 ++ client/components/Page.tsx | 28 +++++++ client/pages/AppPage.tsx | 14 ++++ client/pages/LoadingPage.tsx | 12 +++ client/pages/LoginPage.tsx | 27 ++++++ client/styles.css | 38 +++++++++ client/user.tsx | 75 +++++++++++++++++ common/assertNever.ts | 5 ++ common/user.ts | 3 + import_map.json | 18 ++++ server/client.tsx | 15 ++++ server/routes/api.ts | 19 +++++ server/routes/static.tsx | 139 +++++++++++++++++++++++++++++++ server/server.ts | 51 ++++++++++++ tsconfig.json | 8 ++ 24 files changed, 669 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 Procfile create mode 100644 README.md create mode 100644 assets/example.png create mode 100644 client/App.tsx create mode 100644 client/api/auth.ts create mode 100644 client/app/useSocket.ts create mode 100644 client/components/GithubIcon.tsx create mode 100644 client/components/Loading.tsx create mode 100644 client/components/Page.tsx create mode 100644 client/pages/AppPage.tsx create mode 100644 client/pages/LoadingPage.tsx create mode 100644 client/pages/LoginPage.tsx create mode 100644 client/styles.css create mode 100644 client/user.tsx create mode 100644 common/assertNever.ts create mode 100644 common/user.ts create mode 100644 import_map.json create mode 100644 server/client.tsx create mode 100644 server/routes/api.ts create mode 100644 server/routes/static.tsx create mode 100644 server/server.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d597c19 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "deno.enable": true, + "deno.lint": true, + "deno.unstable": true, + "deno.importMap": "./import_map.json" +} \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..247bf3e --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: deno run --allow-read --unstable --allow-env --allow-net --import-map import_map.json ./server/server.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..fae20a1 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Deno React Web Starter + +This is a template repository for bootstrapping simple React SSR applications using [Deno](https://deno.land/), [Oak](https://deno.land/x/oak), [React](https://reactjs.org/), and [Firebase](https://console.firebase.google.com/u/0/). + +## Why + +Using Deno means being able to work with modern JS features in a direct way without needing transpilation steps. `Deno.emit()` creates a client bundle which is exposed by a simple webserver. + +## Features + +Quickly supports websockets connections, static file serving, and authentication with Firebase. Simply uncomment the appropriate locations in code and add your configuration variables. + +## Code layout + +- `~/client`: any code that is compiled and shipped to the browser. + - `~/client/styles.css`: the stylesheet for the application. more could certainly be integrated here. + - `~/client/App.tsx`: the entrypoint for the app, which imports everything else in the folder. +- `~/common`: modules shared between browser and server code. + - `~/common/wasm`: doesn't exist in this repository, but can be added (using e.g. [`wasm-pack`](https://github.com/rustwasm/wasm-pack)) to easily support webassembly modules in both client and server. + - other common utilities as a common import example. +- `~/server`: a simple webserver with example websocket and REST endpoints. +- `~/import_map.json`: the import map! starts with any relevant dependencies. +- `~/Procfile`: an example Procfile showing how to run the service. To convert this to any other form of script, simply trim the prefix `web:`. + +Enable SSR by un-commenting the React and ReactDOMServer lines in `~/server/routes/static.tsx`. diff --git a/assets/example.png b/assets/example.png new file mode 100644 index 0000000000000000000000000000000000000000..413b0da33bf20ad60d55fd60fd2e8d0306c36d05 GIT binary patch literal 18838 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 obSr4D#HvHkN9C<_kXVoLUA + case 'logged-out': return + case 'logged-in': { + return ( + + + + ) + } + default: return assertNever(user) + } +} + +export default function App() { + return ( + + + + ) +} diff --git a/client/api/auth.ts b/client/api/auth.ts new file mode 100644 index 0000000..a8342ba --- /dev/null +++ b/client/api/auth.ts @@ -0,0 +1,37 @@ +// import firebase from 'firebase' +// import 'firebase/auth' + +// const app = firebase.initializeApp({ +// // your-config-here +// }) + +// const auth = app.auth() + +// auto-login +// export function subscribeToAuthState(handleUser: (user: {uid: string; displayName: string} | null) => void) { +// return auth.onAuthStateChanged(handleUser) +// } + +/// +/// Email/password +/// + +// export async function registerBasic(email: string, password: string) { +// const user = await auth.createUserWithEmailAndPassword(email, password) +// } + +// export async function loginBasic(email: string, password: string): Promise { +// const credentials = await auth.signInWithEmailAndPassword(email, password) +// return credentials.user?.uid ?? null +// } + +/// +/// Google +/// + +// export async function loginGoogle(): Promise { +// // @ts-ignore deno does not recognize firebase.auth the way it is imported, but it exists +// const provider = new firebase.auth.GoogleAuthProvider(); +// const {accessToken, credential, user} = await auth.signInWithPopup(provider) +// return user.uid +// } diff --git a/client/app/useSocket.ts b/client/app/useSocket.ts new file mode 100644 index 0000000..dd0e3d4 --- /dev/null +++ b/client/app/useSocket.ts @@ -0,0 +1,83 @@ +import React from 'react' +import useWebSocket from 'react-use-websocket' + +import assertNever from '~/common/assertNever.ts' + +import { useUser } from '../user.tsx' + +function shouldReconnect() { + console.log('Reconnecting...') + return true +} + +type AsyncHandle = + | { status: 'not-connected' } + | { status: 'connecting' } + | { status: 'connected'; handle: T} + +interface SocketAPI {} + +type State = + | { status: 'not-connected' } + | { status: 'connecting' } + | { status: 'connected'} + +type Action = 'open' | 'close' | 'error' +function reducer(_state: State, action: Action): State { + switch (action) { + case 'open': return { status: 'connected' } + case 'close': return { status: 'not-connected' } + case 'error': return { status: 'connecting' } + default: return assertNever(action) + } +} + +const initialState: State = {status: 'not-connected'} + +export default function useSocket(): AsyncHandle { + const _user = useUser() + const [state, dispatch] = React.useReducer(reducer, initialState) + + const onMessage = React.useCallback((message: WebSocketEventMap['message']) => { + const data = JSON.parse(message.data) + dispatch({ type: 'update', ...data }) + }, []) + + const onOpen = React.useCallback(() => { + console.log('socket opened') + dispatch('open') + }, []) + + const onError = React.useCallback((event: WebSocketEventMap['error']) => { + console.error(event) + dispatch('error') + }, []) + + const onClose = React.useCallback((_event: WebSocketEventMap['close']) => { + console.log('socket closed') + dispatch('close') + }, []) + + const url = React.useMemo(() => + `ws://${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}/api/ws?name=${name}`, + [], + ) + const socket = useWebSocket( + url, + {onMessage, onOpen, onError, onClose, shouldReconnect}, + ) + + const handle = React.useMemo(() => ({ + sendJson: (value: {}) => socket.send(JSON.stringify(value)), + }), [socket]) + + switch (state.status) { + case 'connected': { + return {status: 'connected', handle} + } + case 'connecting': + case 'not-connected': + return state + default: return assertNever(state) + } +} diff --git a/client/components/GithubIcon.tsx b/client/components/GithubIcon.tsx new file mode 100644 index 0000000..61a9614 --- /dev/null +++ b/client/components/GithubIcon.tsx @@ -0,0 +1,25 @@ +import React from 'react' + +interface IconProps { + size?: number +} + +/** + * Github icon implemented as specified in https://github.com/logos. + * Reference svg provided by https://commons.wikimedia.org/wiki/File:Octicons-mark-github.svg. + * + * @returns JSX.Element + */ +export default function GithubIcon({size = 24}: IconProps) { + return ( + + + + ) +} diff --git a/client/components/Loading.tsx b/client/components/Loading.tsx new file mode 100644 index 0000000..c59fcde --- /dev/null +++ b/client/components/Loading.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +export default function Loading() { + return ( + Loading... + ) +} diff --git a/client/components/Page.tsx b/client/components/Page.tsx new file mode 100644 index 0000000..b49bdda --- /dev/null +++ b/client/components/Page.tsx @@ -0,0 +1,28 @@ +import React from 'react' + +import GithubIcon from './GithubIcon.tsx' + +interface PageProps { + children: React.ReactNode +} + +export default function Page({children}: PageProps): JSX.Element { + return ( +
+
+ + Deno Web Starter + + + + +
+
+ {children} +
+
+ ) +} diff --git a/client/pages/AppPage.tsx b/client/pages/AppPage.tsx new file mode 100644 index 0000000..b541c61 --- /dev/null +++ b/client/pages/AppPage.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +import Page from '../components/Page.tsx' + +export default function AppPage() { + return ( + +
+

Hello world!

+ +
+
+ ) +} diff --git a/client/pages/LoadingPage.tsx b/client/pages/LoadingPage.tsx new file mode 100644 index 0000000..811d29e --- /dev/null +++ b/client/pages/LoadingPage.tsx @@ -0,0 +1,12 @@ +import React from 'react' + +import Loading from '../components/Loading.tsx' +import Page from '../components/Page.tsx' + +export default function LoadingPage() { + return ( + + + + ) +} diff --git a/client/pages/LoginPage.tsx b/client/pages/LoginPage.tsx new file mode 100644 index 0000000..b790f98 --- /dev/null +++ b/client/pages/LoginPage.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import GoogleButton from 'react-google-button' + +// import { loginGoogle } from '../api/auth.ts' +import Page from '../components/Page.tsx' + +// https://developers.google.com/identity/branding-guidelines +// https://developers.google.com/identity/sign-in/web/sign-in +function GoogleLoginButton() { + const onClick = React.useCallback(async () => { + // await loginGoogle() + }, []) + + return ( + + ) +} + +export default function LoginPage() { + return ( + +
+ +
+
+ ) +} diff --git a/client/styles.css b/client/styles.css new file mode 100644 index 0000000..d29641a --- /dev/null +++ b/client/styles.css @@ -0,0 +1,38 @@ +.page { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; +} + +.header { + height: 90px; + flex: 0 0 auto; + background-color: #4a6670; + padding-left: 2em; + padding-right: 2em; + box-shadow: 0 1em 1em rgba(0, 0, 0, 0.25); + + display: flex; + align-items: center; + justify-content: space-between; +} + +.header-text { + font-size: 32px; + font-family: sans-serif; + color: #18212b; +} + +.main { + flex: auto; + display: flex; + align-items: center; + justify-content: center; + background-color: #e6eaef; +} + +.frame { + display: flex; + justify-content: space-evenly; +} diff --git a/client/user.tsx b/client/user.tsx new file mode 100644 index 0000000..1ad7940 --- /dev/null +++ b/client/user.tsx @@ -0,0 +1,75 @@ +import React from 'react'; + +import type { UserProfile } from '~/common/user.ts' + +// import { subscribeToAuthState } from './api/auth.ts' + +type AuthState = + | {loginState: 'pending'} + | {loginState: 'logged-out'} + // | {loginState: 'logged-in'; userId: string; profile: UserProfile} + | {loginState: 'logged-in'; profile: {}} +type UserHandle = AuthState + +const AuthContext = React.createContext(null) + +export function useAuth(): UserHandle { + const value = React.useContext(AuthContext) + if (value === null) throw new Error('useAuth must be used inside an AuthProvider') + return value +} + +// TODO: UPDATE HERE TO ENABLE AUTH +const initialAuthState: AuthState = {loginState: 'logged-in', profile: {}} + +interface AuthProviderProps { + children: React.ReactNode +} + +/** + * Handles authentication and token storage. + * + * Attaches a provider with login and logout callbacks. Handles authenticating using + * a token found in the device keychain and attaching/removing the token on login/logout. + * + * @props children + */ +export function AuthProvider({children}: AuthProviderProps): JSX.Element { + const [authState, setAuthState] = React.useState(initialAuthState) + + React.useEffect(() => { + // subscribeToAuthState((user) => { + // if (user) { + // setAuthState({ loginState: 'logged-in'}) + // } + // else setAuthState({ loginState: 'logged-out' }) + // }) + }, []) + + return ( + + {children} + + ) +} + +const UserContext = React.createContext(null) + +export function useUser(): UserProfile { + const value = React.useContext(UserContext) + if (value === null) throw new Error('useUser must be used inside a UserProvider') + return value +} + +interface UserProviderProps { + profile: {} + children: React.ReactNode +} + +export function UserProvider({children, profile}: UserProviderProps) { + return ( + + {children} + + ) +} diff --git a/common/assertNever.ts b/common/assertNever.ts new file mode 100644 index 0000000..b0c5a90 --- /dev/null +++ b/common/assertNever.ts @@ -0,0 +1,5 @@ +export default function assertNever(value: never): never { + // TODO an actual error message + throw new Error(`assertNever failed for value ${value}`) + } + \ No newline at end of file diff --git a/common/user.ts b/common/user.ts new file mode 100644 index 0000000..917ca98 --- /dev/null +++ b/common/user.ts @@ -0,0 +1,3 @@ +export interface UserProfile { +} + \ No newline at end of file diff --git a/import_map.json b/import_map.json new file mode 100644 index 0000000..593c766 --- /dev/null +++ b/import_map.json @@ -0,0 +1,18 @@ +{ + "imports": { + "~/": "./", + + "firebase": "https://cdn.skypack.dev/firebase@8.7.0/app", + "firebase/auth": "https://cdn.skypack.dev/firebase@8.7.0/auth", + "firebase/firestore": "https://cdn.skypack.dev/firebase@8.7.0/firestore", + "http": "https://deno.land/std@0.105.0/http/mod.ts", + "media-types": "https://deno.land/x/media_types@v2.10.0/mod.ts", + "oak": "https://deno.land/x/oak@v9.0.1/mod.ts", + "react": "https://cdn.esm.sh/react@17", + "react-dom": "https://cdn.esm.sh/react-dom@17", + "react-dom/": "https://cdn.esm.sh/react-dom@17/", + "react-google-button": "https://cdn.esm.sh/react-google-button@0.7.2?deps=react@17", + "react-use-websocket": "https://cdn.esm.sh/react-use-websocket@2.7?deps=react@17" + }, + "scopes": {} +} diff --git a/server/client.tsx b/server/client.tsx new file mode 100644 index 0000000..717e569 --- /dev/null +++ b/server/client.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +//// import any necessary wasm file here +// import initWasm from '~/common/wasm/wasm_chess.js' + +import App from '~/client/App.tsx' + +//// init wasm file here +//// fetch file contents from webserver so browser has access +// initWasm(fetch('wasm_chess.wasm')) + +window.addEventListener('load', (event) => { + ReactDOM.hydrate(, document.getElementById('root')) +}) diff --git a/server/routes/api.ts b/server/routes/api.ts new file mode 100644 index 0000000..792dc22 --- /dev/null +++ b/server/routes/api.ts @@ -0,0 +1,19 @@ +import { Router } from 'oak' + +function handleSocket(socket: WebSocket) { + console.log(socket) +} + +const apiRouter = new Router() + +apiRouter + .get('/api/ws', async (context) => { + if (!context.isUpgradable) throw new Error('Context not upgradable.') + const ws = await context.upgrade() + handleSocket(ws) + }) + .get('/api/hello', (context) => { + context.response.body = "Hello world!"; + }) + +export default apiRouter diff --git a/server/routes/static.tsx b/server/routes/static.tsx new file mode 100644 index 0000000..3d7f595 --- /dev/null +++ b/server/routes/static.tsx @@ -0,0 +1,139 @@ +// import React from 'react' +// import ReactDOMServer from 'react-dom/server' +import { contentType } from 'media-types' +import { Router } from 'oak' + +// import App from '~/client/App.tsx' + +async function createClientBundle() { + const {files} = await Deno.emit('server/client.tsx', { + bundle: 'module', + importMapPath: 'import_map.json', + compilerOptions: { + lib: ["dom", "dom.iterable", "esnext"], + allowJs: true, + jsx: "react", + strictPropertyInitialization: false, + }, + }) + return files +} + +const bundle = await createClientBundle() + +function getBundleFile(path: string) { + return bundle[`deno:///${path}`] +} + +function getAssetFile(file: string): Uint8Array | null { + try { + const asset = Deno.readFileSync(`assets/${file}`) + return asset + } catch { + return null + } +} + +const html = ` + + + + + + Deno React Web Starter + + + + + + +
+ ${ /* ReactDOMServer.renderToString() */ '' } +
+ + + +` + +const staticRouter = new Router() + +// handle static routes by proxying to web resources +// https://deno.com/deploy/docs/serve-static-assets +staticRouter + .get('/', (ctx) => { + ctx.response.body = html + }) + // // serve wasm file + // .get('/wasm.wasm', async (ctx) => { + // const headers = new Headers(ctx.response.headers) + // headers.set('Content-Type', 'application/wasm') + // ctx.response.headers = headers + // const body = await Deno.readFile('common/wasm/wasm_bg.wasm') + // ctx.response.body = body + // }) + .get('/styles.css', (ctx) => { + const styles = Deno.readTextFileSync('client/styles.css') + const contentTypeValue = contentType('styles.css') + const headers = new Headers(ctx.response.headers) + if (contentTypeValue) { + headers.set('Content-Type', contentTypeValue) + } + ctx.response.headers = headers + ctx.response.body = styles + }) + .get('/assets/:pathname', async (ctx, next) => { + const assetPath = ctx.params.pathname + if (!assetPath) { + return await next() + } + + const file = getAssetFile(assetPath) + if (!file) { + return await next() + } + + const filePathParts = assetPath.split('/') + const contentTypeValue = contentType(filePathParts[filePathParts.length - 1]) + const headers = new Headers(ctx.response.headers) + if (contentTypeValue) { + headers.set('Content-Type', contentTypeValue) + } + ctx.response.headers = headers + ctx.response.body = file + }) + .get('/:pathname', async (ctx, next) => { + const pathname = ctx.params.pathname + if (!pathname) { + return await next() + } + + const file = getBundleFile(pathname) + if (!file) { + return await next() + } + + // get just the last bit so we can determine the correct filetype + // contentType from media-types@v2.10.0 checks path.includes('/') + const filePathParts = pathname.split('/') + const contentTypeValue = contentType(filePathParts[filePathParts.length - 1]) + const headers = new Headers(ctx.response.headers) + if (contentTypeValue) { + headers.set('Content-Type', contentTypeValue) + } + ctx.response.headers = headers + if (pathname.endsWith('.js')) { + ctx.response.type = 'application/javascript' + } + ctx.response.body = file + }) + +export default staticRouter diff --git a/server/server.ts b/server/server.ts new file mode 100644 index 0000000..9226b3c --- /dev/null +++ b/server/server.ts @@ -0,0 +1,51 @@ +import { Application, Status } from 'oak' + +//// import any necessary wasm file here +// import init from '~/common/wasm.js' + +import apiRouter from './routes/api.ts' +import staticRouter from './routes/static.tsx' + +const app = new Application() + +app.addEventListener("error", (event) => { + console.error(event.error); +}) + +app.use(async (ctx, next) => { + try { + await next(); + } catch (error) { + ctx.response.body = "Internal server error"; + throw error; + } +}); +app.use((ctx, next) => { + ctx.response.headers.set('Access-Control-Allow-Origin', '*') + // avoid CSP concerns + // ctx.response.headers.delete("content-security-policy"); + console.log(ctx.request.url.pathname) + return next() +}) + +app.use(apiRouter.routes()) +app.use(apiRouter.allowedMethods()) + +app.use(staticRouter.routes()) +app.use(staticRouter.allowedMethods()) + +// 404 +app.use((context) => { + context.response.status = Status.NotFound + context.response.body = `"${context.request.url}" not found` +}) + +//// init wasm file here +//// https://deno.com/deploy/docs/serve-static-assets +// const WASM_PATH = new URL('../common/wasm/wasm_bg.wasm', import.meta.url) +//// https://github.com/rustwasm/wasm-pack/issues/672#issuecomment-813630435 +// await init(Deno.readFile(WASM_PATH)) + +const port = +(Deno.env.get('PORT') ?? 8080) +console.log(`Listening on port ${port}...`) +app.listen(`:${port}`) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..16dab82 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext", "deno.ns", "deno.unstable"], + "allowJs": true, + "jsxFactory": "react", + "strictPropertyInitialization": false, + } +}