Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow useDeno and ssr.props access Request #404

Merged
merged 3 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { ComponentType } from 'react'
import React, { FC } from 'react'

export default function App({ Page, pageProps }: { Page: ComponentType<any>, pageProps: any }) {
export default function App({ Page, pageProps }: { Page: FC, pageProps: any }) {
return (
<main>
<head>
Expand Down
26 changes: 26 additions & 0 deletions examples/hello-world-isr/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { SSROptions } from 'aleph/types.d.ts'
import React, { FC } from 'react'

type Props = {
serverTime: number
ua: string | null
}

export const ssr: SSROptions<Props> = {
props: req => ({
$revalidate: 1, // revalidate props after 1 second
serverTime: Date.now(),
ua: req.headers.get('User-Agent'),
})
}

const Page: FC<Props> = (props) => {
return (
<>
<p>Now: {props.serverTime}</p>
<p>UA: {props.ua}</p>
</>
)
}

export default Page
26 changes: 0 additions & 26 deletions examples/hello-world-ssr/pages/index.tsx

This file was deleted.

6 changes: 4 additions & 2 deletions framework/react/context.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { createContext, ReactNode } from 'https://esm.sh/[email protected]'
import type { RouterURL } from '../../types.d.ts'
import type { RendererStore } from './renderer.ts'
import { createContext, ReactNode } from 'https://esm.sh/[email protected]'
import { createBlankRouterURL } from '../core/routing.ts'
import { createNamedContext } from './helper.ts'
import type { RendererStore } from './renderer.ts'

export const RouterContext = createNamedContext<RouterURL>(createBlankRouterURL(), 'RouterContext')
export const FallbackContext = createNamedContext<{ to: ReactNode }>({ to: null }, 'FallbackContext')
export const SSRContext = createContext<RendererStore>({
request: new Request('http://localhost/'),
dataCache: {},
headElements: new Map(),
inlineStyles: new Map(),
scripts: new Map(),
Expand Down
36 changes: 20 additions & 16 deletions framework/react/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useContext, useMemo } from 'https://esm.sh/[email protected]'
import type { RouterURL } from '../../types.d.ts'
import { useContext, useMemo } from 'https://esm.sh/[email protected]'
import util from '../../shared/util.ts'
import events from '../core/events.ts'
import { RouterContext } from './context.ts'
import { RouterContext, SSRContext } from './context.ts'
import { inDeno } from './helper.ts'

export class AsyncUseDenoError extends Error { }
Expand Down Expand Up @@ -30,40 +31,43 @@ export function useRouter(): RouterURL {
* }
* ```
*/
export function useDeno<T = any>(callback: () => (T | Promise<T>), options?: { key?: string | number, revalidate?: number }): T {
export function useDeno<T = any>(callback: (request: Request) => (T | Promise<T>), options?: { key?: string | number, revalidate?: number | boolean | { date: number } }): T {
const { key, revalidate } = options || {}
const uuid = arguments[2] // generated by compiler
const router = useRouter()
const id = useMemo(() => [uuid, key].filter(Boolean).join('-'), [key])
const { request, dataCache } = useContext(SSRContext)
const router = useRouter()

return useMemo(() => {
const global = window as any
const href = router.toString()
const dataUrl = `pagedata://${href}`

if (inDeno) {
const renderingData = global[`rendering-${dataUrl}`]

if (renderingData && id in renderingData) {
return renderingData[id] // 2+ pass
if (id in dataCache) {
return dataCache[id] // 2+ pass
}

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

// thow an `AsyncUseDenoError` to break current rendering
if (value instanceof Promise) {
throw new AsyncUseDenoError()
}

if (renderingData) {
renderingData[id] = value
}
dataCache[id] = value
return value
}

const data = global[`${dataUrl}#${id}`]
const data = (window as any)[`${dataUrl}#${id}`]
return data?.value
}, [id, router])
}, [id, router, dataCache, request])
}
26 changes: 16 additions & 10 deletions framework/react/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { FrameworkRenderResult } from '../../server/renderer.ts'
import type { RouterURL } from '../../types.d.ts'
import { createElement, ComponentType, ReactElement } from 'https://esm.sh/[email protected]'
import { renderToString } from 'https://esm.sh/[email protected]/server'
import util from '../../shared/util.ts'
import type { FrameworkRenderResult } from '../../server/renderer.ts'
import type { RouterURL } from '../../types.d.ts'
import events from '../core/events.ts'
import { RouterContext, SSRContext } from './context.ts'
import { E400MissingComponent, E404Page } from './components/ErrorBoundary.ts'
Expand All @@ -11,12 +11,15 @@ import { isLikelyReactComponent } from './helper.ts'
import { createPageProps } from './pageprops.ts'

export type RendererStore = {
request: Request
dataCache: Record<string, any>
headElements: Map<string, { type: string, props: Record<string, any> }>
inlineStyles: Map<string, string>
scripts: Map<string, { props: Record<string, any> }>
}

export async function render(
request: Request,
url: RouterURL,
App: ComponentType<any> | undefined,
nestedPageComponents: { specifier: string, Component?: any, props?: Record<string, any> }[],
Expand All @@ -30,6 +33,8 @@ export async function render(
data: null,
}
const rendererStore: RendererStore = {
request: request,
dataCache: {},
headElements: new Map(),
inlineStyles: new Map(),
scripts: new Map(),
Expand All @@ -38,7 +43,6 @@ export async function render(
const dataKey = 'rendering-' + dataUrl
const asyncCalls: Array<[string, number, Promise<any>]> = []
const data: Record<string, any> = {}
const renderingData: Record<string, any> = {}
const pageProps = createPageProps(nestedPageComponents)
const defer = async () => {
Reflect.deleteProperty(global, dataKey)
Expand All @@ -47,9 +51,14 @@ export async function render(

nestedPageComponents.forEach(({ specifier, props }) => {
if (util.isPlainObject(props)) {
const { $revalidate } = props
let expires = 0
if (typeof props.$revalidate === 'number' && props.$revalidate > 0) {
expires = Date.now() + Math.round(props.$revalidate * 1000)
if (util.isNumber($revalidate)) {
expires = Date.now() + Math.round($revalidate * 1000)
} else if ($revalidate === true) {
expires = Date.now() - 1000
} else if (util.isPlainObject($revalidate) && util.isNumber($revalidate.date)) {
expires = $revalidate.date
}
data[`props-${btoa(specifier)}`] = {
value: props,
Expand All @@ -58,9 +67,6 @@ export async function render(
}
})

// share rendering data
global[dataKey] = renderingData

// listen `useDeno-*` events to get hooks callback result.
events.on('useDeno-' + dataUrl, ({ id, value, expires }: { id: string, value: any, expires: number }) => {
if (value instanceof Promise) {
Expand Down Expand Up @@ -96,12 +102,12 @@ export async function render(
const datas = await Promise.all(calls.map(a => a[2]))
calls.forEach(([id, expires], i) => {
const value = datas[i]
renderingData[id] = value
rendererStore.dataCache[id] = value
data[id] = { value, expires }
})
}
Object.values(rendererStore).forEach(v => v instanceof Map && v.clear())
try {
Object.values(rendererStore).forEach(map => map.clear())
ret.body = renderToString(createElement(
SSRContext.Provider,
{ value: rendererStore },
Expand Down
19 changes: 10 additions & 9 deletions server/aleph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ export class Aleph implements IAleph {
}

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

const path = loc.pathname + (loc.search || '')
const [_, data] = await this.#renderer.cache(routePath, path, async () => {
return await this.#renderPage(router, nestedModules)
return await this.#renderPage(request, router, nestedModules)
})
return data
}
Expand Down Expand Up @@ -600,7 +600,7 @@ export class Aleph implements IAleph {
}

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

const [html] = await this.#renderer.cache(routePath, path, async () => {
return await this.#renderPage(router, nestedModules)
return await this.#renderPage(request, router, nestedModules)
})
return [200, html]
}

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

// render route pages
const req = new Request('http://localhost/')
await Promise.all(Array.from(paths).map(loc => ([loc, ...locales.map(locale => ({ ...loc, pathname: '/' + locale + loc.pathname }))])).flat().map(async ({ pathname, search }) => {
if (this.#isSSRable(pathname)) {
const [router, nestedModules] = this.#pageRouting.createRouter({ pathname, search })
if (router.routePath !== '') {
const href = router.toString()
const [html, data] = await this.#renderPage(router, nestedModules)
const [html, data] = await this.#renderPage(req, router, nestedModules)
await ensureTextFile(join(outputDir, pathname, 'index.html' + (search || '')), html)
if (data) {
const dataFile = join(
Expand All @@ -1559,7 +1560,7 @@ export class Aleph implements IAleph {
if (nestedModules.length > 0) {
await this.compile(nestedModules[0])
}
const [html] = await this.#renderPage(router, nestedModules.slice(0, 1))
const [html] = await this.#renderPage(req, router, nestedModules.slice(0, 1))
await ensureTextFile(join(outputDir, '404.html'), html)
}
}
Expand Down
Loading