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

feat: code split & dynamic imports #188

Merged
merged 15 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from 13 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
3 changes: 2 additions & 1 deletion .aegir.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export default {
'src/**/*.[jt]sx',
'test/**/*.[jt]s',
'test/**/*.[jt]sx',
'test-e2e/**/*.[jt]s'
'test-e2e/**/*.[jt]s',
'./*.[jt]s'
]
},
dependencyCheck: {
Expand Down
45 changes: 7 additions & 38 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,13 @@
import React, { useContext } from 'react'
import Config from './components/config.jsx'
import { ConfigContext } from './context/config-context.jsx'
import HelperUi from './helper-ui.jsx'
import { isConfigPage } from './lib/is-config-page.js'
import { isPathOrSubdomainRequest, isSubdomainGatewayRequest } from './lib/path-or-subdomain.js'
import RedirectPage from './redirectPage.jsx'
import React, { Suspense } from 'react'
import { RouteContext } from './context/router-context.jsx'
import './app.css'

function App (): JSX.Element {
const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext)
const isRequestToViewConfigPage = isConfigPage(window.location.hash)
const isSubdomainRender = isSubdomainGatewayRequest(window.location)
const shouldRequestBeHandledByServiceWorker = isPathOrSubdomainRequest(window.location) && !isRequestToViewConfigPage

if (shouldRequestBeHandledByServiceWorker) {
if (window.self === window.top && isSubdomainRender) {
return (<RedirectPage />)
} else {
// rendering redirect page without iframe because this is a top level window and subdomain request.
return (<RedirectPage showConfigIframe={false} />)
}
}

if (isRequestToViewConfigPage) {
if (isSubdomainRender && window.self === window.top) {
return <RedirectPage />
}

setConfigExpanded(true)
return <Config />
}

if (isSubdomainRender) {
return (<RedirectPage />)
}

if (isConfigExpanded) {
return (<Config />)
}
const { currentRoute } = React.useContext(RouteContext)
return (
<HelperUi />
<Suspense fallback={<div>Loading...</div>}>
{currentRoute?.component != null && <currentRoute.component />}
</Suspense>
)
Comment on lines +8 to +10
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could avoid any potential "not prod ready" issues with preact's Suspense by doing a simple useEffect wrapper of the async operation

}

Expand Down
12 changes: 7 additions & 5 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React, { useContext } from 'react'
import { ConfigContext } from '../context/config-context.jsx'
import React from 'react'
import { RouteContext } from '../context/router-context.jsx'
import gearIcon from '../gear-icon.svg'
import ipfsLogo from '../ipfs-logo.svg'

export default function Header (): JSX.Element {
const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext)

const { gotoPage } = React.useContext(RouteContext)
return (
<header className='e2e-header flex items-center pa3 bg-navy bb bw3 b--aqua justify-between'>
<a href='https://ipfs.io' title='home'>
Expand All @@ -14,7 +13,10 @@ export default function Header (): JSX.Element {
<span className='e2e-header-title white f3'>IPFS Service Worker Gateway</span>
<button className='e2e-header-config-button'
onClick={() => {
setConfigExpanded(!isConfigExpanded)
// window.history.pushState(null, '', '/#/ipfs-sw-config')
// window.location.hash = '#/ipfs-sw-config'
// window.location.reload()
gotoPage('/ipfs-sw-config')
SgtPooki marked this conversation as resolved.
Show resolved Hide resolved
}}
style={{ border: 'none', background: 'none', cursor: 'pointer' }}
>
Expand Down
34 changes: 0 additions & 34 deletions src/components/TerminalOutput.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,16 @@ function ValidationMessage ({ cid, requestPath, pathNamespacePrefix, children })
</>
}

export default function CidRenderer ({ requestPath }: { requestPath: string }): JSX.Element {
export default function InputValidator ({ requestPath }: { requestPath: string }): JSX.Element {
/**
* requestPath may be any of the following formats:
*
* * `/ipfs/${cid}/${path}`
* * `/ipfs/${cid}`
* * `/ipfs/${cid}[/${path}]`
* * `/ipns/${dnsLinkDomain}[/${path}]`
* * `/ipns/${peerId}[/${path}]`
* * `http[s]://${cid}.ipfs.example.com[/${path}]`
* * `http[s]://${dnsLinkDomain}.ipns.example.com[/${path}]`
* * `http[s]://${peerId}.ipns.example.com[/${path}]`
*/
SgtPooki marked this conversation as resolved.
Show resolved Hide resolved
const requestPathParts = requestPath.split('/')
const pathNamespacePrefix = requestPathParts[1]
Expand Down
4 changes: 2 additions & 2 deletions src/context/config-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export const ConfigContext = createContext({
setConfigExpanded: (value: boolean) => {}
})

export const ConfigProvider = ({ children, expanded = isLoadedInIframe }: { children: JSX.Element[] | JSX.Element, expanded?: boolean }): JSX.Element => {
const [isConfigExpanded, setConfigExpanded] = useState(expanded)
export const ConfigProvider = ({ children }: { children: JSX.Element[] | JSX.Element, expanded?: boolean }): JSX.Element => {
const [isConfigExpanded, setConfigExpanded] = useState(isConfigPage(window.location.hash))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be updated to loaded dynamically but I don't think it would give us much for the effort

const isExplicitlyLoadedConfigPage = isConfigPage(window.location.hash)

const setConfigExpandedWrapped = (value: boolean): void => {
Expand Down
89 changes: 89 additions & 0 deletions src/context/router-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { useCallback, useEffect } from 'react'

export interface Route {
default?: boolean
path?: string
shouldRender?(): Promise<boolean>
component: React.LazyExoticComponent<(...args: any[]) => React.JSX.Element | null>
}

export const RouteContext = React.createContext<{
// routes: Route[]
currentRoute: Route | undefined
gotoPage(route?: string): void
}>({ currentRoute: undefined, gotoPage: () => {} })

export const RouterProvider = ({ children, routes }: { children: React.ReactNode, routes: Route[] }): JSX.Element => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very basic hash-router implementation. I looked around for some and most were 200kb and unmaintained, or very large. All required some intimate knowledge or specific finagling for things we needed.

lmk if there's something I missed.

const [currentRoute, setCurrentRoute] = React.useState<Route | undefined>(undefined)
/**
* The default route is the first route in the list of routes,
* or the first one with the `default` property set to `true`
*/
const defaultRoute = routes.find(route => route.default) ?? routes[0]

const findRouteByPath = useCallback((path: string): Route | undefined => {
const result = routes.find(route => route.path === path)
return result
}, [routes])

const findRouteByRenderFn = useCallback(async (): Promise<Route | undefined> => {
const validRoutes: Route[] = []
for (const route of routes) {
if (route.shouldRender == null) {
continue
}
const renderFuncResult = await route.shouldRender()

if (renderFuncResult) {
validRoutes.push(route)
}
}
return validRoutes[0] ?? undefined
}, [routes])

const setDerivedRoute = useCallback(async (hash: string): Promise<void> => {
setCurrentRoute(findRouteByPath(hash) ?? await findRouteByRenderFn() ?? defaultRoute)
}, [findRouteByPath, findRouteByRenderFn, defaultRoute])

const onHashChange = useCallback((event: HashChangeEvent) => {
const newUrl = new URL(event.newURL)
void setDerivedRoute(newUrl.hash)
}, [setDerivedRoute])

const onPopState = useCallback((event: PopStateEvent) => {
void setDerivedRoute(window.location.hash)
}, [setDerivedRoute])

useEffect(() => {
void setDerivedRoute(window.location.hash)
}, [setDerivedRoute])

useEffect(() => {
window.addEventListener('popstate', onPopState, false)
window.addEventListener('hashchange', onHashChange, false)

return () => {
window.removeEventListener('popstate', onPopState, false)
window.removeEventListener('hashchange', onHashChange, false)
}
}, [onPopState, onHashChange])

return (
<RouteContext.Provider
value={{
currentRoute,
gotoPage: (page?: string) => {
if (page == null) {
// clear out the hash
window.history.pushState('', document.title, `${window.location.pathname}${window.location.search}`)
void setDerivedRoute('')
} else {
window.location.hash = `#${page}`
}
}
}}
>
{children}
</RouteContext.Provider>
)
}
20 changes: 14 additions & 6 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import React from 'react'
import ReactDOMClient from 'react-dom/client'
import './app.css'
import App from './app.jsx'
import { ConfigProvider } from './context/config-context.jsx'
import { RouterProvider, type Route } from './context/router-context.jsx'
import { ServiceWorkerProvider } from './context/service-worker-context.jsx'
import { loadConfigFromLocalStorage } from './lib/config-db.js'
import { isConfigPage } from './lib/is-config-page.js'

await loadConfigFromLocalStorage()

// SW did not trigger for this request
const container = document.getElementById('root')

const root = ReactDOMClient.createRoot(container)

const LazyConfig = React.lazy(async () => import('./pages/config.jsx'))
const LazyHelperUi = React.lazy(async () => import('./pages/helper-ui.jsx'))
const LazyRedirectPage = React.lazy(async () => import('./pages/redirect-page.jsx'))

const routes: Route[] = [
{ default: true, component: LazyHelperUi },
{ path: '#/ipfs-sw-config', shouldRender: async () => (await import('./lib/routing-render-checks')).shouldRenderConfigPage(), component: LazyConfig },
{ shouldRender: async () => (await import('./lib/routing-render-checks')).shouldRenderRedirectPage(), component: LazyRedirectPage }
]

root.render(
<React.StrictMode>
<ServiceWorkerProvider>
<ConfigProvider expanded={isConfigPage(window.location.hash)}>
<ConfigProvider>
<RouterProvider routes={routes}>
<App />
</RouterProvider>
</ConfigProvider>
</ServiceWorkerProvider>
</React.StrictMode>
Expand Down
17 changes: 17 additions & 0 deletions src/lib/routing-render-checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export async function shouldRenderRedirectPage (): Promise<boolean> {
const { isConfigPage } = await import('../lib/is-config-page.js')
const { isPathOrSubdomainRequest } = await import('./path-or-subdomain.js')
const isRequestToViewConfigPage = isConfigPage(window.location.hash)
const shouldRequestBeHandledByServiceWorker = isPathOrSubdomainRequest(window.location) && !isRequestToViewConfigPage
const isTopLevelWindow = window.self === window.top
const isRequestToViewConfigPageAndTopLevelWindow = isRequestToViewConfigPage && isTopLevelWindow
const result = shouldRequestBeHandledByServiceWorker && !isRequestToViewConfigPageAndTopLevelWindow
return result
}

export async function shouldRenderConfigPage (): Promise<boolean> {
const { isConfigPage } = await import('../lib/is-config-page.js')

const isRequestToViewConfigPage = isConfigPage(window.location.hash)
return isRequestToViewConfigPage
}
26 changes: 11 additions & 15 deletions src/components/config.tsx → src/pages/config.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { ConfigContext } from '../context/config-context.jsx'
import React, { useCallback, useEffect, useState } from 'react'
import { Collapsible } from '../components/collapsible.jsx'
import LocalStorageInput from '../components/local-storage-input.jsx'
import { LocalStorageToggle } from '../components/local-storage-toggle.jsx'
import { ServiceWorkerReadyButton } from '../components/sw-ready-button.jsx'
import { RouteContext } from '../context/router-context.jsx'
import { HeliaServiceWorkerCommsChannel } from '../lib/channel.js'
import { getConfig, loadConfigFromLocalStorage } from '../lib/config-db.js'
import { LOCAL_STORAGE_KEYS } from '../lib/local-storage.js'
import { trace } from '../lib/logger.js'
import { Collapsible } from './collapsible.jsx'
import LocalStorageInput from './local-storage-input.jsx'
import { LocalStorageToggle } from './local-storage-toggle.jsx'
import { ServiceWorkerReadyButton } from './sw-ready-button.jsx'

const channel = new HeliaServiceWorkerCommsChannel('WINDOW')

Expand Down Expand Up @@ -35,7 +35,7 @@ const stringValidationFn = (value: string): Error | null => {
}

export default (): JSX.Element | null => {
const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext)
const { gotoPage } = React.useContext(RouteContext)
const [error, setError] = useState<Error | null>(null)

const isLoadedInIframe = window.self !== window.top
Expand Down Expand Up @@ -74,21 +74,17 @@ export default (): JSX.Element | null => {
trace('config-page: RELOAD_CONFIG_SUCCESS for %s', window.location.origin)
// update the <subdomain>.<namespace>.BASE_URL service worker
await postFromIframeToParentSw()
setConfigExpanded(false)
if (!isLoadedInIframe) {
gotoPage()
}
} catch (err) {
setError(err as Error)
}
}, [])

if (!isConfigExpanded) {
return null
}

const isInIframe = window.self !== window.top

return (
<main className='e2e-config-page pa4-l bg-snow mw7 center pa4'>
<Collapsible collapsedLabel="View config" expandedLabel='Hide config' collapsed={isInIframe}>
<Collapsible collapsedLabel="View config" expandedLabel='Hide config' collapsed={isLoadedInIframe}>
<LocalStorageInput className="e2e-config-page-input e2e-config-page-input-gateways" localStorageKey={LOCAL_STORAGE_KEYS.config.gateways} label='Gateways' validationFn={urlValidationFn} defaultValue='[]' />
<LocalStorageInput className="e2e-config-page-input e2e-config-page-input-routers" localStorageKey={LOCAL_STORAGE_KEYS.config.routers} label='Routers' validationFn={urlValidationFn} defaultValue='[]'/>
<LocalStorageToggle className="e2e-config-page-input e2e-config-page-input-autoreload" localStorageKey={LOCAL_STORAGE_KEYS.config.autoReload} onLabel='Auto Reload' offLabel='Show Config' />
Expand Down
8 changes: 4 additions & 4 deletions src/helper-ui.tsx → src/pages/helper-ui.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react'
import CidRenderer from './components/CidRenderer.jsx'
import Form from './components/Form.jsx'
import Header from './components/Header.jsx'
import { LOCAL_STORAGE_KEYS } from './lib/local-storage.js'
import Form from '../components/Form.jsx'
import Header from '../components/Header.jsx'
Comment on lines +2 to +3
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: rename these to snake-case, form.jsx and header.jsx

import CidRenderer from '../components/input-validator.jsx'
import { LOCAL_STORAGE_KEYS } from '../lib/local-storage.js'

export default function (): JSX.Element {
const [requestPath, setRequestPath] = useState(localStorage.getItem(LOCAL_STORAGE_KEYS.forms.requestPath) ?? '')
Expand Down
16 changes: 8 additions & 8 deletions src/redirectPage.tsx → src/pages/redirect-page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import React, { useContext, useEffect, useMemo, useState } from 'react'
import { ServiceWorkerReadyButton } from './components/sw-ready-button.jsx'
import { ServiceWorkerContext } from './context/service-worker-context.jsx'
import { HeliaServiceWorkerCommsChannel } from './lib/channel.js'
import { setConfig, type ConfigDb } from './lib/config-db.js'
import { getSubdomainParts } from './lib/get-subdomain-parts.js'
import { isConfigPage } from './lib/is-config-page.js'
import { error, trace } from './lib/logger.js'
import { ServiceWorkerReadyButton } from '../components/sw-ready-button.jsx'
import { ServiceWorkerContext } from '../context/service-worker-context.jsx'
import { HeliaServiceWorkerCommsChannel } from '../lib/channel.js'
import { setConfig, type ConfigDb } from '../lib/config-db.js'
import { getSubdomainParts } from '../lib/get-subdomain-parts.js'
import { isConfigPage } from '../lib/is-config-page.js'
import { error, trace } from '../lib/logger.js'

const ConfigIframe = (): JSX.Element => {
const { parentDomain } = getSubdomainParts(window.location.href)

const portString = window.location.port === '' ? '' : `:${window.location.port}`
const iframeSrc = `${window.location.protocol}//${parentDomain}${portString}#/ipfs-sw-config@origin=${encodeURIComponent(window.location.origin)}`
const iframeSrc = `${window.location.protocol}//${parentDomain}${portString}/#/ipfs-sw-config@origin=${encodeURIComponent(window.location.origin)}`

return (
<iframe id="redirect-config-iframe" src={iframeSrc} style={{ width: '100vw', height: '100vh', border: 'none' }} />
Expand Down
Loading
Loading