Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit 4f329cb

Browse files
author
Je
committed
feat: add component async import
1 parent 8b81d29 commit 4f329cb

File tree

11 files changed

+246
-178
lines changed

11 files changed

+246
-178
lines changed

aleph.ts

Lines changed: 29 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React, { ComponentType, useCallback, useEffect, useRef, useState } from 'https://esm.sh/react'
22
import { DataContext, RouterContext } from './context.ts'
3-
import { E404Page, E501App, E501Page, ErrorBoundary } from './error.ts'
3+
import { E400MissingDefaultExportAsComponent, E404Page, ErrorBoundary } from './error.ts'
44
import events from './events.ts'
5-
import type { Routing } from './router.ts'
6-
import type { Module, PageProps, RouterURL } from './types.ts'
5+
import { createPageProps, Routing } from './router.ts'
6+
import type { Module, RouterURL } from './types.ts'
77
import util, { hashShort, reModuleExt } from './util.ts'
88

99
export function ALEPH({ initial }: {
@@ -12,26 +12,34 @@ export function ALEPH({ initial }: {
1212
url: RouterURL
1313
staticData: Record<string, any>
1414
components: Record<string, ComponentType<any>>
15-
pageProps: PageProps | null
15+
pageComponentTree: { id: string, Component?: any }[]
1616
}
1717
}) {
1818
const ref = useRef({ routing: initial.routing })
1919
const [staticData, setStaticData] = useState(() => initial.staticData)
20-
const [e404, setE404] = useState(() => {
20+
const [e404, setE404] = useState<{ Component: ComponentType<any>, props?: Record<string, any> }>(() => {
2121
const { E404 } = initial.components
22-
return {
23-
Component: E404 && util.isLikelyReactComponent(E404) ? E404 : E404Page
22+
if (E404) {
23+
if (util.isLikelyReactComponent(E404)) {
24+
return { Component: E404 }
25+
}
26+
return { Component: E400MissingDefaultExportAsComponent, props: { name: 'Custom 404 Page' } }
2427
}
28+
return { Component: E404Page }
2529
})
26-
const [app, setApp] = useState(() => {
30+
const [app, setApp] = useState<{ Component: ComponentType<any> | null, props?: Record<string, any> }>(() => {
2731
const { App } = initial.components
28-
return {
29-
Component: App ? (util.isLikelyReactComponent(App) ? App : E501App) : null
32+
if (App) {
33+
if (util.isLikelyReactComponent(App)) {
34+
return { Component: App }
35+
}
36+
return { Component: E400MissingDefaultExportAsComponent, props: { name: 'Custom App' } }
3037
}
38+
return { Component: null }
3139
})
32-
const [page, setPage] = useState(() => {
33-
const { url, pageProps } = initial
34-
return { pageProps, url }
40+
const [route, setRoute] = useState(() => {
41+
const { url, pageComponentTree } = initial
42+
return { ...createPageProps(pageComponentTree), url }
3543
})
3644
const onpopstate = useCallback(async (e: any) => {
3745
const { routing } = ref.current
@@ -49,34 +57,16 @@ export function ALEPH({ initial }: {
4957
}
5058
const pc = ctree.find(pc => pc.id === mod.id)
5159
if (pc) {
52-
if (util.isLikelyReactComponent(C)) {
53-
pc.Component = C
54-
} else {
55-
pc.Component = E501Page
56-
}
60+
pc.Component = C
5761
}
5862
})
5963
await Promise.all(imports)
60-
const pageProps: PageProps = {
61-
Page: ctree[0].Component || (() => null),
62-
pageProps: {}
63-
}
64-
if (ctree.length > 1) {
65-
ctree.slice(1).reduce((p, m) => {
66-
const c = {
67-
Page: m.Component || (() => null),
68-
pageProps: {}
69-
}
70-
p.pageProps = c
71-
return c
72-
}, pageProps)
73-
}
74-
setPage({ url, pageProps })
64+
setRoute({ ...createPageProps(ctree), url })
7565
if (util.isInt(e.scrollTo)) {
7666
(window as any).scrollTo(e.scrollTo, 0)
7767
}
7868
} else {
79-
setPage({ url, pageProps: null })
69+
setRoute({ Page: null, pageProps: {}, url })
8070
}
8171
}, [ref])
8272

@@ -113,7 +103,7 @@ export function ALEPH({ initial }: {
113103
if (util.isLikelyReactComponent(Component)) {
114104
setApp({ Component })
115105
} else {
116-
setApp({ Component: E501App })
106+
setApp({ Component: E400MissingDefaultExportAsComponent, props: { name: 'Custom App' } })
117107
}
118108
break
119109
}
@@ -192,11 +182,11 @@ export function ALEPH({ initial }: {
192182
{ value: staticData },
193183
React.createElement(
194184
RouterContext.Provider,
195-
{ value: page.url },
185+
{ value: route.url },
196186
...[
197-
(page.pageProps && app.Component) && React.createElement(app.Component, page.pageProps),
198-
(page.pageProps && !app.Component) && React.createElement(page.pageProps.Page, page.pageProps.pageProps),
199-
!page.pageProps && React.createElement(e404.Component)
187+
(route.Page && app.Component) && React.createElement(app.Component, Object.assign({}, app.props, { Page: route.Page, pageProps: route.pageProps })),
188+
(route.Page && !app.Component) && React.createElement(route.Page, route.pageProps),
189+
!route.Page && React.createElement(e404.Component, e404.props)
200190
].filter(Boolean),
201191
)
202192
)

bootstrap.ts

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import React, { ComponentType } from 'https://esm.sh/react'
22
import { hydrate, render } from 'https://esm.sh/react-dom'
33
import { ALEPH, getModuleImportUrl } from './aleph.ts'
4-
import { E501Page } from './error.ts'
54
import { Routing } from './router.ts'
6-
import type { Module, PageProps, Route } from './types.ts'
7-
import util, { reModuleExt } from './util.ts'
5+
import type { Module, Route } from './types.ts'
6+
import { reModuleExt } from './util.ts'
87

98
export default async function bootstrap({
109
routes,
@@ -25,7 +24,7 @@ export default async function bootstrap({
2524
const components: Record<string, ComponentType> = {}
2625
const routing = new Routing(routes, baseUrl, defaultLocale, locales)
2726
const [url, pageModuleTree] = routing.createRouter()
28-
const ctree: { id: string, Component?: ComponentType }[] = pageModuleTree.map(({ id }) => ({ id }))
27+
const pageComponentTree: { id: string, Component?: ComponentType }[] = pageModuleTree.map(({ id }) => ({ id }))
2928
const imports = [...preloadModules, ...pageModuleTree].map(async mod => {
3029
const { default: C } = await import(getModuleImportUrl(baseUrl, mod))
3130
if (mod.asyncDeps) {
@@ -45,33 +44,15 @@ export default async function bootstrap({
4544
components['E404'] = C
4645
break
4746
default:
48-
const pc = ctree.find(pc => pc.id === mod.id)
47+
const pc = pageComponentTree.find(pc => pc.id === mod.id)
4948
if (pc) {
50-
if (util.isLikelyReactComponent(C)) {
51-
pc.Component = C
52-
} else {
53-
pc.Component = E501Page
54-
}
49+
pc.Component = C
5550
}
5651
break
5752
}
5853
})
5954
await Promise.all(imports)
6055

61-
const pageProps: PageProps | null = url.pagePath != '' ? {
62-
Page: ctree[0].Component || (() => null),
63-
pageProps: {}
64-
} : null
65-
if (pageProps && ctree.length > 1) {
66-
ctree.slice(1).reduce((p, m) => {
67-
const c = {
68-
Page: m.Component || (() => null),
69-
pageProps: {}
70-
}
71-
p.pageProps = c
72-
return c
73-
}, pageProps)
74-
}
7556
const el = React.createElement(
7657
ALEPH,
7758
{
@@ -80,7 +61,7 @@ export default async function bootstrap({
8061
url,
8162
staticData,
8263
components,
83-
pageProps
64+
pageComponentTree,
8465
}
8566
}
8667
)

error.ts

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,6 @@
11
import React from 'https://esm.sh/react'
22
import { Head } from './head.ts'
33

4-
const e501AppEl = React.createElement(
5-
ErrorPage,
6-
{
7-
status: 501,
8-
text: 'app module should export default as a react component',
9-
refreshButton: true
10-
}
11-
)
12-
const e501PageEl = React.createElement(
13-
ErrorPage,
14-
{
15-
status: 501,
16-
text: 'page module should export default as a react component',
17-
refreshButton: true
18-
}
19-
)
20-
const e404PageEl = React.createElement(ErrorPage, { status: 404 })
21-
22-
export const E501App = () => e501AppEl
23-
export const E501Page = () => e501PageEl
24-
export const E404Page = () => e404PageEl
25-
264
export class ErrorBoundary extends React.Component {
275
state: { stack: string | null }
286

@@ -55,7 +33,29 @@ export class ErrorBoundary extends React.Component {
5533
}
5634
}
5735

58-
export function ErrorPage({ status, text = getStatusText(status), refreshButton }: { status: number, text?: string, refreshButton?: boolean }) {
36+
37+
export function E404Page() {
38+
return React.createElement(
39+
Error,
40+
{
41+
status: 404,
42+
message: 'page not found'
43+
}
44+
)
45+
}
46+
47+
export function E400MissingDefaultExportAsComponent({ name }: { name: string }) {
48+
return React.createElement(
49+
Error,
50+
{
51+
status: 400,
52+
message: `"${name}" should export a React Component as default`,
53+
refreshButton: true
54+
}
55+
)
56+
}
57+
58+
export function Error({ status, message, refreshButton }: { status: number, message: string, refreshButton?: boolean }) {
5959
return (
6060
React.createElement(
6161
React.Fragment,
@@ -66,7 +66,7 @@ export function ErrorPage({ status, text = getStatusText(status), refreshButton
6666
React.createElement(
6767
'title',
6868
null,
69-
`${status} - ${text} | Aleph.js`
69+
`Error ${status} - ${message} | Aleph.js`
7070
),
7171
),
7272
React.createElement(
@@ -75,6 +75,7 @@ export function ErrorPage({ status, text = getStatusText(status), refreshButton
7575
React.createElement(
7676
'strong',
7777
null,
78+
'Error ',
7879
React.createElement(
7980
'code',
8081
null,
@@ -89,7 +90,7 @@ export function ErrorPage({ status, text = getStatusText(status), refreshButton
8990
React.createElement(
9091
'span',
9192
null,
92-
text
93+
message
9394
)
9495
),
9596
refreshButton && React.createElement(
@@ -109,14 +110,3 @@ export function ErrorPage({ status, text = getStatusText(status), refreshButton
109110
)
110111
)
111112
}
112-
113-
function getStatusText(status: number) {
114-
switch (status) {
115-
case 404:
116-
return 'page not found'
117-
case 500:
118-
return 'internal server error'
119-
default:
120-
return 'error'
121-
}
122-
}

head.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export async function renderHead(styles?: { url: string, hash: string, async?: b
3030
}
3131
})
3232
await Promise.all(styles?.filter(({ async }) => !!async).map(({ url, hash }) => {
33-
return import('file://' + util.cleanPath(`${__appRoot}/.aleph/build-${__buildID}/${url}.${hash.slice(0, hashShort)}.js`))
33+
return import('file://' + util.cleanPath(`${__appRoot}/.aleph/${__buildID}/${url}.${hash.slice(0, hashShort)}.js`))
3434
}) || [])
3535
styles?.forEach(({ url }) => {
3636
if (serverStyles.has(url)) {

link.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import React, { Children, cloneElement, CSSProperties, isValidElement, MouseEvent, PropsWithChildren, useCallback, useEffect, useMemo } from 'https://esm.sh/react'
1+
import React, { Children, cloneElement, ComponentType, CSSProperties, isValidElement, MouseEvent, PropsWithChildren, ReactElement, useCallback, useEffect, useMemo, useState } from 'https://esm.sh/react'
22
import { redirect } from './aleph.ts'
33
import events from './events.ts'
44
import { useRouter } from './hooks.ts'
5-
import util, { reModuleExt, reStyleModuleExt } from './util.ts'
5+
import util, { reModuleExt } from './util.ts'
66

77
interface LinkProps {
88
to: string
@@ -141,12 +141,49 @@ export function NavLink({
141141
return React.createElement(Link, { ...rest, to })
142142
}
143143

144-
export function Import(props: { from: string, props?: Record<string, any> }) {
145-
if (reStyleModuleExt.test(props.from)) {
146-
return null
144+
interface ImportProps {
145+
from: string
146+
props?: Record<string, any>
147+
placeholder?: ReactElement
148+
fallback?: ReactElement
149+
}
150+
151+
export function Import(props: ImportProps) {
152+
const { __importer, __sourceFile } = (props as any)
153+
const [error, setError] = useState<string | null>(null)
154+
const [mod, setMod] = useState<{ Component: ComponentType | null }>({ Component: null })
155+
156+
useEffect(() => {
157+
if (reModuleExt.test(__sourceFile)) {
158+
const p = util.splitPath(__importer)
159+
p.pop()
160+
import(util.cleanPath("/_aleph/" + p.join('/') + '/' + props.from))
161+
.then(({ default: Component }) => {
162+
setMod({ Component })
163+
})
164+
.catch((err: Error) => {
165+
setError(err.message)
166+
})
167+
}
168+
}, [__importer, __sourceFile])
169+
170+
if (error) {
171+
if (props.fallback) {
172+
return props.fallback
173+
}
174+
return React.createElement('div', { style: { color: 'red' } }, error)
175+
}
176+
177+
if (mod.Component) {
178+
return React.createElement(mod.Component, props.props)
147179
}
148-
if (reModuleExt.test(props.from)) {
149-
// todo: import component form props.__url
150-
return null
180+
181+
if (reModuleExt.test(__sourceFile)) {
182+
if (props.placeholder) {
183+
return props.placeholder
184+
}
185+
return React.createElement('div', { style: { color: 'gray' } }, 'Loading...')
151186
}
187+
188+
return null
152189
}

0 commit comments

Comments
 (0)