Skip to content

Commit 4069ff4

Browse files
committed
Allow useDeno and ssr.props access Request (close #22, #364, #401)
1 parent 1de0a35 commit 4069ff4

File tree

8 files changed

+121
-48
lines changed

8 files changed

+121
-48
lines changed

framework/react/context.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { createContext, ReactNode } from 'https://esm.sh/[email protected]'
21
import type { RouterURL } from '../../types.d.ts'
2+
import type { RendererStore } from './renderer.ts'
3+
import { createContext, ReactNode } from 'https://esm.sh/[email protected]'
34
import { createBlankRouterURL } from '../core/routing.ts'
45
import { createNamedContext } from './helper.ts'
5-
import type { RendererStore } from './renderer.ts'
66

77
export const RouterContext = createNamedContext<RouterURL>(createBlankRouterURL(), 'RouterContext')
88
export const FallbackContext = createNamedContext<{ to: ReactNode }>({ to: null }, 'FallbackContext')
99
export const SSRContext = createContext<RendererStore>({
10+
request: new Request('http://localhost/'),
11+
dataCache: {},
1012
headElements: new Map(),
1113
inlineStyles: new Map(),
1214
scripts: new Map(),

framework/react/hooks.ts

+20-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { useContext, useMemo } from 'https://esm.sh/[email protected]'
21
import type { RouterURL } from '../../types.d.ts'
2+
import { useContext, useMemo } from 'https://esm.sh/[email protected]'
3+
import util from '../../shared/util.ts'
34
import events from '../core/events.ts'
4-
import { RouterContext } from './context.ts'
5+
import { RouterContext, SSRContext } from './context.ts'
56
import { inDeno } from './helper.ts'
67

78
export class AsyncUseDenoError extends Error { }
@@ -30,40 +31,43 @@ export function useRouter(): RouterURL {
3031
* }
3132
* ```
3233
*/
33-
export function useDeno<T = any>(callback: () => (T | Promise<T>), options?: { key?: string | number, revalidate?: number }): T {
34+
export function useDeno<T = any>(callback: (request: Request) => (T | Promise<T>), options?: { key?: string | number, revalidate?: number | boolean | { date: number } }): T {
3435
const { key, revalidate } = options || {}
3536
const uuid = arguments[2] // generated by compiler
36-
const router = useRouter()
3737
const id = useMemo(() => [uuid, key].filter(Boolean).join('-'), [key])
38+
const { request, dataCache } = useContext(SSRContext)
39+
const router = useRouter()
3840

3941
return useMemo(() => {
40-
const global = window as any
4142
const href = router.toString()
4243
const dataUrl = `pagedata://${href}`
4344

4445
if (inDeno) {
45-
const renderingData = global[`rendering-${dataUrl}`]
46-
47-
if (renderingData && id in renderingData) {
48-
return renderingData[id] // 2+ pass
46+
if (id in dataCache) {
47+
return dataCache[id] // 2+ pass
4948
}
5049

51-
const value = callback()
52-
const expires = typeof revalidate === 'number' && !isNaN(revalidate) ? Date.now() + revalidate * 1000 : 0
50+
let expires = 0
51+
if (util.isNumber(revalidate)) {
52+
expires = Date.now() + Math.round(revalidate * 1000)
53+
} else if (revalidate === true) {
54+
expires = Date.now() - 1000
55+
} else if (util.isPlainObject(revalidate) && util.isNumber(revalidate.date)) {
56+
expires = revalidate.date
57+
}
58+
const value = callback(request)
5359
events.emit(`useDeno-${dataUrl}`, { id, value, expires })
5460

5561
// thow an `AsyncUseDenoError` to break current rendering
5662
if (value instanceof Promise) {
5763
throw new AsyncUseDenoError()
5864
}
5965

60-
if (renderingData) {
61-
renderingData[id] = value
62-
}
66+
dataCache[id] = value
6367
return value
6468
}
6569

66-
const data = global[`${dataUrl}#${id}`]
70+
const data = (window as any)[`${dataUrl}#${id}`]
6771
return data?.value
68-
}, [id, router])
72+
}, [id, router, dataCache, request])
6973
}

framework/react/renderer.ts

+16-10
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import type { FrameworkRenderResult } from '../../server/renderer.ts'
2+
import type { RouterURL } from '../../types.d.ts'
13
import { createElement, ComponentType, ReactElement } from 'https://esm.sh/[email protected]'
24
import { renderToString } from 'https://esm.sh/[email protected]/server'
35
import util from '../../shared/util.ts'
4-
import type { FrameworkRenderResult } from '../../server/renderer.ts'
5-
import type { RouterURL } from '../../types.d.ts'
66
import events from '../core/events.ts'
77
import { RouterContext, SSRContext } from './context.ts'
88
import { E400MissingComponent, E404Page } from './components/ErrorBoundary.ts'
@@ -11,12 +11,15 @@ import { isLikelyReactComponent } from './helper.ts'
1111
import { createPageProps } from './pageprops.ts'
1212

1313
export type RendererStore = {
14+
request: Request
15+
dataCache: Record<string, any>
1416
headElements: Map<string, { type: string, props: Record<string, any> }>
1517
inlineStyles: Map<string, string>
1618
scripts: Map<string, { props: Record<string, any> }>
1719
}
1820

1921
export async function render(
22+
request: Request,
2023
url: RouterURL,
2124
App: ComponentType<any> | undefined,
2225
nestedPageComponents: { specifier: string, Component?: any, props?: Record<string, any> }[],
@@ -30,6 +33,8 @@ export async function render(
3033
data: null,
3134
}
3235
const rendererStore: RendererStore = {
36+
request: request,
37+
dataCache: {},
3338
headElements: new Map(),
3439
inlineStyles: new Map(),
3540
scripts: new Map(),
@@ -38,7 +43,6 @@ export async function render(
3843
const dataKey = 'rendering-' + dataUrl
3944
const asyncCalls: Array<[string, number, Promise<any>]> = []
4045
const data: Record<string, any> = {}
41-
const renderingData: Record<string, any> = {}
4246
const pageProps = createPageProps(nestedPageComponents)
4347
const defer = async () => {
4448
Reflect.deleteProperty(global, dataKey)
@@ -47,9 +51,14 @@ export async function render(
4751

4852
nestedPageComponents.forEach(({ specifier, props }) => {
4953
if (util.isPlainObject(props)) {
54+
const { $revalidate } = props
5055
let expires = 0
51-
if (typeof props.$revalidate === 'number' && props.$revalidate > 0) {
52-
expires = Date.now() + Math.round(props.$revalidate * 1000)
56+
if (util.isNumber($revalidate)) {
57+
expires = Date.now() + Math.round($revalidate * 1000)
58+
} else if ($revalidate === true) {
59+
expires = Date.now() - 1000
60+
} else if (util.isPlainObject($revalidate) && util.isNumber($revalidate.date)) {
61+
expires = $revalidate.date
5362
}
5463
data[`props-${btoa(specifier)}`] = {
5564
value: props,
@@ -58,9 +67,6 @@ export async function render(
5867
}
5968
})
6069

61-
// share rendering data
62-
global[dataKey] = renderingData
63-
6470
// listen `useDeno-*` events to get hooks callback result.
6571
events.on('useDeno-' + dataUrl, ({ id, value, expires }: { id: string, value: any, expires: number }) => {
6672
if (value instanceof Promise) {
@@ -96,12 +102,12 @@ export async function render(
96102
const datas = await Promise.all(calls.map(a => a[2]))
97103
calls.forEach(([id, expires], i) => {
98104
const value = datas[i]
99-
renderingData[id] = value
105+
rendererStore.dataCache[id] = value
100106
data[id] = { value, expires }
101107
})
102108
}
109+
Object.values(rendererStore).forEach(v => v instanceof Map && v.clear())
103110
try {
104-
Object.values(rendererStore).forEach(map => map.clear())
105111
ret.body = renderToString(createElement(
106112
SSRContext.Provider,
107113
{ value: rendererStore },

server/aleph.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ export class Aleph implements IAleph {
546546
}
547547

548548
/** get ssr data by the given location(page), return `null` if no data defined */
549-
async getSSRData(loc: { pathname: string, search?: string }): Promise<Record<string, SSRData> | null> {
549+
async getSSRData(request: Request, loc: { pathname: string, search?: string }): Promise<Record<string, SSRData> | null> {
550550
const [router, nestedModules] = this.#pageRouting.createRouter(loc)
551551
const { routePath } = router
552552
if (routePath === '' || !this.#isSSRable(router.pathname)) {
@@ -566,7 +566,7 @@ export class Aleph implements IAleph {
566566

567567
const path = loc.pathname + (loc.search || '')
568568
const [_, data] = await this.#renderer.cache(routePath, path, async () => {
569-
return await this.#renderPage(router, nestedModules)
569+
return await this.#renderPage(request, router, nestedModules)
570570
})
571571
return data
572572
}
@@ -600,7 +600,7 @@ export class Aleph implements IAleph {
600600
}
601601

602602
/** render page to HTML by the given location */
603-
async renderPage(loc: { pathname: string, search?: string }): Promise<[number, string]> {
603+
async renderPage(request: Request, loc: { pathname: string, search?: string }): Promise<[number, string]> {
604604
const [router, nestedModules] = this.#pageRouting.createRouter(loc)
605605
const { routePath } = router
606606
const path = loc.pathname + (loc.search || '')
@@ -615,19 +615,19 @@ export class Aleph implements IAleph {
615615
if (routePath === '') {
616616
const [html] = await this.#renderer.cache('404', path, async () => {
617617
const [_, nestedModules] = this.#pageRouting.createRouter({ pathname: '/404' })
618-
return await this.#renderPage(router, nestedModules.slice(0, 1))
618+
return await this.#renderPage(request, router, nestedModules.slice(0, 1))
619619
})
620620
return [404, html]
621621
}
622622

623623
const [html] = await this.#renderer.cache(routePath, path, async () => {
624-
return await this.#renderPage(router, nestedModules)
624+
return await this.#renderPage(request, router, nestedModules)
625625
})
626626
return [200, html]
627627
}
628628

629-
async #renderPage(url: RouterURL, nestedModules: string[]): Promise<[string, Record<string, SSRData> | null]> {
630-
let [html, data] = await this.#renderer.renderPage(url, nestedModules)
629+
async #renderPage(request: Request, url: RouterURL, nestedModules: string[]): Promise<[string, Record<string, SSRData> | null]> {
630+
let [html, data] = await this.#renderer.renderPage(request, url, nestedModules)
631631
for (const callback of this.#renderListeners) {
632632
await callback({ path: url.toString(), html, data })
633633
}
@@ -1534,12 +1534,13 @@ export class Aleph implements IAleph {
15341534
}
15351535

15361536
// render route pages
1537+
const req = new Request('http://localhost/')
15371538
await Promise.all(Array.from(paths).map(loc => ([loc, ...locales.map(locale => ({ ...loc, pathname: '/' + locale + loc.pathname }))])).flat().map(async ({ pathname, search }) => {
15381539
if (this.#isSSRable(pathname)) {
15391540
const [router, nestedModules] = this.#pageRouting.createRouter({ pathname, search })
15401541
if (router.routePath !== '') {
15411542
const href = router.toString()
1542-
const [html, data] = await this.#renderPage(router, nestedModules)
1543+
const [html, data] = await this.#renderPage(req, router, nestedModules)
15431544
await ensureTextFile(join(outputDir, pathname, 'index.html' + (search || '')), html)
15441545
if (data) {
15451546
const dataFile = join(
@@ -1559,7 +1560,7 @@ export class Aleph implements IAleph {
15591560
if (nestedModules.length > 0) {
15601561
await this.compile(nestedModules[0])
15611562
}
1562-
const [html] = await this.#renderPage(router, nestedModules.slice(0, 1))
1563+
const [html] = await this.#renderPage(req, router, nestedModules.slice(0, 1))
15631564
await ensureTextFile(join(outputDir, '404.html'), html)
15641565
}
15651566
}

server/renderer.ts

+47-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import type { HtmlDescriptor, Module, RouterURL, SSRData, SSRRequest } from '../types.d.ts'
2+
import type { Aleph } from './aleph.ts'
13
import { basename, dirname } from 'https://deno.land/[email protected]/path/mod.ts'
24
import log from '../shared/log.ts'
35
import util from '../shared/util.ts'
4-
import type { HtmlDescriptor, Module, RouterURL, SSRData } from '../types.d.ts'
5-
import type { Aleph } from './aleph.ts'
66

77
/** The render result of framework SSR. */
88
export type FrameworkRenderResult = {
@@ -15,6 +15,7 @@ export type FrameworkRenderResult = {
1515
/** The renderer of framework SSR. */
1616
export type FrameworkRenderer = {
1717
render(
18+
request: Request,
1819
url: RouterURL,
1920
AppComponent: any,
2021
nestedPageComponents: { specifier: string, Component?: any, props?: Record<string, any> }[],
@@ -81,7 +82,7 @@ export class Renderer {
8182
}
8283

8384
/** render page base the given location. */
84-
async renderPage(url: RouterURL, nestedModules: string[]): Promise<[HtmlDescriptor, Record<string, SSRData> | null]> {
85+
async renderPage(request: Request, url: RouterURL, nestedModules: string[]): Promise<[HtmlDescriptor, Record<string, SSRData> | null]> {
8586
const start = performance.now()
8687
const isDev = this.#aleph.isDev
8788
const state = { entryFile: '' }
@@ -96,7 +97,7 @@ export class Renderer {
9697
const { default: Component, ssr } = await this.#aleph.importModule(module)
9798
let ssrProps = ssr?.props
9899
if (util.isFunction(ssrProps)) {
99-
ssrProps = ssrProps(url)
100+
ssrProps = ssrProps(new SSRRequestImpl(url, request))
100101
if (ssrProps instanceof Promise) {
101102
ssrProps = await ssrProps
102103
}
@@ -115,6 +116,7 @@ export class Renderer {
115116
].flat())
116117

117118
const { head, body, data, scripts } = await this.#renderer.render(
119+
request,
118120
url,
119121
App,
120122
nestedPageComponents,
@@ -173,6 +175,47 @@ export class Renderer {
173175
}
174176
}
175177

178+
class SSRRequestImpl extends Request implements SSRRequest {
179+
#url: RouterURL
180+
181+
constructor(url: RouterURL, raw: Request) {
182+
super(raw)
183+
this.#url = url
184+
}
185+
186+
get basePath() {
187+
return this.#url.basePath
188+
}
189+
190+
get routePath() {
191+
return this.#url.routePath
192+
}
193+
194+
get locale() {
195+
return this.#url.locale
196+
}
197+
198+
get defaultLocale() {
199+
return this.#url.defaultLocale
200+
}
201+
202+
get locales() {
203+
return this.#url.locales
204+
}
205+
206+
get pathname() {
207+
return this.#url.pathname
208+
}
209+
210+
get params() {
211+
return this.#url.params
212+
}
213+
214+
get query() {
215+
return this.#url.query
216+
}
217+
}
218+
176219
/** build html content by given descriptor */
177220
export function buildHtml({
178221
body,

server/server.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { join } from 'https://deno.land/[email protected]/path/mod.ts'
2-
import { readerFromStreamReader } from "https://deno.land/[email protected]/io/streams.ts"
3-
import { readAll } from "https://deno.land/[email protected]/io/util.ts"
2+
import { readerFromStreamReader } from 'https://deno.land/[email protected]/io/streams.ts'
3+
import { readAll } from 'https://deno.land/[email protected]/io/util.ts'
44
import { builtinModuleExts, trimBuiltinModuleExts } from '../framework/core/module.ts'
55
import { resolveURL } from '../framework/core/routing.ts'
66
import { existsFile } from '../shared/fs.ts'
@@ -142,7 +142,7 @@ export class Server {
142142
if (pathname.startsWith('/_aleph/')) {
143143
if (pathname.startsWith('/_aleph/data/') && pathname.endsWith('.json')) {
144144
const [path, search] = util.splitBy(util.atobUrl(util.trimSuffix(util.trimPrefix(pathname, '/_aleph/data/'), '.json')), '?')
145-
const data = await aleph.getSSRData({ pathname: path, search: search ? '?' + search : undefined })
145+
const data = await aleph.getSSRData(req, { pathname: path, search: search ? '?' + search : undefined })
146146
if (data === null) {
147147
resp.json(null)
148148
end()
@@ -299,7 +299,7 @@ export class Server {
299299
}
300300

301301
// ssr
302-
const [status, html] = await aleph.renderPage({
302+
const [status, html] = await aleph.renderPage(req, {
303303
pathname,
304304
search: Array.from(url.searchParams.keys()).length > 0 ? '?' + url.searchParams.toString() : ''
305305
})

shared/util.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
export default {
2+
isNumber(a: any): a is number {
3+
return typeof a === 'number' && !isNaN(a)
4+
},
25
isString(a: any): a is string {
36
return typeof a === 'string'
47
},
@@ -122,7 +125,7 @@ export default {
122125
},
123126
async isUrlOk(url: string): Promise<boolean> {
124127
const res = await fetch(url).catch((e) => e)
125-
await res.body?.cancel();
128+
await res.body?.cancel()
126129
return res.status === 200
127130
}
128131
}

0 commit comments

Comments
 (0)