Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,12 @@ The main goals of this project are:
You can build and run the project locally:

```console
> npm ci
> npm i
> npm run build
> npm start
```

Now open your browser and go to `http://localhost:3333`
Now open your browser and go to `http://localhost:3000`

Below is an explanation of the different URLs and what they do:

Expand Down Expand Up @@ -137,4 +138,3 @@ This project is dual-licensed under
`SPDX-License-Identifier: Apache-2.0 OR MIT`

See [LICENSE](./LICENSE) for more details.

63 changes: 60 additions & 3 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,56 @@ const renameSwPlugin = {
}
}

/**
* Replaces strings with paths to built files, e.g. `'<%-- src/app.tsx --%>'`
* becomes `'./path/to/app-HASH.js'`
*
* @type {esbuild.Plugin}
*/
const replaceImports = {
name: 'modify-built-files',
setup (build) {
build.onEnd(async (result) => {
const metafile = result.metafile

// Replace '<%-- src --%>' with 'path/to/built/file'
const buildInfo = {}
const jsFiles = []

for (const [outputFile, meta] of Object.entries(metafile.outputs)) {
if (outputFile.endsWith('.css')) {
buildInfo[Object.keys(meta.inputs)[0]] = outputFile
} else if (outputFile.endsWith('.svg')) {
buildInfo[Object.keys(meta.inputs)[0]] = outputFile
} else if (outputFile.endsWith('.js') && meta.entryPoint != null) {
buildInfo[meta.entryPoint] = outputFile
jsFiles.push(outputFile)
} else if (outputFile.endsWith('.map')) {
// ignore
} else {
console.info('Unknown file type:', outputFile, meta)
}
}

const regex = /<%--\s(.*)\s--%>/g

for (const jsFile of jsFiles) {
let file = await fs.readFile(path.resolve(jsFile), 'utf-8')

for (const [target, source] of file.matchAll(regex)) {
const bundledFile = buildInfo[source].replace('dist', '')

console.info('Replace', target, 'with', bundledFile, 'in', jsFile)

file = file.replaceAll(target, bundledFile)
}

await fs.writeFile(path.resolve(jsFile), file)
}
})
}
}

/**
* Plugin to modify built files by running post-build tasks.
*
Expand Down Expand Up @@ -242,23 +292,30 @@ const excludeFilesPlugin = (extensions) => ({
* @type {esbuild.BuildOptions}
*/
export const buildOptions = {
entryPoints: ['src/index.tsx', 'src/sw.ts', 'src/app.tsx', 'src/ipfs-sw-*.ts', 'src/ipfs-sw-*.css'],
entryPoints: [
'src/index.tsx',
'src/sw.ts',
'src/app.tsx',
'src/internal-error.tsx',
'src/ipfs-sw-*.ts',
'src/ipfs-sw-*.css'
],
bundle: true,
outdir: 'dist',
loader: {
'.js': 'jsx',
'.css': 'css',
'.svg': 'file'
},
minify: true,
minify: false,
sourcemap: true,
metafile: true,
splitting: false,
target: ['es2020'],
format: 'esm',
entryNames: 'ipfs-sw-[name]-[hash]',
assetNames: 'ipfs-sw-[name]-[hash]',
plugins: [renameSwPlugin, modifyBuiltFiles, excludeFilesPlugin(['.eot?#iefix', '.otf', '.woff', '.woff2'])]
plugins: [replaceImports, renameSwPlugin, modifyBuiltFiles, excludeFilesPlugin(['.eot?#iefix', '.otf', '.woff', '.woff2'])]
}

const ctx = await esbuild.context(buildOptions)
Expand Down
3 changes: 2 additions & 1 deletion public/ipfs-sw-504.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ <h1 class="e2e-header-title f3 fw2 aqua ttu sans-serif">Service Worker Gateway <
$anchor.classList.add('dib')
}
}
checkUrl()
}
checkUrl()
</script>
</body>

Expand Down
7 changes: 4 additions & 3 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,19 @@ async function renderUi (): Promise<void> {

const LazyConfig = React.lazy(async () => import('./pages/config.jsx'))
const LazyHelperUi = React.lazy(async () => import('./pages/helper-ui.jsx'))
const LazyServiceWorkerErrorPage = React.lazy(async () => import('./pages/errors/no-service-worker.jsx'))
const LazyNoServiceWorkerErrorPage = React.lazy(async () => import('./pages/errors/no-service-worker.jsx'))
const LazySubdomainWarningPage = React.lazy(async () => import('./pages/subdomain-warning.jsx'))

let ErrorPage: null | React.LazyExoticComponent<() => ReactElement> = LazyServiceWorkerErrorPage
let ErrorPage: null | React.LazyExoticComponent<() => ReactElement> = LazyNoServiceWorkerErrorPage

if ('serviceWorker' in navigator) {
ErrorPage = null
}

const routes: Route[] = [
{ default: true, component: ErrorPage ?? LazyHelperUi },
{ shouldRender: async () => renderChecks.shouldRenderConfigPage(), component: LazyConfig },
{ shouldRender: async () => renderChecks.shouldRenderNoServiceWorkerError(), component: LazyServiceWorkerErrorPage },
{ shouldRender: async () => renderChecks.shouldRenderNoServiceWorkerError(), component: LazyNoServiceWorkerErrorPage },
{ shouldRender: renderChecks.shouldRenderSubdomainWarningPage, component: LazySubdomainWarningPage }
]

Expand Down
15 changes: 15 additions & 0 deletions src/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react'
import type { PropsWithChildren, ReactElement } from 'react'

export interface ButtonProps extends PropsWithChildren {
onClick: any
className?: string
}

export function Button ({ onClick, className, children }: ButtonProps): ReactElement {
return (
<>
<button className={`button bn br2 mr2 pa2 pl3 pr3 snow-muted ${className ?? ''}`} onClick={onClick}>{children}</button>
</>
)
}
19 changes: 19 additions & 0 deletions src/components/content-box.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'
import type { PropsWithChildren, ReactElement } from 'react'

export interface ContentBoxProps extends PropsWithChildren {
title: string
}

export default function ContentBox ({ title, children }: ContentBoxProps): ReactElement {
return (
<main className='ma3 br2 ba sans-serif ba br2 b--gray-muted'>
<header className='pt2 pb2 pl3 pr3 bg-snow bb b--gray-muted'>
<strong>{title}</strong>
</header>
<section className='pt2 pb2 pl3 pr3 bg-white'>
{children}
</section>
</main>
)
}
10 changes: 10 additions & 0 deletions src/components/terminal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import type { PropsWithChildren, ReactElement } from 'react'

export default function Terminal ({ children }: PropsWithChildren): ReactElement {
return (
<pre className='terminal br2 ma2 pa3 snow-muted'>
{children}
</pre>
)
}
12 changes: 12 additions & 0 deletions src/internal-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react'
import { hydrateRoot } from 'react-dom/client'
import { InternalErrorPage } from './pages/errors/internal-error.jsx'
import { Page } from './pages/page.js'

hydrateRoot(document, (
<>
<Page>
<InternalErrorPage />
</Page>
</>
))
15 changes: 9 additions & 6 deletions src/ipfs-sw-first-hit.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
/**
* This script is injected into the ipfs-sw-first-hit.html file. This was added when addressing an issue with redirects not preserving query parameters.
* This script is injected into the ipfs-sw-first-hit.html file. This was added
* when addressing an issue with redirects not preserving query parameters.
*
* The solution we're moving forward with is, instead of using 302 redirects with ipfs _redirects file, we are
* using 200 responses with the ipfs-sw-first-hit.html file. That file will include the ipfs-sw-first-hit.js script
* which will be injected into the index.html file, and handle the redirect logic for us.
* The solution we're moving forward with is, instead of using 302 redirects
* with ipfs _redirects file, we are using 200 responses with the
* ipfs-sw-first-hit.html file. That file will include the ipfs-sw-first-hit.js
* script which will be injected into the index.html file, and handle the
* redirect logic for us.
*
* It handles the logic for the first hit to the service worker and should only
* ever run when _redirects file redirects to ipfs-sw-first-hit.html for /ipns
* or /ipfs paths when the service worker is not yet intercepting requests.
*
* Sometimes, redirect solutions do not support redirecting directly to this page, in which case it should be handled
* by index.tsx instead.
* Sometimes, redirect solutions do not support redirecting directly to this
* page, in which case it should be handled by index.tsx instead.
*
* @see https://github.com/ipfs/service-worker-gateway/issues/628
*/
Expand Down
2 changes: 1 addition & 1 deletion src/lib/path-or-subdomain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const findOriginIsolationRedirect = async (location: Pick<Location, 'prot
log?.trace('subdomain support is disabled')
}
}
log?.trace('no need to check for subdomain support', isPathGatewayRequest(location), isSubdomainGatewayRequest(location))
log?.trace('no need to check for subdomain support - is path gateway request %s, is subdomain gateway request %s', isPathGatewayRequest(location), isSubdomainGatewayRequest(location))
return null
}

Expand Down
47 changes: 46 additions & 1 deletion src/pages/default-page-styles.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
@import 'tachyons';
@import 'ipfs-css';

body {
--navy: #0b3a53;
--navy-muted: #244e66;
--aqua: #69c4cd;
--aqua-muted: #9ad4db;
--gray: #b7bbc8;
--gray-muted: #d9dbe2;
--charcoal: #34373f;
--charcoal-muted: #7f8491;
--red: #ea5037;
--red-muted: #f36149;
--yellow: #f39021;
--yellow-muted: #f9a13e;
--teal: #378085;
--teal-muted: #439a9d;
--green: #0cb892;
--green-muted: #0aca9f;
--snow: #edf0f4;
--snow-muted: #f7f8fa;
--link: #117eb3;
--washed-blue: #F0F6FA;
--steel-gray: #3f5667;
}

/* ensure we don't fetch any external fonts */
.sans-serif { font-family: system-ui, sans-serif; }
.sans-serif {
font-family: system-ui, sans-serif;
}

.button {
display: inline-block;
cursor: pointer;
}

.button:hover {
background-color: var(--navy);
}

.terminal {
background-color: var(--steel-gray);
overflow: scroll;
}

.link:hover {
text-decoration: underline;
color: var(--aqua-muted);
}
87 changes: 87 additions & 0 deletions src/pages/errors/internal-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Page to display a user friendly message when `navigator.serviceWorker` is not available.
*/

import React from 'react'
import { Button } from '../../button.jsx'
import Header from '../../components/Header.jsx'
import ContentBox from '../../components/content-box.jsx'
import Terminal from '../../components/terminal.jsx'
import type { ReactElement } from 'react'

declare global {
var props: any
}

export interface InternalErrorPageProps {
error?: any,
response?: any
config?: any
}

export function InternalErrorPage ({ error, response, config }: InternalErrorPageProps): ReactElement {
function retry (): void {
// @ts-expect-error boolean argument is firefox-only
window.location.reload(true)
}

function goBack (): void {
window.history.back()
}

error = error ?? globalThis.props?.error
response = response ?? globalThis.props?.response
config = config ?? globalThis.props?.config

let errorDisplay = <></>

if (error != null) {
errorDisplay = (
<>
<h3>Error</h3>
<Terminal>{error.stack ?? error.message ?? error.toString()}</Terminal>
</>
)
}

let responseDisplay = <></>

if (response != null) {
responseDisplay = (
<>
<h3>Response</h3>
<Terminal>{JSON.stringify(response, null, 2)}</Terminal>
</>
)
}

let configDisplay = <></>

if (config != null) {
configDisplay = (
<>
<h3>Configuration</h3>
<Terminal>{JSON.stringify(config, null, 2)}</Terminal>
</>
)
}

return (
<>
<Header />
<ContentBox title='Internal Error'>
<>
<p>An error occurred in the service worker gateway.</p>
<p>Please <a href='https://github.com/ipfs/service-worker-gateway/issues' className='link' target='_blank' rel='noopener noreferrer'>open an issue</a> with the URL you tried to access and any debugging information displayed below.</p>
<p>
<Button className='bg-teal' onClick={retry}>Retry</Button>
<Button className='bg-navy-muted' onClick={goBack}>Go back</Button>
</p>
{errorDisplay}
{responseDisplay}
{configDisplay}
</>
</ContentBox>
</>
)
}
Loading
Loading