From 9c551a9e990ee7f95462aaab7a6cd5ffe5cccf32 Mon Sep 17 00:00:00 2001 From: Ayo Date: Sun, 5 Apr 2026 22:59:34 +0200 Subject: [PATCH] feat(demo): update demo to use Astro 6 --- demo/astro.config.mjs | 15 +- demo/package.json | 14 +- demo/public/components/web-component.js | 14 ++ demo/public/favicon.ico | Bin 0 -> 15406 bytes demo/src/content/blog/building-a-cozy-web.md | 66 ------ demo/src/content/config.ts | 16 -- demo/src/pages/index.astro | 3 +- demo/src/sw.ts | 138 +++++++---- package/src/index.ts | 232 ------------------- 9 files changed, 122 insertions(+), 376 deletions(-) create mode 100644 demo/public/components/web-component.js create mode 100644 demo/public/favicon.ico delete mode 100644 demo/src/content/blog/building-a-cozy-web.md delete mode 100644 demo/src/content/config.ts delete mode 100644 package/src/index.ts diff --git a/demo/astro.config.mjs b/demo/astro.config.mjs index 944d6fe..23dd886 100644 --- a/demo/astro.config.mjs +++ b/demo/astro.config.mjs @@ -3,7 +3,9 @@ import { defineConfig } from 'astro/config' import node from '@astrojs/node' import serviceWorker from '@ayco/astro-sw' -import { deleteOldCaches, staleWhileRevalidate } from '@ayco/astro-sw/presets' +// import { deleteOldCaches, staleWhileRevalidate } from '@ayco/astro-sw/presets' + +import * as pkg from './package.json' export default defineConfig({ output: 'static', @@ -14,12 +16,11 @@ export default defineConfig({ integrations: [ serviceWorker({ path: './src/sw.ts', - presets: [staleWhileRevalidate(), deleteOldCaches()], - customRoutes: [ - // '/threads' - ], - excludeRoutes: ['/exclude'], - assetCachePrefix: 'hey', + assetCachePrefix: 'AstroSWTest', + assetCacheVersionID: pkg.version, + // presets: [staleWhileRevalidate(), deleteOldCaches()], + exclude: ['/exclude'], + // include: ['/components/web-component.js'], logAssets: true, esbuild: { minify: true, diff --git a/demo/package.json b/demo/package.json index 68c82a6..f9ce244 100644 --- a/demo/package.json +++ b/demo/package.json @@ -1,7 +1,7 @@ { "name": "demo", "private": true, - "version": "1.0.0", + "version": "1.0.3", "main": "index.js", "scripts": { "start": "astro dev", @@ -14,11 +14,11 @@ "license": "MIT", "description": "", "devDependencies": { - "astro": "^5.6.1", - "@astrojs/node": "^9.1.3", - "@fastify/middie": "^9.0.3", - "@fastify/static": "^8.1.1", - "fastify": "^5.2.2", - "@ayco/astro-sw": "workspace:*" + "@astrojs/node": "^10.0.4", + "@ayco/astro-sw": "workspace:*", + "@fastify/middie": "^9.3.1", + "@fastify/static": "^9.0.0", + "astro": "^6.1.3", + "fastify": "^5.8.4" } } diff --git a/demo/public/components/web-component.js b/demo/public/components/web-component.js new file mode 100644 index 0000000..cb5d505 --- /dev/null +++ b/demo/public/components/web-component.js @@ -0,0 +1,14 @@ +function register(){ + if ('customElements' in window) + window.customElements.define('web-component', WebComponent) +} + +class WebComponent extends HTMLElement { + connectedCallback() { + console.log('hello') + } +} + +register() + +export default WebComponent \ No newline at end of file diff --git a/demo/public/favicon.ico b/demo/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3afdf7c8f7407ab878ebbac3a7bc80bd506b8cfd GIT binary patch literal 15406 zcmeHNX-HQ~6u+iswzvdZ5s8t$1Pd+5Ff1f6l>878;)Xm?<3(XnABsM-AO$5AS&;}C zK~_eT_94L~#g(8eNU(j=%F0&SY&-A%Zf`EvzjN>0|NSrZ-n$2enYlA(&Y5#(IdjgD zBp=CF3JHoq)Pc)eEdJ{e@Ieim?Xu>ePk3F!qcoLi~#t zFRJ}Vj~-3_{{CvcuC7jq|K!ONg6tU2+Wg_eheG`C-@j96Xy^!hGqPuc4-5=Ye}Dh5 zgm1qi$ntRsaLz=^DOpZ9pY-5&oV(4FErDMx0i5|aZrtFe_wV1+hYugPZH_p%ZrviB zUCzSm*RN^Jm@y;HJ9!_|nKNgoqoae{hIVb5HfZlj(1gb5RrePWgVj~_p1?%cT}=z~T}ON%<}iWMuked6Qe31hedMMXv2vCGQJ z)M4k$nZxaqnVG5fv0BjTnOfO`FTm%|pK0R6iCp^Iwr#UYR>gyywRBl|{`~n$ALvtl zetz6OCr_SK$FW*~-l)|z8GY!2pFVw}OP4NjX=CkQyLQbgSrrdwC*BNR z+#$R>*f(uHe*AcP_wJoKjw=Pcd9-7*^4YUzxo;V?W9_>OR;^mar5_$1uDkYfa&p{7 zo^ao=Ih)OQ?AYNp`t|koPSZbk?ws4`-@kvKu-?qh9cv$Yh`Rv$jQ94^<0mL6$fa}N zk#D1WfX72zT%612z9SEiC(ar($(^!yJ`0x z`<6;dN(}qzjg_05tGxZK*mm{i3|qsNEn9T$DYA;9ya8ilW9h+z2Q)Z1NU5o*rr(7O zA35w3$BrFy>pKbS;mDCAG;Z8DL!PEiol3pEy^8!ZGBOPNYI)X{Crz3}t*xzkqO|xko6kB*L3{-I??br7*GqN1V<`S1Eld9>L# z*aI6H8iug|JK>BOGYs*758E*Iz9%6+KVQhl?%lhGvC!S!O}HD_9QAV8mT}j363WZV zg*I34p^f{waN&ZkJ)F%2XV=c1J3Yy|GB)l;(LD?w4t#t7$P=+yA{_W);CJvez;6ye zCzBg5pFVv$-M)QWVFmvIHh-S4NQ;Yjq05&qdzyWE`t&I+Sg^p5fB26PR|UYHU%Pg# zaLr+V8^x-*YhuuiMY&%5 zU%YrhDJdyJ{^8SwY&;FnBM>7P5D=it6V4LopLl~ke*BmaN63>o`-XdWMXl9-riD1R+~qVnw7v+4Qs=U(F< zcM{GE5npE8)6&wEd%|lIkO|>@G|Qi;-@0|{yta3lj1Vh_H7?@KOgr9c-Uh^P>BZNW z;hFWpJqX{mw*hgCB6|ws(I`j!fhXTwdUC-#Y2LhfF0UWNBtR$m6#!eMk!;PhLAU!= saP{g{I&tCzRaI44O2EY!HrNudC16XymVhk*XC)xtkv7;8uq=VU0mOzq+5i9m literal 0 HcmV?d00001 diff --git a/demo/src/content/blog/building-a-cozy-web.md b/demo/src/content/blog/building-a-cozy-web.md deleted file mode 100644 index da57784..0000000 --- a/demo/src/content/blog/building-a-cozy-web.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Building a Cozy Web -description: Let us build the web we want! -pubDate: 'Aug 14 2024' -heroImage: '/cozy.jpg' ---- - -> This was originally posted on [Ayo's Blog](https://ayos.blog/building-a-cozy-web). - -Have you ever clicked a link to an article, all hyped up to read the content, only to be slapped in the face with popups over popups of requests to subscribe and asking consent to track you with cookies? - -Do you sometimes wish you can have a consistent experience when opening articles... a place to save all your favorites, and possibly get helpful insights? - -Ah, well you're not alone. 🤣 - -This is exactly why I started [**Cozy** 🧸](https://cozy.ayco.io/). - -It's a simple web page that can make any web page content-focused! 🎉 - -It uses a library called [@extractus/article-extractor](https://www.npmjs.com/package/@extractus/article-extractor) to fetch and extract just the content. - -Then with [Astro](https://astro.build), we can server-side render the page so your browser only gets clean HTML! - -No nonsense. No headaches. - -The project and the road map for features are all public on my [GitHub](https://github.com/ayoayco/cozy-reader) - -## Cozy Features - -Right now, it successfully extracts the content and delivers a clean page to your browser. - -I'm working toward bringing the following in the coming weeks: -1. Save favorites to a library -2. Offline access -3. Smart Insights about the article -4. Easier usage (browser extensions or apps?) - -## Coziest Usage - -The most convenient way to use it right now is through what we call a browser bookmarklet. - -Basically you can have a button there beside your other bookmarks that will open the current page in Cozy. - -You can create this new bookmark titled 'Get cozy!' and put the following as value for the URL: - -```js -javascript:(function(){ window.open('https://cozy.ayco.io/?url=%27 + window.location.href, %27_self%27); })(); -``` - -This is possible on all major browsers, including Safari on iOS (where I personally use this often). Some screenshots: - -| Firefox | Chrome | -| --- | --- | -| ![Screenshot from 2023-05-13 08-31-41](https://github.com/ayoayco/cozy-reader/assets/4262489/9b296d4f-2722-483a-bbc2-431c6b2ae996) | ![Screenshot from 2023-05-12 23-32-08](https://github.com/ayoayco/cozy-reader/assets/4262489/144b74f8-3949-46b9-849c-351e4af0ac12) | - -## Join the Project! - -I'm sure this looks very simple, but I think this is the most exciting hobby project I've started yet. - -There's a lot that happened and a lot of problems could have been avoided if people were equipped to assess the content they find online. - -I think there's lots of good a simple tool could bring if it allows users to cut-through all the distractions and are presented with unbiased and accurate information. - -This project is a groundwork for this experience. - -Let's build the web we want! 🧸 \ No newline at end of file diff --git a/demo/src/content/config.ts b/demo/src/content/config.ts deleted file mode 100644 index 986f2a2..0000000 --- a/demo/src/content/config.ts +++ /dev/null @@ -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 } diff --git a/demo/src/pages/index.astro b/demo/src/pages/index.astro index 81deb50..bceddab 100644 --- a/demo/src/pages/index.astro +++ b/demo/src/pages/index.astro @@ -8,7 +8,8 @@ export const prerender = false Hello + - Hello + Hello diff --git a/demo/src/sw.ts b/demo/src/sw.ts index e4effcb..028d25d 100644 --- a/demo/src/sw.ts +++ b/demo/src/sw.ts @@ -1,57 +1,105 @@ /** * Note: @ayco/astro-sw integration injects variables `__prefix`, `__version`, & `__assets` - * -- find usage in package readme; `astro.config.mjs` integrations + * -- find usage in `astro.config.mjs` integrations * @see https://ayco.io/n/@ayco/astro-sw */ const cacheName = `${__prefix ?? 'app'}-v${__version ?? '000'}` +const forceLogging = true + +/** + * Cleans up old service worker caches by deleting any cache that doesn't match the current cache name. + * This ensures only the current version of the application's cache is retained. + * @async + * @function cleanOldCaches + * @returns {Promise} A promise that resolves when old caches have been deleted + */ +const cleanOldCaches = async () => { + const allowCacheNames = [cacheName] + const allCaches = await caches.keys() + allCaches.forEach((key) => { + if (!allowCacheNames.includes(key)) { + console.info('Deleting old cache', key) + caches + .delete(key) + .then(() => { + console.info('Successfully deleted cache:', key) + }) + .catch((error) => { + console.warn('Failed to delete old cache:', key, error) + }) + } + }) +} + +/** + * Adds specified resources to the service worker cache. + * This function is used to cache static assets for offline access. + * @async + * @function addResourcesToCache + * @param {Array} resources - An array of URLs representing the resources to be cached. + * @returns {Promise} A promise that resolves when all resources have been successfully added to the cache. + */ const addResourcesToCache = async (resources) => { const cache = await caches.open(cacheName) - console.log('adding resources to cache...', resources) - await cache.addAll(resources) + console.info('adding resources to cache...', { + force: !!forceLogging, + context: 'ayco-sw', + data: 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', + error + ) + } } -console.log('test log', { hello: 'world' }) - +/** + * Puts a response in the cache. + * @async + * @function putInCache + * @param {Request} request - The request to be cached. + * @param {Response} response - The response to be cached. + * @returns {Promise} A promise that resolves when the response has been added to the cache. + */ const putInCache = async (request, response) => { const cache = await caches.open(cacheName) - console.log('adding one response to cache...', request) - await cache.put(request, response) + + if (response.ok) { + console.info('adding one response to cache...', request.url) + 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 - } +const networkFirst = async ({ request, fallbackUrl }) => { + const cache = await caches.open(cacheName) - // 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 { + // 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) { - const fallbackResponse = await caches.match(fallbackUrl) - if (fallbackResponse) { - return fallbackResponse + // Try get the resource from the cache + const responseFromCache = await cache.match(request) + if (responseFromCache) { + console.info('using cached response...', responseFromCache.url) + return responseFromCache } + + // If fallback is provided, try to use it, otherwise return error + if (fallbackUrl) { + 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 @@ -62,30 +110,26 @@ const cacheFirst = async ({ request, preloadResponsePromise, fallbackUrl }) => { } } -const enableNavigationPreload = async () => { - if (self.registration.navigationPreload) { - // Enable navigation preloads! - await self.registration.navigationPreload.enable() - } -} - +// eslint-disable-next-line @typescript-eslint/no-unused-vars self.addEventListener('activate', (event) => { - console.log('activating...', event) - event.waitUntil(enableNavigationPreload()) + console.info('activating service worker...') + cleanOldCaches() }) self.addEventListener('install', (event) => { - console.log('installing...', event) + console.info('installing service worker...') + self.skipWaiting() // go straight to activate + event.waitUntil(addResourcesToCache(__assets ?? [])) }) self.addEventListener('fetch', (event) => { - console.log('fetch happened', event.request) + console.info('fetch happened', { data: event }) + event.respondWith( - cacheFirst({ + networkFirst({ request: event.request, - preloadResponsePromise: event.preloadResponse, fallbackUrl: './', }) ) -}) +}) \ No newline at end of file diff --git a/package/src/index.ts b/package/src/index.ts deleted file mode 100644 index 36d765f..0000000 --- a/package/src/index.ts +++ /dev/null @@ -1,232 +0,0 @@ -/** - * @license MIT - * @author Ayo Ayco - */ - -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} - */ - 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)) - } - }, - }, - } -}