Compare commits
No commits in common. "main" and "v0.8.11" have entirely different histories.
46 changed files with 2338 additions and 4822 deletions
11
.build.yml
11
.build.yml
|
@ -1,11 +0,0 @@
|
|||
image: alpine/edge
|
||||
secrets:
|
||||
- bbfcb6dc-7c4a-42ee-a11a-022f0339a133
|
||||
environment:
|
||||
REPO: astro-sw
|
||||
GH_USER: ayoayco
|
||||
tasks:
|
||||
- push-mirror: |
|
||||
cd ~/"${REPO}"
|
||||
git config --global credential.helper store
|
||||
git push --mirror "https://github.com/${GH_USER}/${REPO}"
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -7,4 +7,3 @@ package-lock.json
|
|||
*swo
|
||||
*swp
|
||||
|
||||
.eslintcache
|
|
@ -1 +0,0 @@
|
|||
npm run check
|
|
@ -1,7 +0,0 @@
|
|||
# someday let's think about formatting html
|
||||
**/*.html
|
||||
|
||||
**/*.md
|
||||
**/*.css
|
||||
**/*.yml
|
||||
**/*.yaml
|
91
README.md
91
README.md
|
@ -9,28 +9,16 @@ The integration accepts the path to your service worker and automatically inject
|
|||
|
||||
It works on all Astro output options: `static`, `server`, or `hybrid`, and lets developers retain the flexibility for various [caching strategies](https://developer.chrome.com/docs/workbox/caching-strategies-overview/).
|
||||
|
||||
## Background
|
||||
|
||||
This integration was originally developed to support the Caching strategy needs of [Cozy](https://cozy.pub) -- the modern reading companion for the Web. You can find [an example service worker in the repository](https://github.com/ayoayco/Cozy/blob/main/src/sw.mjs).
|
||||
|
||||
There is a work in progress adding `presets` for using common caching strategies and customizing the behavior of the service worker via config options. This aims to reduce the need for writing the service worker script by hand for most use cases.
|
||||
|
||||
Get in touch:
|
||||
1. Chat via Discord: [Ayo's Projects](https://discord.gg/kkvW7GYNAp)
|
||||
1. Submit tickets via [SourceHut todo](https://todo.sr.ht/~ayoayco/astro-sw)
|
||||
1. Start a [GitHub discussion](https://github.com/ayoayco/astro-sw/discussions)
|
||||
1. Email me: [ayo@ayco.io](mailto:ayo@ayco.io)
|
||||
|
||||
## Installation
|
||||
|
||||
In your [Astro](https://astro.build) project:
|
||||
|
||||
```bash
|
||||
# if using npm
|
||||
$ npm i @ayco/astro-sw
|
||||
$ npm i -D @ayco/astro-sw
|
||||
|
||||
# if using pnpm
|
||||
$ pnpm add @ayco/astro-sw
|
||||
$ pnpm add -D @ayco/astro-sw
|
||||
```
|
||||
|
||||
## Minimal Usage
|
||||
|
@ -45,11 +33,10 @@ export default defineConfig({
|
|||
integrations: [
|
||||
serviceWorker({
|
||||
path: "./src/sw.ts",
|
||||
}),
|
||||
],
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
For more options available, see the [API](#api).
|
||||
|
||||
## TypeScript support
|
||||
|
@ -65,10 +52,10 @@ export default defineConfig({
|
|||
serviceWorker({
|
||||
path: "./src/sw.ts",
|
||||
esbuild: {
|
||||
minify: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
minify: true
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -76,32 +63,11 @@ export default defineConfig({
|
|||
|
||||
The most important variable your service worker will have access to is `__assets`, which contains all routes and public assets that Astro includes in your build. Additionally, you will also get `__prefix` and `__version` you can use for naming & invalidating your Cache storage (useful for debugging purposes).
|
||||
|
||||
## `eslint` globals
|
||||
|
||||
Because of the injected variables not being defined in your script, you might get `eslint` errors for the undefined variables when you have the `no-undef` rule. To prevent this, you can use our exported `globals` object in your eslint config as follows:
|
||||
|
||||
```js
|
||||
import astroSwGlobals from "@ayco/astro-sw/globals";
|
||||
|
||||
export default [
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...astroSwGlobals,
|
||||
},
|
||||
},
|
||||
},
|
||||
// add more generic rule sets here, such as:
|
||||
// jsPlugin.configs.recommended,
|
||||
];
|
||||
```
|
||||
|
||||
## Registration Hooks
|
||||
|
||||
Hooks are provided for adding custom logic that triggers in various service worker registration events.
|
||||
|
||||
The following properties are available for the `registrationHooks` configuration:
|
||||
|
||||
1. `installing` - when the registration is 'installing'
|
||||
1. `waiting` - when the registration is 'waiting'
|
||||
1. `active` - when the registration is 'active'
|
||||
|
@ -119,17 +85,17 @@ export default defineConfig({
|
|||
path: "./src/sw.ts",
|
||||
registrationHooks: {
|
||||
afterRegistration: async () => {
|
||||
const sw = await navigator.serviceWorker.getRegistration();
|
||||
console.log(">>> registrered", sw);
|
||||
const sw = await navigator.serviceWorker.getRegistration();
|
||||
console.log('>>> registrered', sw)
|
||||
},
|
||||
installing: () => console.log("installing..."),
|
||||
waiting: () => console.log("waiting..."),
|
||||
active: () => console.log("active..."),
|
||||
installing: () => console.log('installing...'),
|
||||
waiting: () => console.log('waiting...'),
|
||||
active: () => console.log('active...'),
|
||||
error: (error) => console.error(error),
|
||||
unsupported: () => console.log(":("),
|
||||
},
|
||||
}),
|
||||
],
|
||||
unsupported: () => console.log(':('),
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -137,14 +103,17 @@ export default defineConfig({
|
|||
|
||||
The integration accepts a configuration object with the following properties
|
||||
|
||||
| property | type | required? | notes |
|
||||
| ------------------- | ---------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| path | string | required | path to your _own_ service worker script; no surprises & easy debugging |
|
||||
| assetCachePrefix | string | optional | cache storage name prefix |
|
||||
| assetCacheVersionID | string | optional | cache storage name versioning; by default, a random UUID is used |
|
||||
| customRoutes | string[] | optional | list of custom routes you want to be cached. Beware that non-existent routes that result to HTTP Error404 will cause the service worker to fail |
|
||||
| excludeRoutes | string[] | optional | list of routes you want to be ignored/removed from assets |
|
||||
| logAssets | boolean | optional | set to see a list of the assets found; defaults to false |
|
||||
| esbuild | [BuildOptions](https://esbuild.github.io/api/) | optional | custom build options for your service worker script |
|
||||
| registrationHooks | object | optional | provide callbacks for various registration events; see section on [Registration Hooks](#registration-hooks) |
|
||||
| property | type | required? | notes |
|
||||
| --- | --- | --- | --- |
|
||||
| path | string | required | path to your *own* service worker script; no surprises & easy debugging |
|
||||
| assetCachePrefix | string | optional | cache storage name prefix |
|
||||
| assetCacheVersionID | string | optional | cache storage name versioning; by default, a random UUID is used |
|
||||
| customRoutes | string[] | optional | list of custom routes you want to be cached. Beware that non-existent routes that result to HTTP Error404 will cause the service worker to fail |
|
||||
| excludeRoutes | string[] | optional | list of routes you want to be ignored/removed from assets |
|
||||
| logAssets | boolean | optional | set to see a list of the assets found; defaults to false |
|
||||
| esbuild | [BuildOptions](https://esbuild.github.io/api/) | optional | custom build options for your service worker script |
|
||||
| registrationHooks | object | optional | provide callbacks for various registration events; see section on [Registration Hooks](#registration-hooks) |
|
||||
|
||||
## Background
|
||||
|
||||
This integration was developed to support the Caching strategy needs of [Cozy](https://cozy.ayco.io) -- the modern reading companion for the Web. You can find [an example service worker in the repository](https://github.com/ayoayco/Cozy/blob/main/src/sw.mjs).
|
||||
|
|
239
astro-sw.js
Normal file
239
astro-sw.js
Normal file
|
@ -0,0 +1,239 @@
|
|||
/**
|
||||
* @license MIT <https://opensource.org/licenses/MIT>
|
||||
* @author Ayo Ayco <https://ayo.ayco.io>
|
||||
*/
|
||||
|
||||
import { readFile, writeFile, readdir, unlink } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve, dirname, join } from 'node:path';
|
||||
import { build } from 'esbuild';
|
||||
|
||||
const ASTROSW = '@ayco/astro-sw';
|
||||
/**
|
||||
* @typedef {import('astro').AstroIntegration} AstroIntegration
|
||||
* @typedef {import('esbuild').BuildOptions} BuildOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Accepts configuration options with service worker path
|
||||
* and injects needed variables such as `__assets` generated by Astro
|
||||
* @param {{
|
||||
* path: string,
|
||||
* assetCachePrefix?: string,
|
||||
* assetCacheVersionID?: string,
|
||||
* customRoutes?: string[],
|
||||
* excludeRoutes?: string[],
|
||||
* logAssets?: true,
|
||||
* esbuild?: BuildOptions,
|
||||
* registrationHooks?: {
|
||||
* installing?: () => void,
|
||||
* waiting?: () => void,
|
||||
* active?: () => void,
|
||||
* error?: (error) => void,
|
||||
* unsupported?: () => void,
|
||||
* afterRegistration?: () => void,
|
||||
* }
|
||||
* experimental?: {
|
||||
* strategy?: {
|
||||
* fetchFn: () => void,
|
||||
* installFn: () => void,
|
||||
* activateFn: () => void,
|
||||
* waitFn: () => void,
|
||||
* }
|
||||
* }
|
||||
* }} options
|
||||
* @returns {AstroIntegration}
|
||||
*/
|
||||
export default function serviceWorker(options) {
|
||||
let {
|
||||
assetCachePrefix = ASTROSW,
|
||||
assetCacheVersionID = '0',
|
||||
path: serviceWorkerPath = undefined,
|
||||
customRoutes = [],
|
||||
excludeRoutes = [],
|
||||
logAssets = false,
|
||||
esbuild = {},
|
||||
registrationHooks = {}
|
||||
} = options || {};
|
||||
|
||||
const {
|
||||
installing: installingFn = () => { },
|
||||
waiting: waitingFn = () => { },
|
||||
active: activeFn = () => { },
|
||||
error: errorFn = () => { },
|
||||
unsupported: unsupportedFn = () => { },
|
||||
afterRegistration: afterRegistrationFn = () => { },
|
||||
} = registrationHooks;
|
||||
|
||||
/**
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
let assets = [];
|
||||
|
||||
const registrationScript = `const registerSW = async () => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register("/sw.js", {
|
||||
scope: "/",
|
||||
});
|
||||
if (registration.installing) {
|
||||
(${installingFn.toString()})();
|
||||
} else if (registration.waiting) {
|
||||
(${waitingFn.toString()})();
|
||||
} else if (registration.active) {
|
||||
(${activeFn.toString()})();
|
||||
}
|
||||
|
||||
(${afterRegistrationFn.toString()})();
|
||||
} catch (error) {
|
||||
(${errorFn.toString()})(error);
|
||||
}
|
||||
} else {
|
||||
(${unsupportedFn.toString()})();
|
||||
}
|
||||
}
|
||||
|
||||
registerSW();`
|
||||
|
||||
let output = 'static';
|
||||
const __dirname = resolve(dirname('.'));
|
||||
|
||||
return {
|
||||
'name': ASTROSW,
|
||||
'hooks': {
|
||||
'astro:config:setup': async ({ injectScript, config: _config, command, logger }) => {
|
||||
|
||||
if (!serviceWorkerPath || serviceWorkerPath === '') {
|
||||
// REQUIRED OPTION IS MISSING
|
||||
logger.error('Missing required path to service worker script');
|
||||
}
|
||||
// const transformedScript=await transform(registrationScript)
|
||||
|
||||
output = _config.output;
|
||||
if (command === 'build') {
|
||||
injectScript('page', registrationScript);
|
||||
}
|
||||
},
|
||||
'astro:config:done': async ({ injectTypes, logger }) => {
|
||||
let injectedTypes = `
|
||||
declare const __assets: string[];
|
||||
declare const __version: string;
|
||||
declare const __prefix: string;`
|
||||
injectTypes({ filename: 'caching.d.ts', content: injectedTypes })
|
||||
},
|
||||
'astro:build:ssr': ({ manifest }) => {
|
||||
assets = manifest.assets
|
||||
},
|
||||
'astro:build:done': async ({ dir, routes, pages, logger }) => {
|
||||
const outfile = fileURLToPath(new URL('./sw.js', dir));
|
||||
const swPath = (serviceWorkerPath && serviceWorkerPath !== '')
|
||||
? join(__dirname, serviceWorkerPath)
|
||||
: undefined;
|
||||
let originalScript;
|
||||
|
||||
const _publicFiles = (await readdir(dir, { withFileTypes: true }) ?? [])
|
||||
.filter(dirent => dirent.isFile())
|
||||
.map(dirent => `/${dirent.name}`);
|
||||
|
||||
const _routes = routes
|
||||
.filter(({ isIndex }) => isIndex)
|
||||
.flatMap(({ pathname }) => pathname === '/' ? pathname : [pathname, `${pathname}/`])
|
||||
.filter(pathname => pathname !== '')
|
||||
?? [];
|
||||
|
||||
const _pages = pages
|
||||
.filter(({ pathname }) => pathname !== '')
|
||||
.map(({ pathname }) => `/${pathname}`)
|
||||
?? [];
|
||||
|
||||
const _pagesWithoutEndSlash = pages
|
||||
.filter(({ pathname }) => pathname !== '')
|
||||
.map(({ pathname }) => {
|
||||
const lastChar = pathname.slice(-1);
|
||||
const len = pathname.length;
|
||||
return lastChar === '/'
|
||||
? `/${pathname.slice(0, len - 1)}`
|
||||
: `/${pathname}`;
|
||||
})
|
||||
.filter(pathname => pathname !== '')
|
||||
?? [];
|
||||
|
||||
const _excludeRoutes = [
|
||||
...excludeRoutes,
|
||||
...excludeRoutes.map(route => `${route}/`)
|
||||
];
|
||||
|
||||
assets = [...new Set([
|
||||
...assets,
|
||||
..._routes,
|
||||
..._pages,
|
||||
..._pagesWithoutEndSlash,
|
||||
...customRoutes,
|
||||
..._publicFiles
|
||||
])].filter(asset => !!asset
|
||||
&& asset !== ''
|
||||
&& !asset.includes('404')
|
||||
&& !asset.includes('index.html')
|
||||
&& !_excludeRoutes.includes(asset)
|
||||
);
|
||||
|
||||
|
||||
if (logAssets) {
|
||||
logger.info(`${assets.length} assets for caching: \n ▶ ${assets.toString().replaceAll(',', '\n ▶ ')}\n`);
|
||||
} else {
|
||||
logger.info(`${assets.length} assets for caching.`);
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Using service worker in path: ${swPath}`);
|
||||
originalScript = await readFile(swPath);
|
||||
} catch {
|
||||
logger.error(`Service worker script not found! ${swPath}`)
|
||||
if (!swPath) {
|
||||
|
||||
logger.error(`
|
||||
|
||||
[${ASTROSW}] ERR: The 'path' option is required!
|
||||
[${ASTROSW}] INFO: Please see service worker options in https://ayco.io/gh/astro-sw#readme
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
const assetsDeclaration = `const __assets = ${JSON.stringify(assets)};\n`;
|
||||
const versionDeclaration = `const __version = ${JSON.stringify(assetCacheVersionID)};\n`;
|
||||
const prefixDeclaration = `const __prefix = ${JSON.stringify(assetCachePrefix)};\n`;
|
||||
|
||||
const tempFile = `${swPath}.tmp.ts`;
|
||||
|
||||
try {
|
||||
await writeFile(
|
||||
tempFile,
|
||||
assetsDeclaration + versionDeclaration + prefixDeclaration + originalScript,
|
||||
{ flag: 'w+' }
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err.toString())
|
||||
}
|
||||
|
||||
try {
|
||||
await build({
|
||||
bundle: true,
|
||||
...esbuild,
|
||||
outfile,
|
||||
platform: 'browser',
|
||||
entryPoints: [tempFile],
|
||||
})
|
||||
} catch (err) {
|
||||
logger.error(err.toString())
|
||||
}
|
||||
|
||||
// remove temp file
|
||||
try {
|
||||
await unlink(tempFile);
|
||||
} catch (err) {
|
||||
logger.error(err.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
38
astro.config.mjs
Normal file
38
astro.config.mjs
Normal file
|
@ -0,0 +1,38 @@
|
|||
// @ts-check
|
||||
|
||||
import { defineConfig } from "astro/config";
|
||||
import node from "@astrojs/node";
|
||||
import serviceWorker from "./astro-sw.js";
|
||||
|
||||
export default defineConfig({
|
||||
output: "hybrid",
|
||||
adapter: node({
|
||||
mode: "middleware"
|
||||
}),
|
||||
site: 'https://ayo.ayco.io',
|
||||
integrations: [
|
||||
serviceWorker({
|
||||
path: './src/example_sw.js',
|
||||
customRoutes: [
|
||||
// '/threads'
|
||||
],
|
||||
excludeRoutes: [
|
||||
'/exclude'
|
||||
],
|
||||
logAssets: true,
|
||||
esbuild: {
|
||||
minify: true
|
||||
},
|
||||
registrationHooks: {
|
||||
installing: () => console.log('>>> installing...'),
|
||||
waiting: () => console.log('>>> waiting...'),
|
||||
active: () => console.log('>>> active...'),
|
||||
error: (error) => console.error('>>> error', error),
|
||||
'afterRegistration': async () => {
|
||||
const sw = await navigator.serviceWorker.getRegistration();
|
||||
console.log('>>> registrered', sw)
|
||||
}
|
||||
},
|
||||
})
|
||||
]
|
||||
});
|
|
@ -1,39 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
import { defineConfig } from 'astro/config'
|
||||
import node from '@astrojs/node'
|
||||
import serviceWorker from '@ayco/astro-sw'
|
||||
import { deleteOldCaches, staleWhileRevalidate } from '@ayco/astro-sw/presets'
|
||||
|
||||
export default defineConfig({
|
||||
output: 'static',
|
||||
adapter: node({
|
||||
mode: 'middleware',
|
||||
}),
|
||||
site: 'https://ayo.ayco.io',
|
||||
integrations: [
|
||||
serviceWorker({
|
||||
path: './src/example_sw.js',
|
||||
presets: [staleWhileRevalidate(), deleteOldCaches()],
|
||||
customRoutes: [
|
||||
// '/threads'
|
||||
],
|
||||
excludeRoutes: ['/exclude'],
|
||||
assetCachePrefix: 'hey',
|
||||
logAssets: true,
|
||||
esbuild: {
|
||||
minify: true,
|
||||
},
|
||||
registrationHooks: {
|
||||
installing: () => console.log('>>> installing...'),
|
||||
waiting: () => console.log('>>> waiting...'),
|
||||
active: () => console.log('>>> active...'),
|
||||
error: (error) => console.error('>>> error', error),
|
||||
afterRegistration: async () => {
|
||||
const sw = await navigator.serviceWorker.getRegistration()
|
||||
console.log('>>> registrered', sw)
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"name": "demo",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "astro dev",
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"build:preview:static": "astro build && astro preview",
|
||||
"build:preview": "astro build && node ./server.mjs"
|
||||
},
|
||||
"author": "Ayo Ayco",
|
||||
"license": "MIT",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"astro": "^5.6.1",
|
||||
"@astrojs/node": "^9.1.3",
|
||||
"@fastify/middie": "^9.0.3",
|
||||
"@fastify/static": "^8.1.1",
|
||||
"astro-eslint-parser": "^1.2.2",
|
||||
"fastify": "^5.2.2",
|
||||
"@ayco/astro-sw": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import Fastify from 'fastify'
|
||||
import fastifyMiddie from '@fastify/middie'
|
||||
import fastifyStatic from '@fastify/static'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { handler as ssrHandler } from './dist/server/entry.mjs'
|
||||
|
||||
const app = Fastify({ logger: true })
|
||||
|
||||
await app
|
||||
.register(fastifyStatic, {
|
||||
root: fileURLToPath(new URL('./dist/client', import.meta.url)),
|
||||
})
|
||||
.register(fastifyMiddie)
|
||||
app.use(ssrHandler)
|
||||
|
||||
app.listen({ port: 4321 })
|
|
@ -1,16 +0,0 @@
|
|||
import { defineCollection, z } from 'astro:content'
|
||||
|
||||
const blog = defineCollection({
|
||||
type: 'content',
|
||||
// Type-check frontmatter using a schema
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
// Transform string to Date object
|
||||
pubDate: z.coerce.date(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
heroImage: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const collections = { blog }
|
|
@ -1,91 +0,0 @@
|
|||
/**
|
||||
* Note: @ayco/astro-sw integration injects variables `__prefix`, `__version`, & `__assets`
|
||||
* -- find usage in package readme; `astro.config.mjs` integrations
|
||||
* @see https://ayco.io/n/@ayco/astro-sw
|
||||
*/
|
||||
const cacheName = `${__prefix ?? 'app'}-v${__version ?? '000'}`
|
||||
const addResourcesToCache = async (resources) => {
|
||||
const cache = await caches.open(cacheName)
|
||||
console.log('adding resources to cache...', resources)
|
||||
await cache.addAll(resources)
|
||||
}
|
||||
|
||||
console.log('test log', { hello: 'world' })
|
||||
|
||||
const putInCache = async (request, response) => {
|
||||
const cache = await caches.open(cacheName)
|
||||
console.log('adding one response to cache...', request)
|
||||
await cache.put(request, response)
|
||||
}
|
||||
|
||||
const cacheFirst = async ({ request, preloadResponsePromise, fallbackUrl }) => {
|
||||
// First try to get the resource from the cache
|
||||
const responseFromCache = await caches.match(request)
|
||||
if (responseFromCache) {
|
||||
return responseFromCache
|
||||
}
|
||||
|
||||
// Next try to use the preloaded response, if it's there
|
||||
// NOTE: Chrome throws errors regarding preloadResponse, see:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1420515
|
||||
// https://github.com/mdn/dom-examples/issues/145
|
||||
// To avoid those errors, remove or comment out this block of preloadResponse
|
||||
// code along with enableNavigationPreload() and the "activate" listener.
|
||||
const preloadResponse = await preloadResponsePromise
|
||||
if (preloadResponse) {
|
||||
console.info('using preload response', preloadResponse)
|
||||
putInCache(request, preloadResponse.clone())
|
||||
return preloadResponse
|
||||
}
|
||||
|
||||
// Next try to get the resource from the network
|
||||
try {
|
||||
const responseFromNetwork = await fetch(request.clone())
|
||||
// response may be used only once
|
||||
// we need to save clone to put one copy in cache
|
||||
// and serve second one
|
||||
putInCache(request, responseFromNetwork.clone())
|
||||
return responseFromNetwork
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
const fallbackResponse = await caches.match(fallbackUrl)
|
||||
if (fallbackResponse) {
|
||||
return fallbackResponse
|
||||
}
|
||||
// when even the fallback response is not available,
|
||||
// there is nothing we can do, but we must always
|
||||
// return a Response object
|
||||
return new Response('Network error happened', {
|
||||
status: 408,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const enableNavigationPreload = async () => {
|
||||
if (self.registration.navigationPreload) {
|
||||
// Enable navigation preloads!
|
||||
await self.registration.navigationPreload.enable()
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('activating...', event)
|
||||
event.waitUntil(enableNavigationPreload())
|
||||
})
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('installing...', event)
|
||||
event.waitUntil(addResourcesToCache(__assets ?? []))
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
console.log('fetch happened', event.request)
|
||||
event.respondWith(
|
||||
cacheFirst({
|
||||
request: event.request,
|
||||
preloadResponsePromise: event.preloadResponse,
|
||||
fallbackUrl: './',
|
||||
})
|
||||
)
|
||||
})
|
|
@ -1,17 +0,0 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog')
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
props: post,
|
||||
}))
|
||||
}
|
||||
type Props = CollectionEntry<'blog'>
|
||||
|
||||
const post = Astro.props
|
||||
const { Content } = await post.render()
|
||||
---
|
||||
|
||||
<Content />
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
|
||||
---
|
||||
|
||||
blog index
|
||||
|
||||
<a href="/blog/building-a-cozy-web">post</a>
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
import globals from 'globals'
|
||||
import eslintPluginAstro from 'eslint-plugin-astro'
|
||||
import jsPlugin from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import astroSwGlobals from '@ayco/astro-sw/globals'
|
||||
import astroParser from 'astro-eslint-parser'
|
||||
|
||||
export default [
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...astroSwGlobals,
|
||||
},
|
||||
},
|
||||
},
|
||||
// add more generic rule sets here, such as:
|
||||
jsPlugin.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...eslintPluginAstro.configs['recommended'],
|
||||
...eslintPluginAstro.configs['jsx-a11y-recommended'],
|
||||
{
|
||||
ignores: [
|
||||
'**/dist/*',
|
||||
'**/.output/*',
|
||||
'**/.astro/*',
|
||||
'**/node_modules/*',
|
||||
'**/env.d.ts',
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['**/*.astro'],
|
||||
languageOptions: {
|
||||
parser: astroParser,
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
|
@ -2,4 +2,4 @@ export default {
|
|||
__prefix: false,
|
||||
__version: false,
|
||||
__assets: false,
|
||||
}
|
||||
};
|
62
package.json
62
package.json
|
@ -1,37 +1,43 @@
|
|||
{
|
||||
"name": "astro-sw-monorepo",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "> [!NOTE] > This project moved to [SourceHut](https://git.sr.ht/~ayoayco/astro-sw).",
|
||||
"scripts": {
|
||||
"format": "prettier . --write",
|
||||
"lint": "eslint . --config eslint.config.mjs --cache",
|
||||
"check": "npm run format && npm run lint",
|
||||
"prepare": "husky",
|
||||
"test": "pnpm -F @ayco/astro-sw test",
|
||||
"build": "pnpm -F @ayco/astro-sw build",
|
||||
"demo": "pnpm run build && pnpm -F demo build:preview"
|
||||
},
|
||||
"name": "@ayco/astro-sw",
|
||||
"version": "0.8.11",
|
||||
"description": "Use your own authored service worker with Astro",
|
||||
"homepage": "https://github.com/ayoayco/astro-sw",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@git.sr.ht/~ayoayco/astro-sw.git"
|
||||
"url": "https://github.com/ayoayco/astro-sw"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./astro-sw.js"
|
||||
}
|
||||
},
|
||||
"main": "./astro-sw.js",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"build:preview:static": "astro build && astro preview",
|
||||
"build:preview": "astro build && node ./server.mjs",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"withastro",
|
||||
"perf"
|
||||
],
|
||||
"author": "Ayo Ayco",
|
||||
"license": "MIT",
|
||||
"homepage": "https://ayco.io/n/@ayco/astro-sw#readme",
|
||||
"devDependencies": {
|
||||
"@ayco/astro-sw": "workspace:*",
|
||||
"@eslint/js": "^9.24.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-astro": "^1.3.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"globals": "^16.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.29.0",
|
||||
"vitest": "^3.1.1"
|
||||
"@astrojs/node": "^8.3.3",
|
||||
"@fastify/middie": "^8.3.1",
|
||||
"@fastify/static": "^7.0.4",
|
||||
"astro": "^4.15.2",
|
||||
"fastify": "^4.28.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"esbuild": "^0.23.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
{
|
||||
"name": "@ayco/astro-sw",
|
||||
"version": "0.9.0",
|
||||
"description": "Use your own authored service worker with Astro",
|
||||
"homepage": "https://ayco.io/n/@ayco/astro-sw",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.sr.ht/~ayoayco/astro-sw"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./globals": {
|
||||
"types": "./dist/eslint/globals.d.ts",
|
||||
"default": "./dist/eslint/globals.js"
|
||||
},
|
||||
"./presets": {
|
||||
"types": "./dist/presets/index.d.ts",
|
||||
"default": "./dist/presets/index.js"
|
||||
},
|
||||
"./presets/*": {
|
||||
"types": "./dist/presets/*/index.d.ts",
|
||||
"default": "./dist/presets/*/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./astro-sw.js",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts src/presets/index.ts src/presets/**/index.ts src/eslint/globals.ts --format esm --dts --clean",
|
||||
"test": "vitest run",
|
||||
"version:patch": "npm version patch",
|
||||
"version:minor": "npm version minor",
|
||||
"version:major": "npm version major"
|
||||
},
|
||||
"keywords": [
|
||||
"withastro",
|
||||
"perf"
|
||||
],
|
||||
"author": "Ayo Ayco",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"astro": "^5.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0"
|
||||
}
|
||||
}
|
|
@ -1,232 +0,0 @@
|
|||
/**
|
||||
* @license MIT <https://opensource.org/licenses/MIT>
|
||||
* @author Ayo Ayco <https://ayo.ayco.io>
|
||||
*/
|
||||
|
||||
import { readFile, writeFile, readdir, unlink } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { build } from 'esbuild'
|
||||
import type { AstroServiceWorkerConfig } from './types'
|
||||
import type { AstroIntegration } from 'astro'
|
||||
|
||||
const ASTROSW = '@ayco/astro-sw'
|
||||
|
||||
/**
|
||||
* TODO: update JSDoc
|
||||
* Accepts configuration options with service worker path
|
||||
* and injects needed variables such as `__assets` generated by Astro
|
||||
*/
|
||||
export default function serviceWorker(
|
||||
// TODO handle options undefined
|
||||
options?: AstroServiceWorkerConfig
|
||||
): AstroIntegration {
|
||||
const {
|
||||
presets,
|
||||
assetCachePrefix = ASTROSW,
|
||||
assetCacheVersionID = '0',
|
||||
path: serviceWorkerPath = undefined,
|
||||
customRoutes = [],
|
||||
excludeRoutes = [],
|
||||
logAssets = false,
|
||||
esbuild = {},
|
||||
registrationHooks = {},
|
||||
} = options ?? {}
|
||||
|
||||
const {
|
||||
installing: installingFn = () => {},
|
||||
waiting: waitingFn = () => {},
|
||||
active: activeFn = () => {},
|
||||
error: errorFn = () => {},
|
||||
unsupported: unsupportedFn = () => {},
|
||||
afterRegistration: afterRegistrationFn = () => {},
|
||||
} = registrationHooks
|
||||
|
||||
// TODO use presets
|
||||
console.log(presets)
|
||||
|
||||
/**
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
let manifestAssets: string[] = []
|
||||
|
||||
const registrationScript = `const registerSW = async () => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register("/sw.js", {
|
||||
scope: "/",
|
||||
});
|
||||
if (registration.installing) {
|
||||
(${installingFn.toString()})();
|
||||
} else if (registration.waiting) {
|
||||
(${waitingFn.toString()})();
|
||||
} else if (registration.active) {
|
||||
(${activeFn.toString()})();
|
||||
}
|
||||
|
||||
(${afterRegistrationFn.toString()})();
|
||||
} catch (error) {
|
||||
(${errorFn.toString()})(error);
|
||||
}
|
||||
} else {
|
||||
(${unsupportedFn.toString()})();
|
||||
}
|
||||
}
|
||||
|
||||
registerSW();`
|
||||
|
||||
// let output = 'static'
|
||||
const __dirname = resolve(dirname('.'))
|
||||
|
||||
return {
|
||||
name: ASTROSW,
|
||||
hooks: {
|
||||
'astro:config:setup': async ({ injectScript, command, logger }) => {
|
||||
if (!serviceWorkerPath || serviceWorkerPath === '') {
|
||||
// REQUIRED OPTION IS MISSING
|
||||
logger.error('Missing required path to service worker script')
|
||||
}
|
||||
// const transformedScript=await transform(registrationScript)
|
||||
|
||||
// output = _config.output
|
||||
if (command === 'build') {
|
||||
injectScript('page', registrationScript)
|
||||
}
|
||||
},
|
||||
'astro:config:done': async ({ injectTypes }) => {
|
||||
const injectedTypes = `
|
||||
declare const __assets: string[];
|
||||
declare const __version: string;
|
||||
declare const __prefix: string;`
|
||||
injectTypes({ filename: 'caching.d.ts', content: injectedTypes })
|
||||
},
|
||||
'astro:build:ssr': ({ manifest }) => {
|
||||
manifestAssets = manifest.assets
|
||||
},
|
||||
'astro:build:done': async ({
|
||||
dir,
|
||||
assets: astroAssets,
|
||||
pages,
|
||||
logger,
|
||||
}) => {
|
||||
const outfile = fileURLToPath(new URL('./sw.js', dir))
|
||||
const swPath =
|
||||
serviceWorkerPath && serviceWorkerPath !== ''
|
||||
? join(__dirname, serviceWorkerPath)
|
||||
: undefined
|
||||
let originalScript
|
||||
|
||||
const _publicFiles = (
|
||||
(await readdir(dir, { withFileTypes: true })) ?? []
|
||||
)
|
||||
.filter((dirent) => dirent.isFile())
|
||||
.map((dirent) => `/${dirent.name}`)
|
||||
|
||||
const _assets = Array.from(astroAssets.keys())
|
||||
.filter((key) => !key.includes('[...slug]'))
|
||||
.flatMap((key) => (key === '/' ? key : [key, `${key}/`]))
|
||||
|
||||
const _pages =
|
||||
pages
|
||||
.filter(({ pathname }) => pathname !== '')
|
||||
.map(({ pathname }) => `/${pathname}`) ?? []
|
||||
|
||||
const _pagesWithoutEndSlash =
|
||||
pages
|
||||
.filter(({ pathname }) => pathname !== '')
|
||||
.map(({ pathname }) => {
|
||||
const lastChar = pathname.slice(-1)
|
||||
const len = pathname.length
|
||||
return lastChar === '/'
|
||||
? `/${pathname.slice(0, len - 1)}`
|
||||
: `/${pathname}`
|
||||
})
|
||||
.filter((pathname) => pathname !== '') ?? []
|
||||
|
||||
const _excludeRoutes = [
|
||||
...excludeRoutes,
|
||||
...excludeRoutes.map((route) => `${route}/`),
|
||||
]
|
||||
|
||||
const __assets = [
|
||||
...new Set([
|
||||
...manifestAssets,
|
||||
..._assets,
|
||||
..._pages,
|
||||
..._pagesWithoutEndSlash,
|
||||
...customRoutes,
|
||||
..._publicFiles,
|
||||
]),
|
||||
].filter(
|
||||
(asset) =>
|
||||
!!asset &&
|
||||
asset !== '' &&
|
||||
!asset.includes('404') &&
|
||||
!asset.includes('index.html') &&
|
||||
!_excludeRoutes.includes(asset)
|
||||
)
|
||||
|
||||
if (logAssets) {
|
||||
logger.info(
|
||||
`${__assets.length} assets for caching: \n ▶ ${__assets.toString().replaceAll(',', '\n ▶ ')}\n`
|
||||
)
|
||||
} else {
|
||||
logger.info(`${__assets.length} assets for caching.`)
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Using service worker in path: ${swPath}`)
|
||||
// @ts-expect-error undefined error is caught via try-catch
|
||||
originalScript = await readFile(swPath)
|
||||
} catch (err: unknown) {
|
||||
logger.error(JSON.stringify(err))
|
||||
if (!swPath) {
|
||||
logger.error(`
|
||||
|
||||
[${ASTROSW}] ERR: The 'path' option is required!
|
||||
[${ASTROSW}] INFO: Please see service worker options in https://ayco.io/gh/astro-sw#readme
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
const assetsDeclaration = `const __assets = ${JSON.stringify(__assets)};\n`
|
||||
const versionDeclaration = `const __version = ${JSON.stringify(assetCacheVersionID)};\n`
|
||||
const prefixDeclaration = `const __prefix = ${JSON.stringify(assetCachePrefix)};\n`
|
||||
|
||||
const tempFile = `${swPath}.tmp.ts`
|
||||
|
||||
try {
|
||||
await writeFile(
|
||||
tempFile,
|
||||
assetsDeclaration +
|
||||
versionDeclaration +
|
||||
prefixDeclaration +
|
||||
originalScript,
|
||||
{ flag: 'w+' }
|
||||
)
|
||||
} catch (err) {
|
||||
logger.error(JSON.stringify(err))
|
||||
}
|
||||
|
||||
try {
|
||||
await build({
|
||||
bundle: true,
|
||||
...esbuild,
|
||||
outfile,
|
||||
platform: 'browser',
|
||||
entryPoints: [tempFile],
|
||||
})
|
||||
} catch (err) {
|
||||
logger.error(JSON.stringify(err))
|
||||
}
|
||||
|
||||
// remove temp file
|
||||
try {
|
||||
await unlink(tempFile)
|
||||
} catch (err) {
|
||||
logger.error(JSON.stringify(err))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { AstroServiceWorkerPreset } from '../../types'
|
||||
|
||||
export const activateFn: AstroServiceWorkerPreset['activate'] = async ({
|
||||
cacheName,
|
||||
}) => {
|
||||
const allowCacheNames = [cacheName]
|
||||
const allCaches = await caches.keys()
|
||||
allCaches.forEach((key) => {
|
||||
if (!allowCacheNames.includes(key)) {
|
||||
console.info('Deleting old cache', key)
|
||||
caches.delete(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default activateFn
|
|
@ -1,8 +0,0 @@
|
|||
import { AstroServiceWorkerPreset } from '../../types'
|
||||
import activate from './activate'
|
||||
|
||||
export const deleteOldCaches: () => AstroServiceWorkerPreset = () => ({
|
||||
activate,
|
||||
})
|
||||
|
||||
export default deleteOldCaches
|
|
@ -1,2 +0,0 @@
|
|||
export { staleWhileRevalidate } from './stale-while-revalidate'
|
||||
export { deleteOldCaches } from './delete-old-caches'
|
|
@ -1,93 +0,0 @@
|
|||
import { AstroServiceWorkerPreset } from '../../types'
|
||||
|
||||
export const fetchFn: AstroServiceWorkerPreset['fetch'] = ({
|
||||
event,
|
||||
cacheName,
|
||||
}) => {
|
||||
console.info('fetch happened', { data: event })
|
||||
|
||||
event.respondWith(
|
||||
cacheAndRevalidate(
|
||||
{
|
||||
request: event.request,
|
||||
fallbackUrl: './',
|
||||
},
|
||||
cacheName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default fetchFn
|
||||
|
||||
// @ts-expect-error TODO fix types
|
||||
const putInCache = async (request, response, cacheName) => {
|
||||
const cache = await caches.open(cacheName)
|
||||
|
||||
if (response.ok) {
|
||||
console.info('adding one response to cache...', request.url)
|
||||
|
||||
// if exists, replace
|
||||
cache.keys().then((keys) => {
|
||||
if (keys.includes(request)) {
|
||||
cache.delete(request)
|
||||
}
|
||||
})
|
||||
|
||||
cache.put(request, response)
|
||||
}
|
||||
}
|
||||
|
||||
const cacheAndRevalidate = async (
|
||||
// @ts-expect-error TODO fix types
|
||||
{ request, fallbackUrl },
|
||||
cacheName: string
|
||||
) => {
|
||||
const cache = await caches.open(cacheName)
|
||||
|
||||
// Try get the resource from the cache
|
||||
const responseFromCache = await cache.match(request)
|
||||
if (responseFromCache) {
|
||||
console.info('using cached response...', responseFromCache.url)
|
||||
// get network response for revalidation of cached assets
|
||||
fetch(request.clone())
|
||||
.then((responseFromNetwork) => {
|
||||
if (responseFromNetwork) {
|
||||
console.info('fetched updated resource...', responseFromNetwork.url)
|
||||
putInCache(request, responseFromNetwork.clone(), cacheName)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.info('failed to fetch updated resource', error)
|
||||
})
|
||||
|
||||
return responseFromCache
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to get the resource from the network for 5 seconds
|
||||
const responseFromNetwork = await fetch(request.clone())
|
||||
// response may be used only once
|
||||
// we need to save clone to put one copy in cache
|
||||
// and serve second one
|
||||
putInCache(request, responseFromNetwork.clone(), cacheName)
|
||||
console.info('using network response', responseFromNetwork.url)
|
||||
return responseFromNetwork
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
// Try the fallback
|
||||
const fallbackResponse = await cache.match(fallbackUrl)
|
||||
if (fallbackResponse) {
|
||||
console.info('using fallback cached response...', fallbackResponse.url)
|
||||
return fallbackResponse
|
||||
}
|
||||
|
||||
// when even the fallback response is not available,
|
||||
// there is nothing we can do, but we must always
|
||||
// return a Response object
|
||||
return new Response('Network error happened', {
|
||||
status: 408,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
/**
|
||||
* preset for stale-while-revalidate caching strategy
|
||||
*/
|
||||
|
||||
import { AstroServiceWorkerPreset } from '../../types'
|
||||
import install from './install'
|
||||
import fetch from './fetch'
|
||||
|
||||
export const staleWhileRevalidate: () => AstroServiceWorkerPreset = () => ({
|
||||
install,
|
||||
fetch,
|
||||
})
|
||||
|
||||
export default staleWhileRevalidate
|
|
@ -1,33 +0,0 @@
|
|||
import { AstroServiceWorkerPreset } from '../../types'
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope
|
||||
|
||||
export const installFn: AstroServiceWorkerPreset['install'] = ({
|
||||
event,
|
||||
routes,
|
||||
cacheName,
|
||||
}) => {
|
||||
console.info('installing service worker...')
|
||||
self.skipWaiting() // go straight to activate
|
||||
|
||||
event.waitUntil(addResourcesToCache(routes ?? [], cacheName))
|
||||
}
|
||||
|
||||
// @ts-expect-error TODO fix types
|
||||
const addResourcesToCache = async (resources, cacheName: string) => {
|
||||
const cache = await caches.open(cacheName)
|
||||
console.info('adding resources to cache...', resources)
|
||||
try {
|
||||
await cache.addAll(resources)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'failed to add resources to cache; make sure requests exists and that there are no duplicates',
|
||||
{
|
||||
resources,
|
||||
error,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default installFn
|
|
@ -1,38 +0,0 @@
|
|||
import type { BuildOptions } from 'esbuild'
|
||||
|
||||
export type AstroServiceWorkerPreset = {
|
||||
activate?: (options: { event: ExtendableEvent; cacheName: string }) => void
|
||||
install?: (options: {
|
||||
event: ExtendableEvent
|
||||
routes: string[]
|
||||
cacheName: string
|
||||
}) => void
|
||||
fetch?: (options: { event: FetchEvent; cacheName: string }) => void
|
||||
}
|
||||
|
||||
export type AstroServiceWorkerConfig = {
|
||||
path?: string
|
||||
presets?: AstroServiceWorkerPreset[]
|
||||
assetCachePrefix?: string
|
||||
assetCacheVersionID?: string
|
||||
customRoutes?: string[]
|
||||
excludeRoutes?: string[]
|
||||
logAssets?: true
|
||||
esbuild?: BuildOptions
|
||||
registrationHooks?: {
|
||||
installing?: () => void
|
||||
waiting?: () => void
|
||||
active?: () => void
|
||||
error?: (error: Error) => void
|
||||
unsupported?: () => void
|
||||
afterRegistration?: () => void
|
||||
}
|
||||
experimental?: {
|
||||
strategy?: {
|
||||
fetchFn: () => void
|
||||
installFn: () => void
|
||||
activateFn: () => void
|
||||
waitFn: () => void
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import { expect } from 'vitest'
|
||||
import { test } from 'vitest'
|
||||
|
||||
test('astro-sw', () => {
|
||||
expect(true).toBeTruthy()
|
||||
})
|
5607
pnpm-lock.yaml
5607
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,3 +0,0 @@
|
|||
packages:
|
||||
- "package"
|
||||
- "demo"
|
|
@ -1,21 +0,0 @@
|
|||
/**
|
||||
* @see https://prettier.io/docs/en/configuration.html
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
trailingComma: 'es5',
|
||||
tabWidth: 2,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
plugins: ['prettier-plugin-astro'],
|
||||
overrides: [
|
||||
{
|
||||
files: '*.astro',
|
||||
options: {
|
||||
parser: 'astro',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default config
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
125
sample-sw.js
125
sample-sw.js
|
@ -1,125 +0,0 @@
|
|||
/**
|
||||
* Note: @ayco/astro-sw integration injects variables `__prefix`, `__version`, & `__assets`
|
||||
* -- find usage in `astro.config.mjs` integrations
|
||||
* @see https://ayco.io/n/@ayco/astro-sw
|
||||
*/
|
||||
const cacheName = `${__prefix ?? 'app'}-v${__version ?? '000'}`
|
||||
|
||||
const cleanOldCaches = async () => {
|
||||
const allowCacheNames = ['cozy-reader', cacheName]
|
||||
const allCaches = await caches.keys()
|
||||
allCaches.forEach((key) => {
|
||||
if (!allowCacheNames.includes(key)) {
|
||||
console.info('Deleting old cache', key)
|
||||
caches.delete(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const addResourcesToCache = async (resources) => {
|
||||
const cache = await caches.open(cacheName)
|
||||
console.info('adding resources to cache...', resources)
|
||||
try {
|
||||
await cache.addAll(resources)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'failed to add resources to cache; make sure requests exists and that there are no duplicates',
|
||||
{
|
||||
resources,
|
||||
error,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const putInCache = async (request, response) => {
|
||||
const cache = await caches.open(cacheName)
|
||||
|
||||
if (response.ok) {
|
||||
console.info('adding one response to cache...', request.url)
|
||||
|
||||
// if exists, replace
|
||||
cache.keys().then((keys) => {
|
||||
if (keys.includes(request)) {
|
||||
cache.delete(request)
|
||||
}
|
||||
})
|
||||
|
||||
cache.put(request, response)
|
||||
}
|
||||
}
|
||||
|
||||
const cacheAndRevalidate = async ({ request, fallbackUrl }) => {
|
||||
const cache = await caches.open(cacheName)
|
||||
|
||||
// Try get the resource from the cache
|
||||
const responseFromCache = await cache.match(request)
|
||||
if (responseFromCache) {
|
||||
console.info('using cached response...', responseFromCache.url)
|
||||
// get network response for revalidation of cached assets
|
||||
fetch(request.clone())
|
||||
.then((responseFromNetwork) => {
|
||||
if (responseFromNetwork) {
|
||||
console.info('fetched updated resource...', responseFromNetwork.url)
|
||||
putInCache(request, responseFromNetwork.clone())
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.info('failed to fetch updated resource', error)
|
||||
})
|
||||
|
||||
return responseFromCache
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to get the resource from the network for 5 seconds
|
||||
const responseFromNetwork = await fetch(request.clone())
|
||||
// response may be used only once
|
||||
// we need to save clone to put one copy in cache
|
||||
// and serve second one
|
||||
putInCache(request, responseFromNetwork.clone())
|
||||
console.info('using network response', responseFromNetwork.url)
|
||||
return responseFromNetwork
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
// Try the fallback
|
||||
const fallbackResponse = await cache.match(fallbackUrl)
|
||||
if (fallbackResponse) {
|
||||
console.info('using fallback cached response...', fallbackResponse.url)
|
||||
return fallbackResponse
|
||||
}
|
||||
|
||||
// when even the fallback response is not available,
|
||||
// there is nothing we can do, but we must always
|
||||
// return a Response object
|
||||
return new Response('Network error happened', {
|
||||
status: 408,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.info('activating service worker...')
|
||||
cleanOldCaches()
|
||||
})
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
console.info('installing service worker...')
|
||||
self.skipWaiting() // go straight to activate
|
||||
|
||||
event.waitUntil(addResourcesToCache(__assets ?? []))
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
console.info('fetch happened', { data: event })
|
||||
|
||||
event.respondWith(
|
||||
cacheAndRevalidate({
|
||||
request: event.request,
|
||||
fallbackUrl: './',
|
||||
})
|
||||
)
|
||||
})
|
18
server.mjs
Executable file
18
server.mjs
Executable file
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import Fastify from 'fastify';
|
||||
import fastifyMiddie from '@fastify/middie';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
||||
|
||||
const app = Fastify({ logger: true });
|
||||
|
||||
await app
|
||||
.register(fastifyStatic, {
|
||||
root: fileURLToPath(new URL('./dist/client', import.meta.url)),
|
||||
})
|
||||
.register(fastifyMiddie);
|
||||
app.use(ssrHandler);
|
||||
|
||||
app.listen({ port: 4321 });
|
16
src/content/config.ts
Normal file
16
src/content/config.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const blog = defineCollection({
|
||||
type: 'content',
|
||||
// Type-check frontmatter using a schema
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
// Transform string to Date object
|
||||
pubDate: z.coerce.date(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
heroImage: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog };
|
0
demo/src/env.d.ts → src/env.d.ts
vendored
0
demo/src/env.d.ts → src/env.d.ts
vendored
94
src/example_sw.js
Normal file
94
src/example_sw.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
import { log } from "./utils";
|
||||
|
||||
/**
|
||||
* Note: @ayco/astro-sw integration injects variables `__prefix`, `__version`, & `__assets`
|
||||
* -- find usage in package readme; `astro.config.mjs` integrations
|
||||
* @see https://ayco.io/n/@ayco/astro-sw
|
||||
*/
|
||||
const cacheName = `${__prefix ?? 'app'}-v${__version ?? '000'}`
|
||||
const addResourcesToCache = async (resources) => {
|
||||
const cache = await caches.open(cacheName);
|
||||
console.log('adding resources to cache...', resources)
|
||||
await cache.addAll(resources);
|
||||
};
|
||||
|
||||
log('test log', {hello: 'world'});
|
||||
|
||||
const putInCache = async (request, response) => {
|
||||
const cache = await caches.open(cacheName);
|
||||
console.log('adding one response to cache...', request)
|
||||
await cache.put(request, response);
|
||||
};
|
||||
|
||||
const cacheFirst = async ({ request, preloadResponsePromise, fallbackUrl }) => {
|
||||
// First try to get the resource from the cache
|
||||
const responseFromCache = await caches.match(request);
|
||||
if (responseFromCache) {
|
||||
return responseFromCache;
|
||||
}
|
||||
|
||||
// Next try to use the preloaded response, if it's there
|
||||
// NOTE: Chrome throws errors regarding preloadResponse, see:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1420515
|
||||
// https://github.com/mdn/dom-examples/issues/145
|
||||
// To avoid those errors, remove or comment out this block of preloadResponse
|
||||
// code along with enableNavigationPreload() and the "activate" listener.
|
||||
const preloadResponse = await preloadResponsePromise;
|
||||
if (preloadResponse) {
|
||||
console.info('using preload response', preloadResponse);
|
||||
putInCache(request, preloadResponse.clone());
|
||||
return preloadResponse;
|
||||
}
|
||||
|
||||
// Next try to get the resource from the network
|
||||
try {
|
||||
const responseFromNetwork = await fetch(request.clone());
|
||||
// response may be used only once
|
||||
// we need to save clone to put one copy in cache
|
||||
// and serve second one
|
||||
putInCache(request, responseFromNetwork.clone());
|
||||
return responseFromNetwork;
|
||||
} catch (error) {
|
||||
const fallbackResponse = await caches.match(fallbackUrl);
|
||||
if (fallbackResponse) {
|
||||
return fallbackResponse;
|
||||
}
|
||||
// when even the fallback response is not available,
|
||||
// there is nothing we can do, but we must always
|
||||
// return a Response object
|
||||
return new Response('Network error happened', {
|
||||
status: 408,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const enableNavigationPreload = async () => {
|
||||
if (self.registration.navigationPreload) {
|
||||
// Enable navigation preloads!
|
||||
await self.registration.navigationPreload.enable();
|
||||
}
|
||||
};
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('activating...', event)
|
||||
event.waitUntil(enableNavigationPreload());
|
||||
});
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('installing...', event)
|
||||
event.waitUntil(
|
||||
addResourcesToCache(__assets ?? [])
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
console.log('fetch happened', event.request)
|
||||
event.respondWith(
|
||||
cacheFirst({
|
||||
request: event.request,
|
||||
preloadResponsePromise: event.preloadResponse,
|
||||
fallbackUrl: './',
|
||||
})
|
||||
);
|
||||
});
|
|
@ -1,5 +1,3 @@
|
|||
---
|
||||
|
||||
---
|
||||
|
||||
404
|
17
src/pages/blog/[...slug].astro
Normal file
17
src/pages/blog/[...slug].astro
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<'blog'>;
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
|
||||
<Content />
|
5
src/pages/blog/index.astro
Normal file
5
src/pages/blog/index.astro
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
---
|
||||
blog index
|
||||
|
||||
<a href="/blog/building-a-cozy-web">post</a>
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
|
||||
---
|
||||
|
||||
exclude
|
|
@ -1,14 +1,15 @@
|
|||
---
|
||||
export const prerender = false
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hello</title>
|
||||
</head>
|
||||
<body> Hello </body>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
||||
<!-- <Fragment set:html={content} /> -->
|
3
src/utils/index.ts
Normal file
3
src/utils/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function log(message: any, ...data) {
|
||||
console.log(message, ...data)
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"module": "ESNext" /* Specify what module code is generated. */,
|
||||
"moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"lib": ["WebWorker", "ES2021.String"]
|
||||
},
|
||||
"exclude": ["./dist/**/*"]
|
||||
}
|
Loading…
Reference in a new issue