-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: extract StartClient, StartServer and Meta from @tanstack/start (#…
…64) * feat: refactor-tanstack-start * chore: changelog * refactor: code style * docs: add credits --------- Co-authored-by: 高艳兵 <[email protected]> Co-authored-by: sorrycc <[email protected]>
- Loading branch information
1 parent
3aac455
commit 9812357
Showing
16 changed files
with
884 additions
and
2,978 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@umijs/tnf': patch | ||
--- | ||
|
||
refactor(ssr): extract StartClient, StartServer and Meta from @tanstack/start |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import React from 'react'; | ||
import type { RouterManagedTag } from '@tanstack/react-router'; | ||
|
||
export function Asset({ tag, attrs, children }: RouterManagedTag) { | ||
switch (tag) { | ||
case 'title': | ||
return ( | ||
<title {...attrs} suppressHydrationWarning> | ||
{children} | ||
</title> | ||
); | ||
case 'meta': | ||
return <meta {...attrs} suppressHydrationWarning />; | ||
case 'link': | ||
return <link {...attrs} suppressHydrationWarning />; | ||
case 'style': | ||
return ( | ||
<style | ||
{...attrs} | ||
dangerouslySetInnerHTML={{ __html: children || '' }} | ||
/> | ||
); | ||
case 'script': | ||
if (attrs && attrs.src) { | ||
return <script {...attrs} suppressHydrationWarning />; | ||
} | ||
if (typeof children === 'string') | ||
return ( | ||
<script | ||
{...attrs} | ||
dangerouslySetInnerHTML={{ | ||
__html: children, | ||
}} | ||
suppressHydrationWarning | ||
/> | ||
); | ||
return null; | ||
default: | ||
return null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import React from 'react'; | ||
import { RouterProvider } from '@tanstack/react-router'; | ||
import type { AnyRouter } from '@tanstack/react-router'; | ||
import { afterHydrate } from './utils/serialization'; | ||
|
||
let cleaned = false; | ||
|
||
export function Client(props: { router: AnyRouter }) { | ||
if (!props.router.state.matches.length) { | ||
props.router.hydrate(); | ||
afterHydrate({ router: props.router }); | ||
} | ||
|
||
if (!cleaned) { | ||
cleaned = true; | ||
window.__TSR__?.cleanScripts(); | ||
} | ||
|
||
return <RouterProvider router={props.router} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { createContext } from 'react'; | ||
|
||
class Context { | ||
cache = new Map(); | ||
|
||
private static instance: Context | undefined; | ||
|
||
public static create(): Context { | ||
if (!Context.instance) { | ||
Context.instance = new Context(); | ||
} | ||
|
||
return Context.instance; | ||
} | ||
|
||
private createContext<T>(key: string, initialValue: T) { | ||
const context = createContext(initialValue); | ||
|
||
this.cache.set(key, context); | ||
|
||
return context; | ||
} | ||
|
||
get<T>(key: string, initialValue?: T) { | ||
return this.cache.get(key) || this.createContext(key, initialValue); | ||
} | ||
} | ||
|
||
const context = Context.create(); | ||
|
||
export default context; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import * as React from 'react'; | ||
import { ScriptOnce, useRouter, useRouterState } from '@tanstack/react-router'; | ||
import type { | ||
MakeRouteMatch, | ||
RouterManagedTag, | ||
RouterState, | ||
} from '@tanstack/react-router'; | ||
import jsesc from 'jsesc'; | ||
import { Asset } from './Asset'; | ||
import Context from './Context'; | ||
|
||
export const useMeta = (): RouterManagedTag[] => { | ||
const router = useRouter(); | ||
|
||
const routeMeta = useRouterState({ | ||
select: (state) => { | ||
return state.matches.map((match) => match.meta!).filter(Boolean); | ||
}, | ||
}); | ||
|
||
const meta: RouterManagedTag[] = React.useMemo(() => { | ||
const resultMeta: RouterManagedTag[] = []; | ||
const metaByName: Record<string, boolean> = {}; | ||
let title: RouterManagedTag | undefined; | ||
[...routeMeta].reverse().forEach((metas) => { | ||
[...metas].reverse().forEach((m) => { | ||
if (!m) return; | ||
|
||
if (m.title) { | ||
if (!title) { | ||
title = { | ||
tag: 'title', | ||
children: m.title, | ||
}; | ||
} | ||
} else { | ||
if (m.name) { | ||
if (metaByName[m.name]) { | ||
return; | ||
} else { | ||
metaByName[m.name] = true; | ||
} | ||
} | ||
|
||
resultMeta.push({ | ||
tag: 'meta', | ||
attrs: { | ||
...m, | ||
}, | ||
}); | ||
} | ||
}); | ||
}); | ||
|
||
if (title) { | ||
resultMeta.push(title); | ||
} | ||
|
||
resultMeta.reverse(); | ||
|
||
return resultMeta; | ||
}, [routeMeta]); | ||
|
||
const links = useRouterState({ | ||
select: (state: RouterState<any, MakeRouteMatch<any>>) => | ||
state.matches | ||
.map((match) => match.links!) | ||
.filter(Boolean) | ||
.flat(1) | ||
.map((link) => ({ | ||
tag: 'link', | ||
attrs: { | ||
...link, | ||
}, | ||
})) as RouterManagedTag[], | ||
}); | ||
|
||
const preloadMeta = useRouterState({ | ||
select: (state: RouterState<any, MakeRouteMatch<any>>) => { | ||
const preloadMeta: RouterManagedTag[] = []; | ||
|
||
state.matches | ||
.map((match) => router.looseRoutesById[match.routeId]!) | ||
.forEach((route) => | ||
router.manifest?.routes[route.id]?.preloads | ||
?.filter(Boolean) | ||
.forEach((preload) => { | ||
preloadMeta.push({ | ||
tag: 'link', | ||
attrs: { | ||
rel: 'modulepreload', | ||
href: preload, | ||
}, | ||
}); | ||
}), | ||
); | ||
|
||
return preloadMeta; | ||
}, | ||
}); | ||
|
||
return uniqBy([...meta, ...preloadMeta, ...links], (d) => { | ||
return JSON.stringify(d); | ||
}); | ||
}; | ||
|
||
function uniqBy<T>(arr: T[], fn: (item: T) => string): T[] { | ||
const seen = new Set<string>(); | ||
return arr.filter((item) => { | ||
const key = fn(item); | ||
if (seen.has(key)) { | ||
return false; | ||
} | ||
seen.add(key); | ||
return true; | ||
}); | ||
} | ||
|
||
export const useMetaElements = (): JSX.Element => { | ||
const router = useRouter(); | ||
const meta = useMeta(); | ||
|
||
const dehydratedCtx = React.useContext( | ||
Context.get('TanStackRouterHydrationContext', {}), | ||
); | ||
|
||
return ( | ||
<> | ||
{meta.map((asset, i) => ( | ||
<Asset {...asset} key={`tsr-meta-${JSON.stringify(asset)}`} /> | ||
))} | ||
<> | ||
<ScriptOnce | ||
log={false} | ||
children={`__TSR__={matches:[],streamedValues:{},queue:[],runQueue:()=>{let e=!1;__TSR__.queue=__TSR__.queue.filter((_=>!_()||(e=!0,!1))),e&&__TSR__.runQueue()},initMatch:e=>{__TSR__.queue.push((()=>(__TSR__.matches[e.index]||(__TSR__.matches[e.index]=e,Object.entries(e.extracted).forEach((([e,_])=>{if("stream"===_.type){let e;_.value=new ReadableStream({start(_){e=_}}),_.value.controller=e}else if("promise"===_.type){let e,t;_.value=new Promise(((_,u)=>{e=_,t=u})),_.resolve=e,_.reject=t}}))),!0))),__TSR__.runQueue()},resolvePromise:e=>{__TSR__.queue.push((()=>{const _=__TSR__.matches[e.matchIndex];if(_){const t=_.extracted[e.id];if(t)return t.resolve(e.value.data),!0}return!1})),__TSR__.runQueue()},cleanScripts:()=>{document.querySelectorAll(".tsr-once").forEach((e=>{e.remove()}))}};`} | ||
/> | ||
<ScriptOnce | ||
children={`__TSR__.dehydrated = ${jsesc( | ||
router.options.transformer.stringify(dehydratedCtx), | ||
{ | ||
isScriptContext: true, | ||
wrap: true, | ||
json: true, | ||
}, | ||
)}`} | ||
/> | ||
</> | ||
</> | ||
); | ||
}; | ||
|
||
/** | ||
* @description The `Meta` component is used to render meta tags and links for the current route. | ||
* It should be rendered in the `<head>` of your document. | ||
*/ | ||
export const Meta = (): JSX.Element => { | ||
return <>{useMetaElements()}</>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import * as React from 'react'; | ||
import { RouterProvider } from '@tanstack/react-router'; | ||
import type { AnyRouter } from '@tanstack/react-router'; | ||
import jsesc from 'jsesc'; | ||
import Context from './Context'; | ||
import { AfterEachMatch } from './utils/serialization'; | ||
|
||
export function Server<TRouter extends AnyRouter>(props: { router: TRouter }) { | ||
props.router.AfterEachMatch = AfterEachMatch; | ||
props.router.serializer = (value) => | ||
jsesc(value, { | ||
isScriptContext: true, | ||
wrap: true, | ||
json: true, | ||
}); | ||
|
||
const hydrationContext = Context.get('TanStackRouterHydrationContext', {}); | ||
|
||
const hydrationCtxValue = React.useMemo( | ||
() => ({ | ||
router: props.router.dehydrate(), | ||
payload: props.router.options.dehydrate?.(), | ||
}), | ||
[props.router], | ||
); | ||
|
||
return ( | ||
<hydrationContext.Provider value={hydrationCtxValue}> | ||
<RouterProvider router={props.router} /> | ||
</hydrationContext.Provider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import type { ReactNode } from 'react'; | ||
import type { AnyRouter } from '@tanstack/react-router'; | ||
|
||
export interface StartClientProps { | ||
router: AnyRouter; | ||
} | ||
|
||
export interface AssetProps { | ||
tag: 'title' | 'meta' | 'link' | 'style' | 'script'; | ||
attrs?: Record<string, any>; | ||
children?: ReactNode; | ||
} | ||
|
||
export function StartClient(props: StartClientProps): JSX.Element; | ||
export function StartServer<TRouter extends AnyRouter>(props: { | ||
router: TRouter; | ||
}): JSX.Element; | ||
export function Meta(): JSX.Element; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export { Client } from './Client'; | ||
export { Server } from './Server'; | ||
export { Meta } from './Meta'; | ||
|
Oops, something went wrong.