Skip to content

Instantly share code, notes, and snippets.

@thepassle
Last active April 23, 2024 08:17
Show Gist options
  • Save thepassle/be9010c330d7524133a4cfcf0a1c2ea1 to your computer and use it in GitHub Desktop.
Save thepassle/be9010c330d7524133a4cfcf0a1c2ea1 to your computer and use it in GitHub Desktop.
swsr.js
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