vite/rollup plugin to inject and transform manifest & service worker

This commit is contained in:
Bruno Windels 2021-10-06 20:34:16 +02:00
parent 3fe1c0cdc3
commit 216afd45cc
5 changed files with 170 additions and 4 deletions

View file

@ -0,0 +1,46 @@
const fs = require('fs/promises');
const path = require('path');
module.exports = function injectWebManifest(manifestFile) {
let root;
let manifestHref;
return {
name: "injectWebManifest",
apply: "build",
configResolved: config => {
root = config.root;
},
transformIndexHtml: {
transform(html) {
return [{
tag: "link",
attrs: {rel: "manifest", href: manifestHref},
injectTo: "head"
}];
},
},
generateBundle: async function() {
const absoluteManifestFile = path.resolve(root, manifestFile);
const manifestDir = path.dirname(absoluteManifestFile);
const json = await fs.readFile(absoluteManifestFile, {encoding: "utf8"});
const manifest = JSON.parse(json);
for (const icon of manifest.icons) {
const iconFileName = path.resolve(manifestDir, icon.src);
const imgData = await fs.readFile(iconFileName);
const ref = this.emitFile({
type: "asset",
name: path.basename(iconFileName),
source: imgData
});
icon.src = this.getFileName(ref);
}
const outputName = path.basename(absoluteManifestFile);
const manifestRef = this.emitFile({
type: "asset",
name: outputName,
source: JSON.stringify(manifest)
});
manifestHref = this.getFileName(manifestRef);
}
};
}

View file

@ -0,0 +1,110 @@
const fs = require('fs/promises');
const path = require('path');
const xxhash = require('xxhashjs');
function contentHash(str) {
var hasher = new xxhash.h32(0);
hasher.update(str);
return hasher.digest();
}
module.exports = function injectServiceWorker(swFile) {
let root;
let manifestHref;
return {
name: "injectServiceWorker",
apply: "build",
enforce: "post",
configResolved: config => {
root = config.root;
},
generateBundle: async function(_, bundle) {
const absoluteSwFile = path.resolve(root, swFile);
const packageManifest = path.resolve(path.join(__dirname, "../../package.json"));
const version = JSON.parse(await fs.readFile(packageManifest, "utf8")).version;
let swSource = await fs.readFile(absoluteSwFile, {encoding: "utf8"});
const assets = Object.values(bundle).filter(a => a.type === "asset");
const cachedFileNames = assets.map(o => o.fileName).filter(fileName => fileName !== "index.html");
const uncachedFileContentMap = {
"index.html": assets.find(o => o.fileName === "index.html").source,
"sw.js": swSource
};
const globalHash = getBuildHash(cachedFileNames, uncachedFileContentMap);
swSource = await buildServiceWorker(swSource, version, globalHash, assets);
const outputName = path.basename(absoluteSwFile);
this.emitFile({
type: "asset",
fileName: outputName,
source: swSource
});
}
};
}
function getBuildHash(cachedFileNames, uncachedFileContentMap) {
const unhashedHashes = Object.entries(uncachedFileContentMap).map(([fileName, content]) => {
return `${fileName}-${contentHash(Buffer.from(content))}`;
});
const globalHashAssets = cachedFileNames.concat(unhashedHashes);
globalHashAssets.sort();
return contentHash(globalHashAssets.join(",")).toString();
}
const NON_PRECACHED_JS = [
"hydrogen-legacy.js",
"olm_legacy.js",
// most environments don't need the worker
"main.js"
];
function isPreCached(asset) {
const {name, fileName} = asset;
return name.endsWith(".svg") ||
name.endsWith(".png") ||
name.endsWith(".css") ||
name.endsWith(".wasm") ||
name.endsWith(".html") ||
// the index and vendor chunks don't have an extension in `name`, so check extension on `fileName`
fileName.endsWith(".js") && !NON_PRECACHED_JS.includes(path.basename(name));
}
async function buildServiceWorker(swSource, version, globalHash, assets) {
const unhashedPreCachedAssets = [];
const hashedPreCachedAssets = [];
const hashedCachedOnRequestAssets = [];
for (const asset of assets) {
const {name: unresolved, fileName: resolved} = asset;
if (!unresolved || resolved === unresolved) {
unhashedPreCachedAssets.push(resolved);
} else if (isPreCached(asset)) {
hashedPreCachedAssets.push(resolved);
} else {
hashedCachedOnRequestAssets.push(resolved);
}
}
const replaceArrayInSource = (name, value) => {
const newSource = swSource.replace(`${name} = []`, `${name} = ${JSON.stringify(value)}`);
if (newSource === swSource) {
throw new Error(`${name} was not found in the service worker source`);
}
return newSource;
};
const replaceStringInSource = (name, value) => {
const newSource = swSource.replace(new RegExp(`${name}\\s=\\s"[^"]*"`), `${name} = ${JSON.stringify(value)}`);
if (newSource === swSource) {
throw new Error(`${name} was not found in the service worker source`);
}
return newSource;
};
// write service worker
swSource = swSource.replace(`"%%VERSION%%"`, `"${version}"`);
swSource = swSource.replace(`"%%GLOBAL_HASH%%"`, `"${globalHash}"`);
swSource = replaceArrayInSource("UNHASHED_PRECACHED_ASSETS", unhashedPreCachedAssets);
swSource = replaceArrayInSource("HASHED_PRECACHED_ASSETS", hashedPreCachedAssets);
swSource = replaceArrayInSource("HASHED_CACHED_ON_REQUEST_ASSETS", hashedCachedOnRequestAssets);
swSource = replaceStringInSource("NOTIFICATION_BADGE_ICON", assets.find(a => a.name === "icon.png").fileName);
return swSource;
}

View file

@ -5,8 +5,8 @@
"description": "Lightweight matrix client with legacy and mobile browser support", "description": "Lightweight matrix client with legacy and mobile browser support",
"start_url": "index.html", "start_url": "index.html",
"icons": [ "icons": [
{"src": "assets/icon.png", "sizes": "384x384", "type": "image/png"}, {"src": "icon.png", "sizes": "384x384", "type": "image/png"},
{"src": "assets/icon-maskable.png", "sizes": "384x384", "type": "image/png", "purpose": "maskable"} {"src": "icon-maskable.png", "sizes": "384x384", "type": "image/png", "purpose": "maskable"}
], ],
"theme_color": "#0DBD8B" "theme_color": "#0DBD8B"
} }

View file

@ -30,6 +30,7 @@
downloadSandbox: downloadSandboxPath, downloadSandbox: downloadSandboxPath,
defaultHomeServer: "matrix.org", defaultHomeServer: "matrix.org",
// NOTE: uncomment this if you want the service worker for local development // NOTE: uncomment this if you want the service worker for local development
// and adjust apply in the service-worker build plugin
// serviceWorker: "sw.js", // serviceWorker: "sw.js",
// NOTE: provide push config if you want push notifs for local development // NOTE: provide push config if you want push notifs for local development
// see assets/config.json for what the config looks like // see assets/config.json for what the config looks like

View file

@ -1,3 +1,6 @@
const injectWebManifest = require("./scripts/build-plugins/manifest");
const injectServiceWorker = require("./scripts/build-plugins/service-worker");
export default { export default {
public: false, public: false,
root: "src/platform/web", root: "src/platform/web",
@ -12,6 +15,12 @@ export default {
}, },
build: { build: {
outDir: "../../../target", outDir: "../../../target",
minify: false emptyOutDir: true,
} minify: true,
sourcemap: true
},
plugins: [
injectWebManifest("assets/manifest.json"),
injectServiceWorker("sw.js")
]
}; };