Last active
April 23, 2024 08:17
-
-
Save thepassle/be9010c330d7524133a4cfcf0a1c2ea1 to your computer and use it in GitHub Desktop.
swsr.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { defineConfig } from 'astro/config'; | |
import netlify from '@astrojs/netlify'; | |
import path from 'path'; | |
import fs from 'fs'; | |
import { injectManifest } from 'workbox-build'; | |
import { build } from "esbuild"; | |
import { createRequire } from 'module'; | |
const require = createRequire(import.meta.url); | |
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@'; | |
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g'); | |
const SW_FILE_NAME = 'sw.js'; | |
const SW_SCRIPT = ` | |
navigator.serviceWorker.register('/${SW_FILE_NAME}'); | |
let refreshing = false; | |
navigator.serviceWorker.addEventListener('controllerchange', () => { | |
if (refreshing) return; | |
window.location.reload(); | |
refreshing = true; | |
}); | |
`; | |
// @TODO: require.resolve('astro-service-worker/service-worker-integration/shim.js') | |
const shim = `${process.cwd()}/service-worker-integration/shim.js` | |
/** | |
* Adapter | |
*/ | |
function getAdapter(options) { | |
return { | |
name: "astro-swsr-adapter", | |
// @TODO: 'astro-service-worker/service-worker-integration/server.js' | |
serverEntrypoint: `${process.cwd()}/service-worker-integration/server.js`, | |
exports: ['start'], | |
args: {} | |
} | |
} | |
const virtualSwModuleId = 'astro-swsr-virtual-module-sw'; | |
const resolvedVirtualSwModuleId = '\0' + virtualSwModuleId; | |
const pagesVirtualModuleId = 'astro-swsr-virtual-module-pages'; | |
const resolvedPagesVirtualModuleId = '\0' + pagesVirtualModuleId; | |
function vitePluginPages(options) { | |
return { | |
name: 'vite-plugin-swsr-pages', | |
enforce: 'post', | |
options(opts) { | |
return { | |
...opts, | |
input: [...(opts.input ?? []), pagesVirtualModuleId] | |
} | |
}, | |
resolveId(id) { | |
if(id === pagesVirtualModuleId) { | |
return resolvedPagesVirtualModuleId; | |
} | |
}, | |
load(id) { | |
if (id === resolvedPagesVirtualModuleId) { | |
let importMap = ""; | |
let imports = []; | |
let i = 0; | |
for (const page of options.pages.values()) { | |
if(options.networkOnly.includes(page.route.pathname)) continue; | |
const variable = `_page${i}`; | |
imports.push(`import * as ${variable} from '${page.moduleSpecifier}';`); | |
importMap += `['${page.component}', ${variable}],`; | |
i++; | |
} | |
i = 0; | |
let rendererItems = ""; | |
for (const renderer of options.renderers) { | |
const variable = `_renderer${i}`; | |
imports.unshift(`import ${variable} from '${renderer.serverEntrypoint}';`); | |
rendererItems += `Object.assign(${JSON.stringify(renderer)}, { ssr: ${variable} }),`; | |
i++; | |
} | |
const def = `${imports.join("\n")} | |
export const pageMap = new Map([${importMap}]); | |
export const renderers = [${rendererItems}];`; | |
return def; | |
} | |
} | |
} | |
} | |
/** | |
* Vite plugin | |
*/ | |
function vitePluginSW(options) { | |
let customServiceWorkerCode = ''; | |
if('swSrc' in options) { | |
const userSwPath = path.join(process.cwd(), options.swSrc); | |
customServiceWorkerCode = fs.readFileSync(userSwPath, 'utf-8'); | |
} | |
return { | |
name: 'vite-plugin-swsr-sw', | |
enforce: 'post', | |
options(opts) { | |
return { | |
...opts, | |
input: [...(opts.input ?? []), virtualSwModuleId], | |
}; | |
}, | |
resolveId(id) { | |
if (id === virtualSwModuleId) { | |
return resolvedVirtualSwModuleId; | |
} | |
}, | |
load(id) { | |
const adapter = options.adapter; | |
if (id === resolvedVirtualSwModuleId) { | |
return `import { start } from '${adapter.serverEntrypoint}'; | |
import * as _main from '${pagesVirtualModuleId}'; | |
import { deserializeManifest as _deserializeManifest } from 'astro/app'; | |
${customServiceWorkerCode} | |
const _manifest = Object.assign(_deserializeManifest('${manifestReplace}'), { | |
pageMap: _main.pageMap, | |
renderers: _main.renderers | |
}); | |
const _args = ${adapter.args ? JSON.stringify(adapter.args) : '{}'}; | |
start(_manifest, _args); | |
`; | |
} | |
}, | |
generateBundle(_, bundle) { | |
for (const [_chunkName, chunk] of Object.entries(bundle)) { | |
if (chunk?.modules?.[resolvedVirtualSwModuleId]) { | |
chunk.fileName = SW_FILE_NAME; | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Astro Integration | |
*/ | |
function serviceWorker(options) { | |
let cfg, swInDir, manifest, renderers = []; | |
return { | |
name: 'astro-swsr-integration', | |
hooks: { | |
'astro:config:setup': ({ config, command, injectScript }) => { | |
renderers = config._ctx.renderers; | |
/** Add SW registration script */ | |
if(command === 'build') { | |
injectScript({stage: 'head-inline', content: SW_SCRIPT}); | |
} | |
}, | |
"astro:config:done": ({ config }) => { | |
cfg = config; | |
}, | |
'astro:build:setup': async ({ vite, pages }) => { | |
vite.plugins.push(vitePluginPages({ | |
pages, | |
renderers, | |
networkOnly: options.networkOnly, | |
})); | |
/** | |
* This plugin should be the last, even after vite-plugin-ssr, | |
* otherwise the main Integration (netlify, firebase, etc) function will be output as `entry2.mjs`, | |
* and then the created redirects might not work correctly (e.g. they'll point to `entry.mjs`, which is now the SW) | |
*/ | |
vite.plugins.push(vitePluginSW({ | |
swSrc: options?.swSrc, | |
adapter: getAdapter(), | |
})); | |
swInDir = vite.build.outDir; | |
}, | |
'astro:build:start': async ({ buildConfig }) => { | |
buildConfig.excludePages = options.networkOnly; | |
}, | |
'astro:build:ssr': (ssr) => { | |
manifest = ssr.manifest; | |
manifest.routes = manifest.routes.filter(({routeData}) => !options.networkOnly.includes(routeData.pathname)); | |
}, | |
"astro:build:done": async () => { | |
const globDirectory = cfg.outDir.pathname; | |
const swInPath = path.join(swInDir, SW_FILE_NAME); | |
const swInFile = fs.readFileSync(swInPath, 'utf-8'); | |
const swOutPath = cfg.outDir.pathname; | |
const swOutFile = path.join(swOutPath, SW_FILE_NAME); | |
/** Add precacheManifest via Workbox */ | |
await injectManifest({ | |
globDirectory, | |
swSrc: swInPath, | |
swDest: swInPath, | |
...(options?.workbox ?? {}) | |
}); | |
/** Add SSR Manifest */ | |
fs.writeFileSync( | |
swInPath, | |
swInFile.replace( | |
replaceExp, | |
() => JSON.stringify(manifest) | |
) | |
); | |
/** Bundle and build for the browser */ | |
await build({ | |
entryPoints: [swInPath], | |
outfile: swOutFile, | |
platform: 'browser', | |
bundle: true, | |
inject: [shim], | |
minify: options?.dev ?? true, | |
...(options?.esbuild ?? {}) | |
}); | |
fs.unlinkSync(swInPath); | |
} | |
} | |
} | |
} | |
// https://astro.build/config | |
export default defineConfig({ | |
adapter: netlify(), | |
integrations: [ | |
serviceWorker({ | |
/** Provide custom service worker logic */ | |
swSrc: 'user-sw.js', | |
/** | |
* @TODO discussion | |
* Excludes specific pages from the service worker bundle, and forces them to always go to the network | |
* This is useful for server-only specific code, for example database connections | |
*/ | |
networkOnly: ['/networkonly-astro'], | |
/** Configure workbox options */ | |
workbox: {}, | |
/** Configure esbuild options */ | |
esbuild: {}, | |
/** When set to true, enables minifcation for esbuild, defaults to true */ | |
dev: false | |
}), | |
] | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment