mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-13 02:53:45 +00:00
chore(*): Add workbox service worker
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
import process from 'node:process'
|
||||||
|
import AdapterNode from '@sveltejs/adapter-node';
|
||||||
|
import AdpaterStatic from '@sveltejs/adapter-static';
|
||||||
|
|
||||||
|
export const nodeAdapter = process.env.NODE_ADAPTER === 'true'
|
||||||
|
|
||||||
|
export const adapter = nodeAdapter ? AdapterNode() : AdpaterStatic()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
|
||||||
|
test('Test offline and trailing slashes', async ({ browser }) => {
|
||||||
|
// test offline + trailing slashes routes
|
||||||
|
const context = await browser.newContext();
|
||||||
|
const offlinePage = await context.newPage();
|
||||||
|
await offlinePage.goto('/');
|
||||||
|
const offlineSwURL = await offlinePage.evaluate(async () => {
|
||||||
|
const registration = await Promise.race([
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
navigator.serviceWorker.ready,
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('Service worker registration failed: time out')), 10000)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
// @ts-expect-error registration is of type unknown
|
||||||
|
return registration.active?.scriptURL;
|
||||||
|
});
|
||||||
|
const offlineSwName = 'sw.js';
|
||||||
|
expect(offlineSwURL).toBe(`http://localhost:4173/${offlineSwName}`);
|
||||||
|
await context.setOffline(true);
|
||||||
|
const aboutAnchor = offlinePage.getByRole('link', { name: 'About' });
|
||||||
|
expect(await aboutAnchor.getAttribute('href')).toBe('/about');
|
||||||
|
await aboutAnchor.click({ noWaitAfter: false });
|
||||||
|
const url = await offlinePage.evaluate(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||||
|
return location.href;
|
||||||
|
});
|
||||||
|
expect(url).toBe('http://localhost:4173/about');
|
||||||
|
expect(offlinePage.locator('li[aria-current="page"] a').getByText('About')).toBeTruthy();
|
||||||
|
await offlinePage.reload({ waitUntil: 'load' });
|
||||||
|
expect(offlinePage.url()).toBe('http://localhost:4173/about');
|
||||||
|
expect(offlinePage.locator('li[aria-current="page"] a').getByText('About')).toBeTruthy();
|
||||||
|
// Dispose context once it's no longer needed.
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
|
||||||
|
test('The service worker is registered and cache storage is present', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
const swURL = await page.evaluate(async () => {
|
||||||
|
const registration = await Promise.race([
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
navigator.serviceWorker.ready,
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('Service worker registration failed: time out')), 10000)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
// @ts-expect-error registration is of type unknown
|
||||||
|
return registration.active?.scriptURL;
|
||||||
|
});
|
||||||
|
const swName = 'sw.js';
|
||||||
|
expect(swURL).toBe(`http://localhost:4173/${swName}`);
|
||||||
|
|
||||||
|
const cacheContents = await page.evaluate(async () => {
|
||||||
|
const cacheState: Record<string, Array<string>> = {};
|
||||||
|
for (const cacheName of await caches.keys()) {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
cacheState[cacheName] = (await cache.keys()).map((req) => req.url);
|
||||||
|
}
|
||||||
|
return cacheState;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Object.keys(cacheContents).length).toEqual(1);
|
||||||
|
|
||||||
|
const key = 'workbox-precache-v2-http://localhost:4173/';
|
||||||
|
|
||||||
|
expect(Object.keys(cacheContents)[0]).toEqual(key);
|
||||||
|
|
||||||
|
const urls = cacheContents[key].map((url) => url.slice('http://localhost:4173/'.length));
|
||||||
|
|
||||||
|
/*
|
||||||
|
'http://localhost:4173/about?__WB_REVISION__=38251751d310c9b683a1426c22c135a2',
|
||||||
|
'http://localhost:4173/?__WB_REVISION__=073370aa3804305a787b01180cd6b8aa',
|
||||||
|
'http://localhost:4173/manifest.webmanifest?__WB_REVISION__=27df2fa4f35d014b42361148a2207da3'
|
||||||
|
*/
|
||||||
|
expect(urls.some((url) => url.startsWith('manifest.webmanifest?__WB_REVISION__='))).toEqual(true);
|
||||||
|
expect(urls.some((url) => url.startsWith('?__WB_REVISION__='))).toEqual(true);
|
||||||
|
expect(urls.some((url) => url.startsWith('about?__WB_REVISION__='))).toEqual(true);
|
||||||
|
// dontCacheBustURLsMatching: any asset in _app/immutable folder shouldn't have a revision (?__WB_REVISION__=)
|
||||||
|
expect(urls.some((url) => url.startsWith('_app/immutable/') && url.endsWith('.css'))).toEqual(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(urls.some((url) => url.startsWith('_app/immutable/') && url.endsWith('.js'))).toEqual(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(urls.some((url) => url.includes('_app/version.json?__WB_REVISION__='))).toEqual(true);
|
||||||
|
});
|
||||||
Generated
+5827
-5
File diff suppressed because it is too large
Load Diff
+26
-7
@@ -4,22 +4,41 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npx tailwindcss -i ./static/input.css -o ./static/output.css && vite dev",
|
"dev": "npx tailwindcss -i ./static/input.css -o ./static/output.css && vite dev",
|
||||||
|
"dev-generate": "GENERATE_SW=true npx tailwindcss -i ./static/input.css -o ./static/output.css && vite dev",
|
||||||
|
"dev-generate-suppress-w": "GENERATE_SW=true SUPPRESS_WARNING=true npx tailwindcss -i ./static/input.css -o ./static/output.css && vite dev",
|
||||||
|
"build-generate-sw": "GENERATE_SW=true vite build",
|
||||||
|
"build-generate-sw-node": "NODE_ADAPTER=true GENERATE_SW=true vite build",
|
||||||
"build": "npx tailwindcss -i ./static/input.css -o ./static/output.css && vite build",
|
"build": "npx tailwindcss -i ./static/input.css -o ./static/output.css && vite build",
|
||||||
"preview": "vite preview",
|
"build-inject-manifest-node": "NODE_ADAPTER=true vite build",
|
||||||
|
"build-self-destroying": "SELF_DESTROYING_SW=true vite build",
|
||||||
|
"preview": "vite preview --port=4173",
|
||||||
|
"preview-node": "PORT=4173 node build",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check . && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"lint-fix": "npm run lint --fix",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"tailwind": "npx tailwindcss -i ./static/input.css -o ./static/output.css"
|
"tailwind": "npx tailwindcss -i ./static/input.css -o ./static/output.css",
|
||||||
|
"test-generate-sw": "npm run build-generate-sw && GENERATE_SW=true vitest run && GENERATE_SW=true playwright test",
|
||||||
|
"test-generate-sw-node": "npm run build-generate-sw-node && NODE_ADAPTER=true GENERATE_SW=true vitest run && NODE_ADAPTER=true GENERATE_SW=true playwright test",
|
||||||
|
"test-inject-manifest": "npm run build-inject-manifest && vitest run && playwright test",
|
||||||
|
"test-inject-manifest-node": "npm run build-inject-manifest-node && NODE_ADAPTER=true vitest run && NODE_ADAPTER=true playwright test",
|
||||||
|
"test": "npm run test-generate-sw && npm run test-generate-sw-node && npm run test-inject-manifest && npm run test-inject-manifest-node"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.37.1",
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
"@sveltejs/adapter-netlify": "^4.1.0",
|
"@sveltejs/adapter-netlify": "^4.1.0",
|
||||||
|
"@sveltejs/adapter-node": "^2.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.0",
|
||||||
"@sveltejs/kit": "^2.0.6",
|
"@sveltejs/kit": "^2.0.6",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/eslint": "^8.56.0",
|
"@types/eslint": "^8.56.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^7.0.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
|
"@vite-pwa/assets-generator": "^0.2.4",
|
||||||
|
"@vite-pwa/sveltekit": "^0.4.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"daisyui": "^4.10.1",
|
"daisyui": "^4.10.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
@@ -28,12 +47,12 @@
|
|||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"prettier-plugin-svelte": "^3.1.2",
|
"prettier-plugin-svelte": "^3.1.2",
|
||||||
"svelte": "^4.2.7",
|
"svelte": "^4.2.8",
|
||||||
"svelte-check": "^3.6.0",
|
"svelte-check": "^3.6.2",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^5.0.3"
|
"vitest": "^1.0.4"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
|
const url = 'http://localhost:4173'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
import { nodeAdapter } from './adapter.mjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// require('dotenv').config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './client-test',
|
||||||
|
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||||
|
outputDir: 'test-results/',
|
||||||
|
timeout: 5 * 1000,
|
||||||
|
expect: {
|
||||||
|
/**
|
||||||
|
* Maximum time expect() should wait for the condition to be met.
|
||||||
|
* For example in `await expect(locator).toHaveText();`
|
||||||
|
*/
|
||||||
|
timeout: 1000,
|
||||||
|
},
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: 'line',
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||||
|
actionTimeout: 0,
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: url,
|
||||||
|
//offline: true,
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
// {
|
||||||
|
// name: 'firefox',
|
||||||
|
// use: { ...devices['Desktop Firefox'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
// {
|
||||||
|
// name: 'webkit',
|
||||||
|
// use: { ...devices['Desktop Safari'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: nodeAdapter ? 'pnpm run preview-node' : 'pnpm run preview',
|
||||||
|
url,
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
createAppleSplashScreens,
|
||||||
|
defineConfig,
|
||||||
|
minimal2023Preset
|
||||||
|
} from '@vite-pwa/assets-generator/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
headLinkOptions: {
|
||||||
|
preset: '2023'
|
||||||
|
},
|
||||||
|
preset: {
|
||||||
|
...minimal2023Preset,
|
||||||
|
appleSplashScreens: createAppleSplashScreens(
|
||||||
|
{
|
||||||
|
padding: 0.3,
|
||||||
|
resizeOptions: { fit: 'contain', background: 'white' },
|
||||||
|
darkResizeOptions: { fit: 'contain', background: 'black' },
|
||||||
|
linkMediaOptions: {
|
||||||
|
log: true,
|
||||||
|
addMediaScreen: true,
|
||||||
|
xhtml: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
['iPad Air 9.7"']
|
||||||
|
)
|
||||||
|
},
|
||||||
|
images: 'static/favicon.png'
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
export const generateSW = process.env.GENERATE_SW === 'true'
|
||||||
Vendored
+19
-7
@@ -1,13 +1,25 @@
|
|||||||
|
import 'vite-plugin-pwa/svelte';
|
||||||
|
import 'vite-plugin-pwa/info';
|
||||||
|
import 'vite-plugin-pwa/pwa-assets';
|
||||||
|
|
||||||
// See https://kit.svelte.dev/docs/types#app
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
|
// and what to do when importing types
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
const __DATE__: string;
|
||||||
// interface Error {}
|
const __RELOAD_SW__: boolean;
|
||||||
// interface Locals {}
|
namespace App {
|
||||||
// interface PageData {}
|
// interface Error {}
|
||||||
// interface PageState {}
|
interface Locals {
|
||||||
// interface Platform {}
|
userid: string;
|
||||||
}
|
buildDate: string;
|
||||||
|
periodicUpdates: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -4,20 +4,6 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
<meta name="theme-color" content="#f00000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="A free and open source web app to track completion of a living Pokédex, which works offline."
|
|
||||||
/>
|
|
||||||
<meta property="og:title" content="Living Dex Tracker - A free Pokédex completion tool" />
|
|
||||||
<meta property="og:url" content="https://pokedex.jcreek.co.uk" />
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content="A free and open source web app to track completion of a living Pokédex, which works offline."
|
|
||||||
/>
|
|
||||||
<link rel="canonical" href="https://pokedex.jcreek.co.uk" />
|
|
||||||
<link rel="apple-touch-icon" href="%sveltekit.assets%/apple-touch-icon.png" />
|
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
<link rel="stylesheet" href="%sveltekit.assets%/output.css" />
|
<link rel="stylesheet" href="%sveltekit.assets%/output.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { useRegisterSW } from 'virtual:pwa-register/svelte';
|
||||||
|
const { needRefresh, updateServiceWorker, offlineReady } = useRegisterSW({
|
||||||
|
onRegistered(r) {
|
||||||
|
// uncomment following code if you want check for updates
|
||||||
|
// r && setInterval(() => {
|
||||||
|
// console.log('Checking for sw update')
|
||||||
|
// r.update()
|
||||||
|
// }, 20000 /* 20s for testing purposes */)
|
||||||
|
console.log(`SW Registered: ${r}`);
|
||||||
|
},
|
||||||
|
onRegisterError(error) {
|
||||||
|
console.log('SW registration error', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const close = () => {
|
||||||
|
offlineReady.set(false);
|
||||||
|
needRefresh.set(false);
|
||||||
|
};
|
||||||
|
$: toast = $offlineReady || $needRefresh;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if toast}
|
||||||
|
<div class="pwa-toast" role="alert">
|
||||||
|
<div class="message">
|
||||||
|
{#if $offlineReady}
|
||||||
|
<span> App ready to work offline </span>
|
||||||
|
{:else}
|
||||||
|
<span> New content available, click on reload button to update. </span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if $needRefresh}
|
||||||
|
<button on:click={() => updateServiceWorker(true)}> Reload </button>
|
||||||
|
{/if}
|
||||||
|
<button on:click={close}> Close </button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pwa-toast {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #8885;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: left;
|
||||||
|
box-shadow: 3px 4px 5px 0 #8885;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
.pwa-toast .message {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.pwa-toast button {
|
||||||
|
border: 1px solid #8885;
|
||||||
|
outline: none;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/// <reference lib="WebWorker" />
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference no-default-lib="true"/>
|
||||||
|
/// <reference lib="esnext" />
|
||||||
|
import {
|
||||||
|
cleanupOutdatedCaches,
|
||||||
|
createHandlerBoundToURL,
|
||||||
|
precacheAndRoute,
|
||||||
|
precache
|
||||||
|
} from 'workbox-precaching';
|
||||||
|
import { NavigationRoute, registerRoute } from 'workbox-routing';
|
||||||
|
|
||||||
|
declare let self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add root route to precache manifest
|
||||||
|
precache([{ url: '/', revision: null }]);
|
||||||
|
|
||||||
|
// self.__WB_MANIFEST is default injection point
|
||||||
|
precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
|
// clean old assets
|
||||||
|
cleanupOutdatedCaches();
|
||||||
|
|
||||||
|
let allowlist: undefined | RegExp[];
|
||||||
|
if (import.meta.env.DEV) allowlist = [/^\/$/];
|
||||||
|
|
||||||
|
// to allow work offline
|
||||||
|
registerRoute(new NavigationRoute(createHandlerBoundToURL('/'), { allowlist }));
|
||||||
@@ -6,6 +6,11 @@
|
|||||||
import SignIn from '$lib/components/SignIn.svelte';
|
import SignIn from '$lib/components/SignIn.svelte';
|
||||||
import SignOut from '$lib/components/SignOut.svelte';
|
import SignOut from '$lib/components/SignOut.svelte';
|
||||||
|
|
||||||
|
import { pwaInfo } from 'virtual:pwa-info';
|
||||||
|
import { pwaAssetsHead } from 'virtual:pwa-assets/head';
|
||||||
|
|
||||||
|
$: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : '';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
let { supabase } = data;
|
let { supabase } = data;
|
||||||
$: ({ supabase } = data);
|
$: ({ supabase } = data);
|
||||||
@@ -18,6 +23,24 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await getUser();
|
await getUser();
|
||||||
|
|
||||||
|
if (pwaInfo) {
|
||||||
|
const { registerSW } = await import('virtual:pwa-register');
|
||||||
|
registerSW({
|
||||||
|
immediate: true,
|
||||||
|
onRegistered(r) {
|
||||||
|
// uncomment following code if you want check for updates
|
||||||
|
// r && setInterval(() => {
|
||||||
|
// console.log('Checking for sw update')
|
||||||
|
// r.update()
|
||||||
|
// }, 20000 /* 20s for testing purposes */)
|
||||||
|
console.log(`SW Registered: ${r}`);
|
||||||
|
},
|
||||||
|
onRegisterError(error) {
|
||||||
|
console.log('SW registration error', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getUser() {
|
async function getUser() {
|
||||||
@@ -37,6 +60,34 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
{#if pwaAssetsHead.themeColor}
|
||||||
|
<meta name="theme-color" content={pwaAssetsHead.themeColor.content} />
|
||||||
|
{/if}
|
||||||
|
{#if pwaAssetsHead.description}
|
||||||
|
<meta name="description" content={pwaAssetsHead.description.content} />
|
||||||
|
{/if}
|
||||||
|
{#each pwaAssetsHead.links as link}
|
||||||
|
<link {...link} />
|
||||||
|
{/each}
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html webManifestLink}
|
||||||
|
|
||||||
|
<!-- <meta
|
||||||
|
name="description"
|
||||||
|
content="A free and open source web app to track completion of a living Pokédex, which works offline."
|
||||||
|
/> -->
|
||||||
|
<meta property="og:title" content="Living Dex Tracker - A free Pokédex completion tool" />
|
||||||
|
<meta property="og:url" content="https://pokedex.jcreek.co.uk" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="A free and open source web app to track completion of a living Pokédex, which works offline."
|
||||||
|
/>
|
||||||
|
<link rel="canonical" href="https://pokedex.jcreek.co.uk" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<!-- <link rel="apple-touch-icon" href="%sveltekit.assets%/apple-touch-icon.png" /> -->
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<div class="navbar bg-base-100">
|
<div class="navbar bg-base-100">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -104,3 +155,7 @@
|
|||||||
<a class="link link-hover">Cookie policy</a>
|
<a class="link link-hover">Cookie policy</a>
|
||||||
</nav>
|
</nav>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
{#await import('$lib/components/pwa/ReloadPrompt.svelte') then { default: ReloadPrompt }}
|
||||||
|
<ReloadPrompt />
|
||||||
|
{/await}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<svelte:head>
|
||||||
|
<title>About</title>
|
||||||
|
<meta name="description" content="About this app" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<h1>About this app</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This is a <a href="https://kit.svelte.dev">SvelteKit</a> app. You can make your own by typing the
|
||||||
|
following into your command line and following the prompts:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre>npm create svelte@latest</pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The page you're looking at is purely static HTML, with no client-side interactivity needed.
|
||||||
|
Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening
|
||||||
|
the devtools network panel and reloading.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--column-width);
|
||||||
|
margin: var(--column-margin-top) auto 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { dev } from '$app/environment';
|
||||||
|
|
||||||
|
// we don't need any JS on this page, though we'll load
|
||||||
|
// it in dev so that we get hot module replacement...
|
||||||
|
export const csr = dev;
|
||||||
|
|
||||||
|
// since there's no dynamic data here, we can prerender
|
||||||
|
// it so that it gets served as a static asset in prod
|
||||||
|
export const prerender = true;
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/// <reference types="@sveltejs/kit" />
|
|
||||||
import { build, files, version } from '$service-worker';
|
|
||||||
|
|
||||||
// Create a unique cache name for this deployment
|
|
||||||
const CACHE = `cache-${version}`;
|
|
||||||
|
|
||||||
const ASSETS = [
|
|
||||||
...build, // the app itself
|
|
||||||
...files // everything in `static`
|
|
||||||
];
|
|
||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
|
||||||
// Create a new cache and add all files to it
|
|
||||||
async function addFilesToCache() {
|
|
||||||
const cache = await caches.open(CACHE);
|
|
||||||
await cache.addAll(ASSETS);
|
|
||||||
}
|
|
||||||
|
|
||||||
event.waitUntil(addFilesToCache());
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
|
||||||
// Remove previous cached data from disk
|
|
||||||
async function deleteOldCaches() {
|
|
||||||
for (const key of await caches.keys()) {
|
|
||||||
if (key !== CACHE) await caches.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.waitUntil(deleteOldCaches());
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('fetch', (event) => {
|
|
||||||
// ignore POST requests etc
|
|
||||||
if (event.request.method !== 'GET') return;
|
|
||||||
|
|
||||||
async function respond() {
|
|
||||||
const url = new URL(event.request.url);
|
|
||||||
const cache = await caches.open(CACHE);
|
|
||||||
|
|
||||||
// `build`/`files` can always be served from the cache
|
|
||||||
if (ASSETS.includes(url.pathname)) {
|
|
||||||
const response = await cache.match(url.pathname);
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// for everything else, try the network first, but
|
|
||||||
// fall back to the cache if we're offline
|
|
||||||
try {
|
|
||||||
const response = await fetch(event.request);
|
|
||||||
|
|
||||||
// if we're offline, fetch can return a value that is not a Response
|
|
||||||
// instead of throwing - and we can't pass this non-Response to respondWith
|
|
||||||
if (!(response instanceof Response)) {
|
|
||||||
throw new Error('invalid response from fetch');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
cache.put(event.request, response.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (err) {
|
|
||||||
const response = await cache.match(event.request);
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there's no cache, then just error out
|
|
||||||
// as there is nothing we can do to respond to this request
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.respondWith(respond());
|
|
||||||
});
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Living Dex Tracker",
|
|
||||||
"short_name": "Living Dex Tracker",
|
|
||||||
"description": "A tool to enable you to track your progress in completing the living Pokédex in Pokémon games.",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/android-chrome-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": "/",
|
|
||||||
"theme_color": "#f00000",
|
|
||||||
"background_color": "#f0f0f0",
|
|
||||||
"display": "standalone"
|
|
||||||
}
|
|
||||||
+10
-1
@@ -1,5 +1,7 @@
|
|||||||
import adapter from '@sveltejs/adapter-netlify';
|
import adapter from '@sveltejs/adapter-netlify';
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
// you don't need to do this if you're using generateSW strategy in your app
|
||||||
|
import { generateSW } from './pwa.mjs';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
@@ -17,7 +19,14 @@ const config = {
|
|||||||
// instead of creating a single one for the entire app.
|
// instead of creating a single one for the entire app.
|
||||||
// if `edge` is true, this option cannot be used
|
// if `edge` is true, this option cannot be used
|
||||||
split: false
|
split: false
|
||||||
})
|
}),
|
||||||
|
serviceWorker: {
|
||||||
|
register: true
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
// you don't need to do this if you're using generateSW strategy in your app
|
||||||
|
serviceWorker: generateSW ? undefined : 'src/prompt-sw.ts'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { existsSync, readFileSync } from 'node:fs'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
import { generateSW } from '../pwa.mjs'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
import { nodeAdapter } from '../adapter.mjs'
|
||||||
|
|
||||||
|
describe(`test-build: ${nodeAdapter ? 'node' : 'static'} adapter`, () => {
|
||||||
|
it(`service worker is generated: ${generateSW ? 'sw.js' : 'prompt-sw.js'}`, () => {
|
||||||
|
const swName = `./build/${nodeAdapter ? 'client/': ''}${generateSW ? 'sw.js' : 'prompt-sw.js'}`
|
||||||
|
expect(existsSync(swName), `${swName} doesn't exist`).toBeTruthy()
|
||||||
|
const webManifest = `./build/${nodeAdapter ? 'client/': ''}manifest.webmanifest`
|
||||||
|
expect(existsSync(webManifest), `${webManifest} doesn't exist`).toBeTruthy()
|
||||||
|
const swContent = readFileSync(swName, 'utf-8')
|
||||||
|
let match: RegExpMatchArray | null
|
||||||
|
if (generateSW) {
|
||||||
|
match = swContent.match(/define\(\['\.\/(workbox-\w+)'/)
|
||||||
|
expect(match && match.length === 2, `workbox-***.js entry not found in ${swName}`).toBeTruthy()
|
||||||
|
const workboxName = `./build/${nodeAdapter ? 'client/': ''}${match?.[1]}.js`
|
||||||
|
expect(existsSync(workboxName),`${workboxName} doesn't exist`).toBeTruthy()
|
||||||
|
}
|
||||||
|
match = swContent.match(/"url":\s*"manifest\.webmanifest"/)
|
||||||
|
expect(match && match.length === 1, 'missing manifest.webmanifest in sw precache manifest').toBeTruthy()
|
||||||
|
match = swContent.match(/"url":\s*"\/"/)
|
||||||
|
expect(match && match.length === 1, 'missing entry point route (/) in sw precache manifest').toBeTruthy()
|
||||||
|
match = swContent.match(/"url":\s*"about"/)
|
||||||
|
expect(match && match.length === 1,'missing about route (/about) in sw precache manifest').toBeTruthy()
|
||||||
|
if (nodeAdapter) {
|
||||||
|
match = swContent.match(/"url":\s*"server\//)
|
||||||
|
expect(match === null, 'found server/ entries in sw precache manifest').toBeTruthy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
+9
-2
@@ -9,8 +9,15 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "bundler"
|
"moduleResolution": "bundler",
|
||||||
}
|
"types": [
|
||||||
|
"vite-plugin-pwa/svelte",
|
||||||
|
"vite-plugin-pwa/vanillajs",
|
||||||
|
"vite-plugin-pwa/info",
|
||||||
|
"virtual:pwa-assets"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["node_modules/vite-plugin-pwa/client.d.ts", "node_modules/**/*.d.ts"]
|
||||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||||
//
|
//
|
||||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
|||||||
+60
-1
@@ -1,6 +1,65 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
|
||||||
|
// you don't need to do this if you're using generateSW strategy in your app
|
||||||
|
import { generateSW } from './pwa.mjs';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()]
|
plugins: [
|
||||||
|
sveltekit(),
|
||||||
|
SvelteKitPWA({
|
||||||
|
srcDir: './src',
|
||||||
|
mode: 'development',
|
||||||
|
// you don't need to do this if you're using generateSW strategy in your app
|
||||||
|
strategies: generateSW ? 'generateSW' : 'injectManifest',
|
||||||
|
// you don't need to do this if you're using generateSW strategy in your app
|
||||||
|
filename: generateSW ? undefined : 'prompt-sw.ts',
|
||||||
|
scope: '/',
|
||||||
|
base: '/',
|
||||||
|
selfDestroying: process.env.SELF_DESTROYING_SW === 'true',
|
||||||
|
pwaAssets: {
|
||||||
|
config: true
|
||||||
|
},
|
||||||
|
manifest: {
|
||||||
|
name: 'Living Dex Tracker',
|
||||||
|
short_name: 'Living Dex Tracker',
|
||||||
|
description:
|
||||||
|
'A tool to enable you to track your progress in completing the living Pokédex in Pokémon games.',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/android-chrome-192x192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/android-chrome-512x512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any maskable'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
start_url: '/',
|
||||||
|
scope: '/',
|
||||||
|
display: 'standalone',
|
||||||
|
theme_color: '#f00000',
|
||||||
|
background_color: '#f0f0f0'
|
||||||
|
},
|
||||||
|
injectManifest: {
|
||||||
|
globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2,webmanifest}']
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2,webmanifest}']
|
||||||
|
},
|
||||||
|
devOptions: {
|
||||||
|
enabled: false,
|
||||||
|
suppressWarnings: process.env.SUPPRESS_WARNING === 'true',
|
||||||
|
type: 'module',
|
||||||
|
navigateFallback: '/'
|
||||||
|
},
|
||||||
|
// if you have shared info in svelte config file put in a separate module and use it also here
|
||||||
|
kit: {
|
||||||
|
includeVersionFile: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ['test/*.test.ts']
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user