Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into add-cache
Browse files Browse the repository at this point in the history
* origin/main:
  feat: allow loading config page from subdomain after sw registration (#96)
  chore!: dist files optimizations (sw asset name changes) (#97)
  Update src/components/CidRenderer.tsx
  chore: remove in-page rendering
  • Loading branch information
2color committed Mar 12, 2024
2 parents 3cc5b98 + 1201b22 commit 1e9a30a
Show file tree
Hide file tree
Showing 15 changed files with 271 additions and 802 deletions.
769 changes: 136 additions & 633 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"keywords": [],
"license": "MIT",
"scripts": {
"analyze-bundle": "webpack --env analyze",
"clean": "aegir clean",
"dep-check": "aegir dep-check",
"lint": "aegir lint",
Expand Down Expand Up @@ -49,19 +50,20 @@
"@types/react": "^18.0.31",
"aegir": "^42.2.2",
"babel-loader": "^9.1.3",
"compression-webpack-plugin": "^10.0.0",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.10.0",
"eslint-config-standard-with-typescript": "^34.0.1",
"html-webpack-plugin": "^5.5.0",
"node-polyfill-webpack-plugin": "^2.0.1",
"npm-run-all": "^4.1.5",
"patch-package": "^6.5.1",
"rimraf": "^4.4.1",
"style-loader": "^3.3.4",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.77.0",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-merge": "^5.8.0"
}
},
"sideEffects": false
}
1 change: 0 additions & 1 deletion public/_redirects
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
/config /#/config 302
/* /?helia-sw=/:splat 302
10 changes: 8 additions & 2 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ import RedirectPage from './redirectPage.tsx'

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

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

if (isConfigPage()) {
setConfigExpanded(true)
return <Config />
}

if (isPathOrSubdomainRequest(window.location)) {
if (isSubdomainRender) {
return (<RedirectPage />)
}

Expand Down
79 changes: 4 additions & 75 deletions src/components/CidRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import { CID } from 'multiformats/cid'
import React, { useState } from 'react'
import React from 'react'

/**
* Test files:
Expand All @@ -22,38 +22,6 @@ import React, { useState } from 'react'
*
*/

export function ContentRender ({ blob, contentType, text, path, isLoading }): JSX.Element {
let content: JSX.Element | null = null
if (isLoading) {
content = <span>Loading...</span>
} else if (contentType?.startsWith('video/') && blob != null) {
content = (
<video controls autoPlay loop className="center" width="100%">
<source src={URL.createObjectURL(blob)} type={contentType} />
</video>
)
} else if (contentType?.startsWith('image/') && blob != null) {
content = <img src={URL.createObjectURL(blob)} />
} else if (text != null) {
if (!contentType?.startsWith('text/html')) {
// parsing failed
content = <pre id="text-content">{text}</pre>
} else {
const iframeSrc = path[0] === '/' ? `${path}` : `/${path}`
// parsing succeeded
content = <iframe src={iframeSrc} width="100%" height="100%"/>
}
} else {
content = <span>Not a supported content-type of <pre>{contentType}</pre></span>
}

return (
<div id="loaded-content" className="pt3 db" style={{ height: '50vh' }}>
{content}
</div>
)
}

function ValidationMessage ({ cid, requestPath, pathNamespacePrefix, children }): JSX.Element {
let errorElement: JSX.Element | null = null
if (requestPath == null || requestPath === '') {
Expand All @@ -75,19 +43,13 @@ function ValidationMessage ({ cid, requestPath, pathNamespacePrefix, children })
}

return <>
<span className="pb3 db">
<span className="pb3 pa3 db bg-light-yellow">
{ errorElement }
</span>
</>
}

export default function CidRenderer ({ requestPath }: { requestPath: string }): JSX.Element {
const [contentType, setContentType] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [abortController, setAbortController] = useState<AbortController | null>(null)
const [blob, setBlob] = useState<Blob | null>(null)
const [text, setText] = useState('')
const [lastFetchPath, setLastFetchPath] = useState<string | null>(null)
/**
* requestPath may be any of the following formats:
*
Expand All @@ -100,47 +62,14 @@ export default function CidRenderer ({ requestPath }: { requestPath: string }):
const cidPath = requestPathParts[3] ? `/${requestPathParts.slice(3).join('/')}` : ''
const swPath = `/${pathNamespacePrefix}/${cid ?? ''}${cidPath ?? ''}`

const makeRequest = async (): Promise<void> => {
abortController?.abort()
const newAbortController = new AbortController()
setAbortController(newAbortController)
setLastFetchPath(swPath)
setIsLoading(true)

const res = await fetch(swPath, {
signal: newAbortController.signal,
method: 'GET',
headers: {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8'
}
})
const contentType = res.headers.get('content-type')

setContentType(contentType)
setBlob(await res.clone().blob())
setText(await res.text())
setIsLoading(false)
}

let inPageContent: JSX.Element | null = null
if (lastFetchPath === swPath) {
if (isLoading) {
inPageContent = <span>Loading...</span>
} else {
inPageContent = ContentRender({ blob, contentType, text, path: `${pathNamespacePrefix}/${cid}${cidPath}`, isLoading })
}
}

return (
<div>
<ValidationMessage pathNamespacePrefix={pathNamespacePrefix} cid={cid} requestPath={requestPath}>
<button id="load-in-page" onClick={() => { void makeRequest() }} className='button-reset pv3 tc bn bg-animate bg-black-80 hover-bg-aqua white pointer w-100'>Load in-page</button>

<a className="pt3 db" href={swPath} target="_blank">
<button id="load-directly" className='button-reset pv3 tc bn bg-animate bg-black-80 hover-bg-aqua white pointer w-100'>Load directly / download</button>
<a className="db" href={swPath} target="_blank">
<button id="load-directly" className='button-reset pv3 tc bn bg-animate bg-black-80 hover-bg-aqua white pointer w-100'>Load content</button>
</a>

{inPageContent}
</ValidationMessage>
</div>
)
Expand Down
1 change: 0 additions & 1 deletion src/components/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export default (): JSX.Element | null => {
return
}
// we get the iframe origin from a query parameter called 'origin', if this is loaded in an iframe
// TODO: why we need this origin here? where is targetOrigin used?
const targetOrigin = decodeURIComponent(window.location.hash.split('@origin=')[1])
const config = await getConfig()
trace('config-page: postMessage config to origin ', config, origin)
Expand Down
2 changes: 1 addition & 1 deletion src/context/config-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const ConfigContext = createContext({

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

const setConfigExpandedWrapped = (value: boolean): void => {
if (isLoadedInIframe || isExplicitlyLoadedConfigPage) {
Expand Down
20 changes: 0 additions & 20 deletions src/get-helia.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/helper-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function (): JSX.Element {
<>
<Header />
<main className='pa4-l bg-snow mw7 mv5 center pa4'>
<h1 className='pa0 f2 ma0 mb4 aqua tc'>Fetch content from IPFS using Helia in a SW</h1>
<h1 className='pa0 f2 ma0 mb4 aqua tc'>Fetch & Verify IPFS content with a Service Worker</h1>
<Form
handleSubmit={handleSubmit}
requestPath={requestPath}
Expand Down
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const root = ReactDOMClient.createRoot(container)
root.render(
<React.StrictMode>
<ServiceWorkerProvider>
<ConfigProvider expanded={isConfigPage()}>
<ConfigProvider expanded={isConfigPage(window.location.hash)}>
<App />
</ConfigProvider>
</ServiceWorkerProvider>
Expand Down
7 changes: 3 additions & 4 deletions src/lib/is-config-page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export function isConfigPage (): boolean {
const isConfigPathname = window.location.pathname === '/config'
const isConfigHashPath = window.location.hash.startsWith('#/config') // needed for _redirects and IPFS hosted sw gateways
return isConfigPathname || isConfigHashPath
export function isConfigPage (hash: string): boolean {
const isConfigHashPath = hash.startsWith('#/ipfs-sw-config') // needed for _redirects and IPFS hosted sw gateways
return isConfigHashPath
}
14 changes: 10 additions & 4 deletions src/redirectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { ServiceWorkerContext } from './context/service-worker-context.tsx'
import { HeliaServiceWorkerCommsChannel } from './lib/channel.ts'
import { setConfig, type ConfigDb } from './lib/config-db.ts'
import { getSubdomainParts } from './lib/get-subdomain-parts'
import { isConfigPage } from './lib/is-config-page'
import { error, trace } from './lib/logger.ts'

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}#/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 Expand Up @@ -55,19 +56,24 @@ export default function RedirectPage (): JSX.Element {
}
}, [])

let reloadUrl = window.location.href
if (isConfigPage(window.location.hash)) {
reloadUrl = window.location.href.replace('#/ipfs-sw-config', '')
}

const displayString = useMemo(() => {
if (!isServiceWorkerRegistered) {
return 'Registering Helia service worker...'
}
if (isAutoReloadEnabled) {
if (isAutoReloadEnabled && !isConfigPage(window.location.hash)) {
return 'Redirecting you because Auto Reload is enabled.'
}

return 'Please save your changes to the config to apply them. You can then refresh the page to load your content.'
}, [isAutoReloadEnabled, isServiceWorkerRegistered])

useEffect(() => {
if (isAutoReloadEnabled && isServiceWorkerRegistered) {
if (isAutoReloadEnabled && isServiceWorkerRegistered && !isConfigPage(window.location.hash)) {
window.location.reload()
}
}, [isAutoReloadEnabled, isServiceWorkerRegistered])
Expand All @@ -76,7 +82,7 @@ export default function RedirectPage (): JSX.Element {
<div className="redirect-page">
<div className="pa4-l mw7 mv5 center pa4">
<h3 className="">{displayString}</h3>
<ServiceWorkerReadyButton id="load-content" label='Load content' waitingLabel='Waiting for service worker registration...' onClick={() => { window.location.reload() }} />
<ServiceWorkerReadyButton id="load-content" label='Load content' waitingLabel='Waiting for service worker registration...' onClick={() => { window.location.href = reloadUrl }} />
</div>
<ConfigIframe />
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/service-worker-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { log } from './lib/logger.ts'

export async function registerServiceWorker (): Promise<ServiceWorkerRegistration> {
const swRegistration = await navigator.serviceWorker.register(new URL('sw.ts', import.meta.url))
const swRegistration = await navigator.serviceWorker.register(new URL(/* webpackChunkName: "sw" */'sw.ts', import.meta.url))
return new Promise((resolve, reject) => {
swRegistration.addEventListener('updatefound', () => {
const newWorker = swRegistration.installing
Expand Down
41 changes: 36 additions & 5 deletions src/sw.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { getVerifiedFetch } from './get-helia.ts'
import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers'
import { createVerifiedFetch, type VerifiedFetch } from '@helia/verified-fetch'
import { HeliaServiceWorkerCommsChannel, type ChannelMessage } from './lib/channel.ts'
import { getConfig } from './lib/config-db.ts'
import { contentTypeParser } from './lib/content-type-parser.ts'
import { getSubdomainParts } from './lib/get-subdomain-parts.ts'
import { isConfigPage } from './lib/is-config-page.ts'
import { error, log, trace } from './lib/logger.ts'
import { findOriginIsolationRedirect } from './lib/path-or-subdomain.ts'
import type { VerifiedFetch } from '@helia/verified-fetch'

/**
******************************************************
Expand Down Expand Up @@ -51,11 +54,11 @@ const updateVerifiedFetch = async (): Promise<void> => {
self.addEventListener('install', (event) => {
// 👇 When a new version of the SW is installed, activate immediately
void self.skipWaiting()
// ensure verifiedFetch is ready for use
event.waitUntil(updateVerifiedFetch())
})

self.addEventListener('activate', (event) => {
// ensure verifiedFetch is ready for use
event.waitUntil(updateVerifiedFetch())
/**
* 👇 Claim all clients immediately. This handles the case when subdomain is
* loaded for the first time, and config is updated and then a pre-fetch is
Expand Down Expand Up @@ -84,7 +87,11 @@ self.addEventListener('fetch', (event) => {
const urlString = request.url
const url = new URL(urlString)

if (!isValidRequestForSW(event)) {
if (isConfigPageRequest(url) || isSwAssetRequest(event)) {
// get the assets from the server
trace('helia-sw: config page or js asset request, ignoring ', urlString)
return
} else if (!isValidRequestForSW(event)) {
trace('helia-sw: not a valid request for helia-sw, ignoring ', urlString)
return
} else {
Expand Down Expand Up @@ -129,6 +136,21 @@ self.addEventListener('fetch', (event) => {
* Functions
******************************************************
*/
async function getVerifiedFetch (): Promise<VerifiedFetch> {
const config = await getConfig()
log(`config-debug: got config for sw location ${self.location.origin}`, config)

const verifiedFetch = await createVerifiedFetch({
gateways: config.gateways ?? ['https://trustless-gateway.link'],
routers: config.routers ?? ['https://delegated-ipfs.dev'],
dnsResolvers: ['https://delegated-ipfs.dev/dns-query'].map(dnsJsonOverHttps)
}, {
contentTypeParser
})

return verifiedFetch
}

function isRootRequestForContent (event: FetchEvent): boolean {
const urlIsPreviouslyIntercepted = urlInterceptRegex.some(regex => regex.test(event.request.url))
const isRootRequest = urlIsPreviouslyIntercepted
Expand All @@ -143,6 +165,10 @@ function isSubdomainRequest (event: FetchEvent): boolean {
return id != null && protocol != null
}

function isConfigPageRequest (url: URL): boolean {
return isConfigPage(url.hash)
}

function isValidRequestForSW (event: FetchEvent): boolean {
return isSubdomainRequest(event) || isRootRequestForContent(event)
}
Expand Down Expand Up @@ -172,6 +198,11 @@ function getVerifiedFetchUrl ({ protocol, id, path }: GetVerifiedFetchUrlOptions
return `${namespaceString}://${pathRootString}/${contentPath}`
}

function isSwAssetRequest (event: FetchEvent): boolean {
const isActualSwAsset = /^.+\/(?:ipfs-sw-).+\.js$/.test(event.request.url)
return isActualSwAsset
}

async function fetchHandler ({ path, request }: FetchHandlerArg): Promise<Response> {
/**
* > Any global variables you set will be lost if the service worker shuts down.
Expand Down
Loading

0 comments on commit 1e9a30a

Please sign in to comment.