Initial commit
This commit is contained in:
commit
3cb3286b71
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"deno.enable": true,
|
||||
"deno.lint": true,
|
||||
"deno.unstable": true,
|
||||
"deno.importMap": "./import_map.json"
|
||||
}
|
1
Procfile
Normal file
1
Procfile
Normal file
@ -0,0 +1 @@
|
||||
web: deno run --allow-read --unstable --allow-env --allow-net --import-map import_map.json ./server/server.ts
|
25
README.md
Normal file
25
README.md
Normal file
@ -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`.
|
BIN
assets/example.png
Normal file
BIN
assets/example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
33
client/App.tsx
Normal file
33
client/App.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
|
||||
import assertNever from '~/common/assertNever.ts'
|
||||
|
||||
import AppPage from './pages/AppPage.tsx'
|
||||
import LoadingPage from './pages/LoadingPage.tsx'
|
||||
import LoginPage from './pages/LoginPage.tsx'
|
||||
import { AuthProvider, UserProvider, useAuth } from './user.tsx'
|
||||
|
||||
function AuthRouter() {
|
||||
const user = useAuth()
|
||||
|
||||
switch (user.loginState) {
|
||||
case 'pending': return <LoadingPage />
|
||||
case 'logged-out': return <LoginPage />
|
||||
case 'logged-in': {
|
||||
return (
|
||||
<UserProvider profile={user.profile}>
|
||||
<AppPage />
|
||||
</UserProvider>
|
||||
)
|
||||
}
|
||||
default: return assertNever(user)
|
||||
}
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<AuthRouter />
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
37
client/api/auth.ts
Normal file
37
client/api/auth.ts
Normal file
@ -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<string | null> {
|
||||
// const credentials = await auth.signInWithEmailAndPassword(email, password)
|
||||
// return credentials.user?.uid ?? null
|
||||
// }
|
||||
|
||||
///
|
||||
/// Google
|
||||
///
|
||||
|
||||
// export async function loginGoogle(): Promise<string> {
|
||||
// // @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
|
||||
// }
|
83
client/app/useSocket.ts
Normal file
83
client/app/useSocket.ts
Normal file
@ -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<T> =
|
||||
| { 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<SocketAPI> {
|
||||
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)
|
||||
}
|
||||
}
|
25
client/components/GithubIcon.tsx
Normal file
25
client/components/GithubIcon.tsx
Normal file
@ -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 (
|
||||
<svg width={size} height={size} viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"
|
||||
transform="scale(64)"
|
||||
fill="#1b1f23"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
7
client/components/Loading.tsx
Normal file
7
client/components/Loading.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<span>Loading...</span>
|
||||
)
|
||||
}
|
28
client/components/Page.tsx
Normal file
28
client/components/Page.tsx
Normal file
@ -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 (
|
||||
<div className="page">
|
||||
<header className="header">
|
||||
<span className="header-text">
|
||||
Deno Web Starter
|
||||
</span>
|
||||
<a
|
||||
href="https://github.com/sullivansean27/deno-react-web-starter"
|
||||
target="_blank"
|
||||
>
|
||||
<GithubIcon size={60} />
|
||||
</a>
|
||||
</header>
|
||||
<main className="main">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
14
client/pages/AppPage.tsx
Normal file
14
client/pages/AppPage.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
import Page from '../components/Page.tsx'
|
||||
|
||||
export default function AppPage() {
|
||||
return (
|
||||
<Page>
|
||||
<div>
|
||||
<p>Hello world!</p>
|
||||
<img style={{width: 100, height: 100}} src="/assets/example.png" />
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
}
|
12
client/pages/LoadingPage.tsx
Normal file
12
client/pages/LoadingPage.tsx
Normal file
@ -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 (
|
||||
<Page>
|
||||
<Loading />
|
||||
</Page>
|
||||
)
|
||||
}
|
27
client/pages/LoginPage.tsx
Normal file
27
client/pages/LoginPage.tsx
Normal file
@ -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 (
|
||||
<GoogleButton onClick={onClick} />
|
||||
)
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<Page>
|
||||
<div>
|
||||
<GoogleLoginButton />
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
}
|
38
client/styles.css
Normal file
38
client/styles.css
Normal file
@ -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;
|
||||
}
|
75
client/user.tsx
Normal file
75
client/user.tsx
Normal file
@ -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<UserHandle | null>(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<AuthState>(initialAuthState)
|
||||
|
||||
React.useEffect(() => {
|
||||
// subscribeToAuthState((user) => {
|
||||
// if (user) {
|
||||
// setAuthState({ loginState: 'logged-in'})
|
||||
// }
|
||||
// else setAuthState({ loginState: 'logged-out' })
|
||||
// })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={authState}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const UserContext = React.createContext<UserProfile | null>(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 (
|
||||
<UserContext.Provider value={profile}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
)
|
||||
}
|
5
common/assertNever.ts
Normal file
5
common/assertNever.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default function assertNever(value: never): never {
|
||||
// TODO an actual error message
|
||||
throw new Error(`assertNever failed for value ${value}`)
|
||||
}
|
||||
|
3
common/user.ts
Normal file
3
common/user.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface UserProfile {
|
||||
}
|
||||
|
18
import_map.json
Normal file
18
import_map.json
Normal file
@ -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": {}
|
||||
}
|
15
server/client.tsx
Normal file
15
server/client.tsx
Normal file
@ -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(<App />, document.getElementById('root'))
|
||||
})
|
19
server/routes/api.ts
Normal file
19
server/routes/api.ts
Normal file
@ -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
|
139
server/routes/static.tsx
Normal file
139
server/routes/static.tsx
Normal file
@ -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 = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Deno React Web Starter</title>
|
||||
<meta name="description" content="Template for SSR React apps with Deno, Oak, and Firebase." />
|
||||
<link href="styles.css" rel="stylesheet" type="text/css">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body, #root {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">
|
||||
${ /* ReactDOMServer.renderToString(<App />) */ '' }
|
||||
</div>
|
||||
<script src="/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
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
|
51
server/server.ts
Normal file
51
server/server.ts
Normal file
@ -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}`)
|
8
tsconfig.json
Normal file
8
tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext", "deno.ns", "deno.unstable"],
|
||||
"allowJs": true,
|
||||
"jsxFactory": "react",
|
||||
"strictPropertyInitialization": false,
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user