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",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue