Skip to content

Commit

Permalink
feat: code split & dynamic imports (#188)
Browse files Browse the repository at this point in the history
* fix: add chunk hashes to all assets except sw

* test: update verify-dist tests

* chore: remove unused component

* tmp

* feat: basic router implemented

* fix: redirect page loads on subdomain

* chore: cleaned up code

* feat: minimal assets loaded on subdomain first-hit

* chore: remove runtimeChunk from webpack

* chore: remove unused deps

* test: remove check css content thats in another chunk

* chore: fix tests after merging code from #182

* chore: apply suggestions from code review
  • Loading branch information
SgtPooki authored Apr 12, 2024
1 parent 24192ab commit bdc2979
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 141 deletions.
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>
)
}

Expand Down
9 changes: 4 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,7 @@ 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)
gotoPage('/ipfs-sw-config')
}}
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,17 @@ 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}]`
* TODO: https://github.com/ipfs-shipyard/service-worker-gateway/issues/66
*/
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))
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 => {
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'
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

0 comments on commit bdc2979

Please sign in to comment.