forked from mystiq/hydrogen-web
vite/rollup plugin to inject and transform manifest & service worker
This commit is contained in:
parent
3fe1c0cdc3
commit
216afd45cc
5 changed files with 170 additions and 4 deletions
46
scripts/build-plugins/manifest.js
Normal file
46
scripts/build-plugins/manifest.js
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
110
scripts/build-plugins/service-worker.js
Normal file
110
scripts/build-plugins/service-worker.js
Normal 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;
|
||||
}
|
|
@ -5,8 +5,8 @@
|
|||
"description": "Lightweight matrix client with legacy and mobile browser support",
|
||||
"start_url": "index.html",
|
||||
"icons": [
|
||||
{"src": "assets/icon.png", "sizes": "384x384", "type": "image/png"},
|
||||
{"src": "assets/icon-maskable.png", "sizes": "384x384", "type": "image/png", "purpose": "maskable"}
|
||||
{"src": "icon.png", "sizes": "384x384", "type": "image/png"},
|
||||
{"src": "icon-maskable.png", "sizes": "384x384", "type": "image/png", "purpose": "maskable"}
|
||||
],
|
||||
"theme_color": "#0DBD8B"
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
downloadSandbox: downloadSandboxPath,
|
||||
defaultHomeServer: "matrix.org",
|
||||
// NOTE: uncomment this if you want the service worker for local development
|
||||
// and adjust apply in the service-worker build plugin
|
||||
// serviceWorker: "sw.js",
|
||||
// NOTE: provide push config if you want push notifs for local development
|
||||
// see assets/config.json for what the config looks like
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
const injectWebManifest = require("./scripts/build-plugins/manifest");
|
||||
const injectServiceWorker = require("./scripts/build-plugins/service-worker");
|
||||
|
||||
export default {
|
||||
public: false,
|
||||
root: "src/platform/web",
|
||||
|
@ -12,6 +15,12 @@ export default {
|
|||
},
|
||||
build: {
|
||||
outDir: "../../../target",
|
||||
minify: false
|
||||
}
|
||||
emptyOutDir: true,
|
||||
minify: true,
|
||||
sourcemap: true
|
||||
},
|
||||
plugins: [
|
||||
injectWebManifest("assets/manifest.json"),
|
||||
injectServiceWorker("sw.js")
|
||||
]
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue