Merge branch 'master' into ts-conversion-matrix-net
|
@ -13,5 +13,13 @@ module.exports = {
|
|||
"no-empty": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-unused-vars": "warn"
|
||||
},
|
||||
"globals": {
|
||||
"DEFINE_VERSION": "readonly",
|
||||
"DEFINE_GLOBAL_HASH": "readonly",
|
||||
// only available in sw.js
|
||||
"DEFINE_UNHASHED_PRECACHED_ASSETS": "readonly",
|
||||
"DEFINE_HASHED_PRECACHED_ASSETS": "readonly",
|
||||
"DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS": "readonly"
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,3 +8,5 @@
|
|||
otherwise it becomes hard to remember what was a default/named export
|
||||
- should we return promises from storage mutation calls? probably not, as we don't await them anywhere. only read calls should return promises?
|
||||
- we don't anymore
|
||||
- don't use these features, as they are not widely enough supported.
|
||||
- [lookbehind in regular expressions](https://caniuse.com/js-regexp-lookbehind)
|
||||
|
|
|
@ -2,12 +2,6 @@ SDK:
|
|||
|
||||
- we need to compile src/lib.ts to javascript, with a d.ts file generated as well. We need to compile to javascript once for cjs and once of es modules. The package.json looks like this:
|
||||
|
||||
we don't need to bundle for the sdk case! we might need to do some transpilation to just plain ES6 (e.g. don't assume ?. and ??) we could use a browserslist query for this e.g. `node 14`. esbuild seems to support this as well, tldraw uses esbuild for their build.
|
||||
|
||||
one advantage of not bundling the files for the sdk is that you can still use import overrides in the consuming project build settings. is that an idiomatic way of doing things though?
|
||||
|
||||
|
||||
|
||||
```
|
||||
"main": "./dist/index.cjs",
|
||||
"exports": {
|
||||
|
@ -17,6 +11,13 @@ one advantage of not bundling the files for the sdk is that you can still use im
|
|||
"types": "dist/index.d.ts",
|
||||
```
|
||||
|
||||
we don't need to bundle for the sdk case! we might need to do some transpilation to just plain ES6 (e.g. don't assume ?. and ??) we could use a browserslist query for this e.g. `node 14`. esbuild seems to support this as well, tldraw uses esbuild for their build.
|
||||
|
||||
one advantage of not bundling the files for the sdk is that you can still use import overrides in the consuming project build settings. is that an idiomatic way of doing things though?
|
||||
|
||||
|
||||
|
||||
|
||||
this way we will support typescript, non-esm javascript and esm javascript using libhydrogen as an SDK
|
||||
|
||||
got this from https://medium.com/dazn-tech/publishing-npm-packages-as-native-es-modules-41ffbc0a9dea
|
||||
|
@ -95,3 +96,14 @@ we could put this file per build system, as ESM, in dist as well so you can incl
|
|||
- css files and any resource used therein
|
||||
- download-sandbox.html
|
||||
- a type declaration file (index.d.ts)
|
||||
|
||||
## Questions
|
||||
- can rollup not bundle the source tree and leave modules intact?
|
||||
- if we can use a function that creates a chunk per file to pass to manualChunks and disable chunk hashing we can probably do this. See https://rollupjs.org/guide/en/#outputmanualchunks
|
||||
|
||||
looks like we should be able to disable chunk name hashing with chunkFileNames https://rollupjs.org/guide/en/#outputoptions-object
|
||||
|
||||
|
||||
we should test this with a vite test config
|
||||
|
||||
we also need to compile down to ES6, both for the app and for the sdk
|
||||
|
|
32
package.json
|
@ -10,10 +10,9 @@
|
|||
"lint": "eslint --cache src/",
|
||||
"lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts",
|
||||
"lint-ci": "eslint src/",
|
||||
"test": "impunity --entry-point src/main.js src/platform/web/Platform.js --force-esm-dirs lib/ src/",
|
||||
"start": "snowpack dev --port 3000",
|
||||
"build": "node --experimental-modules scripts/build.mjs",
|
||||
"postinstall": "node ./scripts/post-install.js"
|
||||
"test": "impunity --entry-point src/platform/web/main.js src/platform/web/Platform.js --force-esm-dirs lib/ src/ --root-dir src/",
|
||||
"start": "vite --port 3000",
|
||||
"build": "vite build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -26,46 +25,29 @@
|
|||
},
|
||||
"homepage": "https://github.com/vector-im/hydrogen-web/#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.1",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@rollup/plugin-babel": "^5.1.0",
|
||||
"@rollup/plugin-multi-entry": "^4.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
||||
"@typescript-eslint/parser": "^4.29.2",
|
||||
"autoprefixer": "^10.2.6",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"commander": "^6.0.0",
|
||||
"core-js": "^3.6.5",
|
||||
"eslint": "^7.32.0",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"finalhandler": "^1.1.1",
|
||||
"impunity": "^1.0.9",
|
||||
"mdn-polyfills": "^5.20.0",
|
||||
"postcss": "^8.1.1",
|
||||
"postcss-css-variables": "^0.17.0",
|
||||
"postcss-flexbugs-fixes": "^4.2.1",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-url": "^8.0.0",
|
||||
"node-html-parser": "^4.0.0",
|
||||
"postcss-css-variables": "^0.18.0",
|
||||
"postcss-flexbugs-fixes": "^5.0.2",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rollup-plugin-cleanup": "^3.1.1",
|
||||
"serve-static": "^1.13.2",
|
||||
"snowpack": "^3.8.3",
|
||||
"typescript": "^4.3.5",
|
||||
"vite": "^2.6.14",
|
||||
"xxhashjs": "^0.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||
"@rollup/plugin-commonjs": "^15.0.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"aes-js": "^3.1.2",
|
||||
"another-json": "^0.2.0",
|
||||
"base64-arraybuffer": "^0.2.0",
|
||||
"bs58": "^4.0.1",
|
||||
"dompurify": "^2.3.0",
|
||||
"es6-promise": "https://github.com/bwindels/es6-promise.git#bwindels/expose-flush",
|
||||
"node-html-parser": "^4.0.0",
|
||||
"rollup": "^2.26.4",
|
||||
"text-encoding": "^0.7.0"
|
||||
}
|
||||
}
|
||||
|
|
51
scripts/build-plugins/manifest.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
const fs = require('fs/promises');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = function injectWebManifest(manifestFile) {
|
||||
let root;
|
||||
let base;
|
||||
let manifestHref;
|
||||
return {
|
||||
name: "hydrogen:injectWebManifest",
|
||||
apply: "build",
|
||||
configResolved: config => {
|
||||
root = config.root;
|
||||
base = config.base;
|
||||
},
|
||||
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
|
||||
});
|
||||
// we take the basename as getFileName gives the filename
|
||||
// relative to the output dir, but the manifest is an asset
|
||||
// just like they icon, so we assume they end up in the same dir
|
||||
icon.src = path.basename(this.getFileName(ref));
|
||||
}
|
||||
const outputName = path.basename(absoluteManifestFile);
|
||||
const manifestRef = this.emitFile({
|
||||
type: "asset",
|
||||
name: outputName,
|
||||
source: JSON.stringify(manifest)
|
||||
});
|
||||
manifestHref = base + this.getFileName(manifestRef);
|
||||
}
|
||||
};
|
||||
}
|
154
scripts/build-plugins/service-worker.js
Normal file
|
@ -0,0 +1,154 @@
|
|||
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();
|
||||
}
|
||||
|
||||
function injectServiceWorker(swFile, otherUnhashedFiles, placeholdersPerChunk) {
|
||||
const swName = path.basename(swFile);
|
||||
let root;
|
||||
let version;
|
||||
|
||||
return {
|
||||
name: "hydrogen:injectServiceWorker",
|
||||
apply: "build",
|
||||
enforce: "post",
|
||||
buildStart() {
|
||||
this.emitFile({
|
||||
type: "chunk",
|
||||
fileName: swName,
|
||||
id: swFile,
|
||||
});
|
||||
},
|
||||
configResolved: config => {
|
||||
root = config.root;
|
||||
version = JSON.parse(config.define.DEFINE_VERSION); // unquote
|
||||
},
|
||||
generateBundle: async function(options, bundle) {
|
||||
const unhashedFilenames = [swName].concat(otherUnhashedFiles);
|
||||
const unhashedFileContentMap = unhashedFilenames.reduce((map, fileName) => {
|
||||
const chunkOrAsset = bundle[fileName];
|
||||
if (!chunkOrAsset) {
|
||||
throw new Error("could not get content for uncached asset or chunk " + fileName);
|
||||
}
|
||||
map[fileName] = chunkOrAsset.source || chunkOrAsset.code;
|
||||
return map;
|
||||
}, {});
|
||||
const assets = Object.values(bundle);
|
||||
const hashedFileNames = assets.map(o => o.fileName).filter(fileName => !unhashedFileContentMap[fileName]);
|
||||
const globalHash = getBuildHash(hashedFileNames, unhashedFileContentMap);
|
||||
const placeholderValues = {
|
||||
DEFINE_GLOBAL_HASH: `"${globalHash}"`,
|
||||
...getCacheFileNamePlaceholderValues(swName, unhashedFilenames, assets, placeholdersPerChunk)
|
||||
};
|
||||
replacePlaceholdersInChunks(assets, placeholdersPerChunk, placeholderValues);
|
||||
console.log(`\nBuilt ${version} (${globalHash})`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getBuildHash(hashedFileNames, unhashedFileContentMap) {
|
||||
const unhashedHashes = Object.entries(unhashedFileContentMap).map(([fileName, content]) => {
|
||||
return `${fileName}-${contentHash(Buffer.from(content))}`;
|
||||
});
|
||||
const globalHashAssets = hashedFileNames.concat(unhashedHashes);
|
||||
globalHashAssets.sort();
|
||||
return contentHash(globalHashAssets.join(",")).toString();
|
||||
}
|
||||
|
||||
const NON_PRECACHED_JS = [
|
||||
"hydrogen-legacy",
|
||||
"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));
|
||||
}
|
||||
|
||||
function getCacheFileNamePlaceholderValues(swName, unhashedFilenames, assets) {
|
||||
const unhashedPreCachedAssets = [];
|
||||
const hashedPreCachedAssets = [];
|
||||
const hashedCachedOnRequestAssets = [];
|
||||
|
||||
for (const asset of assets) {
|
||||
const {name, fileName} = asset;
|
||||
// the service worker should not be cached at all,
|
||||
// it's how updates happen
|
||||
if (fileName === swName) {
|
||||
continue;
|
||||
} else if (unhashedFilenames.includes(fileName)) {
|
||||
unhashedPreCachedAssets.push(fileName);
|
||||
} else if (isPreCached(asset)) {
|
||||
hashedPreCachedAssets.push(fileName);
|
||||
} else {
|
||||
hashedCachedOnRequestAssets.push(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
DEFINE_UNHASHED_PRECACHED_ASSETS: JSON.stringify(unhashedPreCachedAssets),
|
||||
DEFINE_HASHED_PRECACHED_ASSETS: JSON.stringify(hashedPreCachedAssets),
|
||||
DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS: JSON.stringify(hashedCachedOnRequestAssets)
|
||||
}
|
||||
}
|
||||
|
||||
function replacePlaceholdersInChunks(assets, placeholdersPerChunk, placeholderValues) {
|
||||
for (const [name, placeholderMap] of Object.entries(placeholdersPerChunk)) {
|
||||
const chunk = assets.find(a => a.type === "chunk" && a.name === name);
|
||||
if (!chunk) {
|
||||
throw new Error(`could not find chunk ${name} to replace placeholders`);
|
||||
}
|
||||
for (const [placeholderName, placeholderLiteral] of Object.entries(placeholderMap)) {
|
||||
const replacedValue = placeholderValues[placeholderName];
|
||||
const oldCode = chunk.code;
|
||||
chunk.code = chunk.code.replaceAll(placeholderLiteral, replacedValue);
|
||||
if (chunk.code === oldCode) {
|
||||
throw new Error(`Could not replace ${placeholderName} in ${name}, looking for literal ${placeholderLiteral}:\n${chunk.code}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** creates a value to be include in the `define` build settings,
|
||||
* but can be replace at the end of the build in certain chunks.
|
||||
* We need this for injecting the global build hash and the final
|
||||
* filenames in the service worker and index chunk.
|
||||
* These values are only known in the generateBundle step, so we
|
||||
* replace them by unique strings wrapped in a prompt call so no
|
||||
* transformation will touch them (minifying, ...) and we can do a
|
||||
* string replacement still at the end of the build. */
|
||||
function definePlaceholderValue(mode, name, devValue) {
|
||||
if (mode === "production") {
|
||||
// note that `prompt(...)` will never be in the final output, it's replaced by the final value
|
||||
// once we know at the end of the build what it is and just used as a temporary value during the build
|
||||
// as something that will not be transformed.
|
||||
// I first considered Symbol but it's not inconceivable that babel would transform this.
|
||||
return `prompt(${JSON.stringify(name)})`;
|
||||
} else {
|
||||
return JSON.stringify(devValue);
|
||||
}
|
||||
}
|
||||
|
||||
function createPlaceholderValues(mode) {
|
||||
return {
|
||||
DEFINE_GLOBAL_HASH: definePlaceholderValue(mode, "DEFINE_GLOBAL_HASH", null),
|
||||
DEFINE_UNHASHED_PRECACHED_ASSETS: definePlaceholderValue(mode, "UNHASHED_PRECACHED_ASSETS", []),
|
||||
DEFINE_HASHED_PRECACHED_ASSETS: definePlaceholderValue(mode, "HASHED_PRECACHED_ASSETS", []),
|
||||
DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS: definePlaceholderValue(mode, "HASHED_CACHED_ON_REQUEST_ASSETS", []),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {injectServiceWorker, createPlaceholderValues};
|
|
@ -1,578 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {build as snowpackBuild, loadConfiguration} from "snowpack"
|
||||
import cheerio from "cheerio";
|
||||
import fsRoot from "fs";
|
||||
const fs = fsRoot.promises;
|
||||
import path from "path";
|
||||
import xxhash from 'xxhashjs';
|
||||
import { rollup } from 'rollup';
|
||||
import postcss from "postcss";
|
||||
import postcssImport from "postcss-import";
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import commander from "commander";
|
||||
// needed for legacy bundle
|
||||
import babel from '@rollup/plugin-babel';
|
||||
// needed to find the polyfill modules in the main-legacy.js bundle
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
// needed because some of the polyfills are written as commonjs modules
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
// multi-entry plugin so we can add polyfill file to main
|
||||
import multi from '@rollup/plugin-multi-entry';
|
||||
import removeJsComments from 'rollup-plugin-cleanup';
|
||||
// replace urls of asset names with content hashed version
|
||||
import postcssUrl from "postcss-url";
|
||||
|
||||
import cssvariables from "postcss-css-variables";
|
||||
import autoprefixer from "autoprefixer";
|
||||
import flexbugsFixes from "postcss-flexbugs-fixes";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const projectDir = path.join(__dirname, "../");
|
||||
const snowpackOutPath = path.join(projectDir, "snowpack-build-output");
|
||||
const cssSrcDir = path.join(projectDir, "src/platform/web/ui/css/");
|
||||
const snowpackConfig = await loadConfiguration({buildOptions: {out: snowpackOutPath}}, "snowpack.config.js");
|
||||
const snowpackOutDir = snowpackConfig.buildOptions.out.substring(projectDir.length);
|
||||
const srcDir = path.join(projectDir, `${snowpackOutDir}/src/`);
|
||||
const isPathInSrcDir = path => path.startsWith(srcDir);
|
||||
|
||||
const parameters = new commander.Command();
|
||||
parameters
|
||||
.option("--modern-only", "don't make a legacy build")
|
||||
.option("--override-imports <json file>", "pass in a file to override import paths, see doc/SKINNING.md")
|
||||
.option("--override-css <main css file>", "pass in an alternative main css file")
|
||||
parameters.parse(process.argv);
|
||||
|
||||
/**
|
||||
* We use Snowpack to handle the translation of TypeScript
|
||||
* into JavaScript. We thus can't bundle files straight from
|
||||
* the src directory, since some of them are TypeScript, and since
|
||||
* they may import Node modules. We thus bundle files after they
|
||||
* have been processed by Snowpack. This function returns paths
|
||||
* to the files that have already been pre-processed in this manner.
|
||||
*/
|
||||
function srcPath(src) {
|
||||
return path.join(snowpackOutDir, 'src', src);
|
||||
}
|
||||
|
||||
async function build({modernOnly, overrideImports, overrideCss}) {
|
||||
await snowpackBuild({config: snowpackConfig});
|
||||
// get version number
|
||||
const version = JSON.parse(await fs.readFile(path.join(projectDir, "package.json"), "utf8")).version;
|
||||
let importOverridesMap;
|
||||
if (overrideImports) {
|
||||
importOverridesMap = await readImportOverrides(overrideImports);
|
||||
}
|
||||
const devHtml = await fs.readFile(path.join(snowpackOutPath, "index.html"), "utf8");
|
||||
const doc = cheerio.load(devHtml);
|
||||
const themes = [];
|
||||
findThemes(doc, themeName => {
|
||||
themes.push(themeName);
|
||||
});
|
||||
// clear target dir
|
||||
const targetDir = path.join(projectDir, "target/");
|
||||
await removeDirIfExists(targetDir);
|
||||
await createDirs(targetDir, themes);
|
||||
const assets = new AssetMap(targetDir);
|
||||
// copy olm assets
|
||||
const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory);
|
||||
assets.addSubMap(olmAssets);
|
||||
await assets.write(`hydrogen.js`, await buildJs(srcPath("main.js"), [srcPath("platform/web/Platform.js")], importOverridesMap));
|
||||
if (!modernOnly) {
|
||||
await assets.write(`hydrogen-legacy.js`, await buildJsLegacy(srcPath("main.js"), [
|
||||
srcPath('platform/web/legacy-polyfill.js'),
|
||||
srcPath('platform/web/LegacyPlatform.js')
|
||||
], importOverridesMap));
|
||||
await assets.write(`worker.js`, await buildJsLegacy(srcPath("platform/web/worker/main.js"), [srcPath('platform/web/worker/polyfill.js')]));
|
||||
}
|
||||
// copy over non-theme assets
|
||||
const baseConfig = JSON.parse(await fs.readFile(path.join(projectDir, "assets/config.json"), {encoding: "utf8"}));
|
||||
const downloadSandbox = "download-sandbox.html";
|
||||
let downloadSandboxHtml = await fs.readFile(path.join(projectDir, `assets/${downloadSandbox}`));
|
||||
await assets.write(downloadSandbox, downloadSandboxHtml);
|
||||
// creates the directories where the theme css bundles are placed in,
|
||||
// and writes to assets, so the build bundles can translate them, so do it first
|
||||
await copyThemeAssets(themes, assets);
|
||||
await buildCssBundles(buildCssLegacy, themes, assets, overrideCss);
|
||||
await buildManifest(assets);
|
||||
// all assets have been added, create a hash from all assets name to cache unhashed files like index.html
|
||||
assets.addToHashForAll("index.html", devHtml);
|
||||
let swSource = await fs.readFile(path.join(snowpackOutPath, "sw.js"), "utf8");
|
||||
assets.addToHashForAll("sw.js", swSource);
|
||||
|
||||
const globalHash = assets.hashForAll();
|
||||
|
||||
await buildServiceWorker(swSource, version, globalHash, assets);
|
||||
await buildHtml(doc, version, baseConfig, globalHash, modernOnly, assets);
|
||||
await removeDirIfExists(snowpackOutPath);
|
||||
console.log(`built hydrogen ${version} (${globalHash}) successfully with ${assets.size} files`);
|
||||
}
|
||||
|
||||
async function findThemes(doc, callback) {
|
||||
doc("link[rel~=stylesheet][title]").each((i, el) => {
|
||||
const theme = doc(el);
|
||||
const href = theme.attr("href");
|
||||
const themesPrefix = "/themes/";
|
||||
const prefixIdx = href.indexOf(themesPrefix);
|
||||
if (prefixIdx !== -1) {
|
||||
const themeNameStart = prefixIdx + themesPrefix.length;
|
||||
const themeNameEnd = href.indexOf("/", themeNameStart);
|
||||
const themeName = href.substr(themeNameStart, themeNameEnd - themeNameStart);
|
||||
callback(themeName, theme);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function createDirs(targetDir, themes) {
|
||||
await fs.mkdir(targetDir);
|
||||
const themeDir = path.join(targetDir, "themes");
|
||||
await fs.mkdir(themeDir);
|
||||
for (const theme of themes) {
|
||||
await fs.mkdir(path.join(themeDir, theme));
|
||||
}
|
||||
}
|
||||
|
||||
async function copyThemeAssets(themes, assets) {
|
||||
for (const theme of themes) {
|
||||
const themeDstFolder = path.join(assets.directory, `themes/${theme}`);
|
||||
const themeSrcFolder = path.join(cssSrcDir, `themes/${theme}`);
|
||||
const themeAssets = await copyFolder(themeSrcFolder, themeDstFolder, file => {
|
||||
return !file.endsWith(".css");
|
||||
});
|
||||
assets.addSubMap(themeAssets);
|
||||
}
|
||||
return assets;
|
||||
}
|
||||
|
||||
async function buildHtml(doc, version, baseConfig, globalHash, modernOnly, assets) {
|
||||
// transform html file
|
||||
// change path to main.css to css bundle
|
||||
doc("link[rel=stylesheet]:not([title])").attr("href", assets.resolve(`hydrogen.css`));
|
||||
// adjust file name of icon on iOS
|
||||
doc("link[rel=apple-touch-icon]").attr("href", assets.resolve(`icon-maskable.png`));
|
||||
// change paths to all theme stylesheets
|
||||
findThemes(doc, (themeName, theme) => {
|
||||
theme.attr("href", assets.resolve(`themes/${themeName}/bundle.css`));
|
||||
});
|
||||
const configJSON = JSON.stringify(Object.assign({}, baseConfig, {
|
||||
worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null,
|
||||
downloadSandbox: assets.resolve("download-sandbox.html"),
|
||||
serviceWorker: "sw.js",
|
||||
olm: {
|
||||
wasm: assets.resolve("olm.wasm"),
|
||||
legacyBundle: assets.resolve("olm_legacy.js"),
|
||||
wasmBundle: assets.resolve("olm.js"),
|
||||
}
|
||||
}));
|
||||
const modernScript = `import {main, Platform} from "./${assets.resolve(`hydrogen.js`)}"; main(new Platform(document.body, ${configJSON}));`;
|
||||
const mainScripts = [
|
||||
`<script type="module">${wrapWithLicenseComments(modernScript)}</script>`
|
||||
];
|
||||
if (!modernOnly) {
|
||||
const legacyScript = `hydrogen.main(new hydrogen.Platform(document.body, ${configJSON}));`;
|
||||
mainScripts.push(
|
||||
`<script type="text/javascript" nomodule src="${assets.resolve(`hydrogen-legacy.js`)}"></script>`,
|
||||
`<script type="text/javascript" nomodule>${wrapWithLicenseComments(legacyScript)}</script>`
|
||||
);
|
||||
}
|
||||
doc("script#main").replaceWith(mainScripts.join(""));
|
||||
|
||||
const versionScript = doc("script#version");
|
||||
versionScript.attr("type", "text/javascript");
|
||||
let vSource = versionScript.contents().text();
|
||||
vSource = vSource.replace(`"%%VERSION%%"`, `"${version}"`);
|
||||
vSource = vSource.replace(`"%%GLOBAL_HASH%%"`, `"${globalHash}"`);
|
||||
versionScript.text(wrapWithLicenseComments(vSource));
|
||||
doc("head").append(`<link rel="manifest" href="${assets.resolve("manifest.json")}">`);
|
||||
await assets.writeUnhashed("index.html", doc.html());
|
||||
}
|
||||
|
||||
async function buildJs(mainFile, extraFiles, importOverrides) {
|
||||
// create js bundle
|
||||
const plugins = [multi(), removeJsComments({comments: "none"})];
|
||||
if (importOverrides) {
|
||||
plugins.push(overridesAsRollupPlugin(importOverrides));
|
||||
}
|
||||
const bundle = await rollup({
|
||||
// for fake-indexeddb, so usage for tests only doesn't put it in bundle
|
||||
treeshake: {moduleSideEffects: isPathInSrcDir},
|
||||
input: extraFiles.concat(mainFile),
|
||||
plugins
|
||||
});
|
||||
const {output} = await bundle.generate({
|
||||
format: 'es',
|
||||
// TODO: can remove this?
|
||||
name: `hydrogen`
|
||||
});
|
||||
const code = output[0].code;
|
||||
return wrapWithLicenseComments(code);
|
||||
}
|
||||
|
||||
async function buildJsLegacy(mainFile, extraFiles, importOverrides) {
|
||||
// compile down to whatever IE 11 needs
|
||||
const babelPlugin = babel.babel({
|
||||
babelHelpers: 'bundled',
|
||||
exclude: 'node_modules/**',
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: "entry",
|
||||
corejs: "3.4",
|
||||
targets: "IE 11",
|
||||
// we provide our own promise polyfill (es6-promise)
|
||||
// with support for synchronous flushing of
|
||||
// the queue for idb where needed
|
||||
exclude: ["es.promise", "es.promise.all-settled", "es.promise.finally"]
|
||||
}
|
||||
]
|
||||
]
|
||||
});
|
||||
const plugins = [multi(), commonjs()];
|
||||
if (importOverrides) {
|
||||
plugins.push(overridesAsRollupPlugin(importOverrides));
|
||||
}
|
||||
plugins.push(nodeResolve(), babelPlugin);
|
||||
// create js bundle
|
||||
const rollupConfig = {
|
||||
// for fake-indexeddb, so usage for tests only doesn't put it in bundle
|
||||
treeshake: {moduleSideEffects: isPathInSrcDir},
|
||||
// important the extraFiles come first,
|
||||
// so polyfills are available in the global scope
|
||||
// if needed for the mainfile
|
||||
input: extraFiles.concat(mainFile),
|
||||
plugins
|
||||
};
|
||||
const bundle = await rollup(rollupConfig);
|
||||
const {output} = await bundle.generate({
|
||||
format: 'iife',
|
||||
name: `hydrogen`
|
||||
});
|
||||
const code = output[0].code;
|
||||
return wrapWithLicenseComments(code);
|
||||
}
|
||||
|
||||
function wrapWithLicenseComments(code) {
|
||||
// Add proper license comments to make GNU LibreJS accept the file
|
||||
const start = '// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0';
|
||||
const end = '// @license-end';
|
||||
return `${start}\n${code}\n${end}`;
|
||||
}
|
||||
|
||||
const NON_PRECACHED_JS = [
|
||||
"hydrogen-legacy.js",
|
||||
"olm_legacy.js",
|
||||
"worker.js"
|
||||
];
|
||||
|
||||
function isPreCached(asset) {
|
||||
return asset.endsWith(".svg") ||
|
||||
asset.endsWith(".png") ||
|
||||
asset.endsWith(".css") ||
|
||||
asset.endsWith(".wasm") ||
|
||||
asset.endsWith(".html") ||
|
||||
// most environments don't need the worker
|
||||
asset.endsWith(".js") && !NON_PRECACHED_JS.includes(asset);
|
||||
}
|
||||
|
||||
async function buildManifest(assets) {
|
||||
const webManifest = JSON.parse(await fs.readFile(path.join(projectDir, "assets/manifest.json"), "utf8"));
|
||||
// copy manifest icons
|
||||
for (const icon of webManifest.icons) {
|
||||
let iconData = await fs.readFile(path.join(projectDir, icon.src));
|
||||
const iconTargetPath = path.basename(icon.src);
|
||||
icon.src = await assets.write(iconTargetPath, iconData);
|
||||
}
|
||||
await assets.write("manifest.json", JSON.stringify(webManifest));
|
||||
}
|
||||
|
||||
async function buildServiceWorker(swSource, version, globalHash, assets) {
|
||||
const unhashedPreCachedAssets = ["index.html"];
|
||||
const hashedPreCachedAssets = [];
|
||||
const hashedCachedOnRequestAssets = [];
|
||||
|
||||
for (const [unresolved, resolved] of assets) {
|
||||
if (unresolved === resolved) {
|
||||
unhashedPreCachedAssets.push(resolved);
|
||||
} else if (isPreCached(unresolved)) {
|
||||
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.resolve("icon.png"));
|
||||
|
||||
// service worker should not have a hashed name as it is polled by the browser for updates
|
||||
await assets.writeUnhashed("sw.js", swSource);
|
||||
}
|
||||
|
||||
async function buildCssBundles(buildFn, themes, assets, mainCssFile = null) {
|
||||
if (!mainCssFile) {
|
||||
mainCssFile = path.join(cssSrcDir, "main.css");
|
||||
}
|
||||
const bundleCss = await buildFn(mainCssFile);
|
||||
await assets.write(`hydrogen.css`, bundleCss);
|
||||
for (const theme of themes) {
|
||||
const themeRelPath = `themes/${theme}/`;
|
||||
const themeRoot = path.join(cssSrcDir, themeRelPath);
|
||||
const assetUrlMapper = ({absolutePath}) => {
|
||||
if (!absolutePath.startsWith(themeRoot)) {
|
||||
throw new Error("resource is out of theme directory: " + absolutePath);
|
||||
}
|
||||
const relPath = absolutePath.substr(themeRoot.length);
|
||||
const hashedDstPath = assets.resolve(path.join(themeRelPath, relPath));
|
||||
if (hashedDstPath) {
|
||||
return hashedDstPath.substr(themeRelPath.length);
|
||||
}
|
||||
};
|
||||
const themeCss = await buildFn(path.join(themeRoot, `theme.css`), assetUrlMapper);
|
||||
await assets.write(path.join(themeRelPath, `bundle.css`), themeCss);
|
||||
}
|
||||
}
|
||||
|
||||
// async function buildCss(entryPath, urlMapper = null) {
|
||||
// const preCss = await fs.readFile(entryPath, "utf8");
|
||||
// const options = [postcssImport];
|
||||
// if (urlMapper) {
|
||||
// options.push(postcssUrl({url: urlMapper}));
|
||||
// }
|
||||
// const cssBundler = postcss(options);
|
||||
// const result = await cssBundler.process(preCss, {from: entryPath});
|
||||
// return result.css;
|
||||
// }
|
||||
|
||||
async function buildCssLegacy(entryPath, urlMapper = null) {
|
||||
const preCss = await fs.readFile(entryPath, "utf8");
|
||||
const options = [
|
||||
postcssImport,
|
||||
cssvariables({
|
||||
preserve: (declaration) => {
|
||||
return declaration.value.indexOf("var(--ios-") == 0;
|
||||
}
|
||||
}),
|
||||
autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}),
|
||||
flexbugsFixes()
|
||||
];
|
||||
if (urlMapper) {
|
||||
options.push(postcssUrl({url: urlMapper}));
|
||||
}
|
||||
const cssBundler = postcss(options);
|
||||
const result = await cssBundler.process(preCss, {from: entryPath});
|
||||
return result.css;
|
||||
}
|
||||
|
||||
async function removeDirIfExists(targetDir) {
|
||||
try {
|
||||
await fs.rmdir(targetDir, {recursive: true});
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function copyFolder(srcRoot, dstRoot, filter, assets = null) {
|
||||
assets = assets || new AssetMap(dstRoot);
|
||||
const dirEnts = await fs.readdir(srcRoot, {withFileTypes: true});
|
||||
for (const dirEnt of dirEnts) {
|
||||
const dstPath = path.join(dstRoot, dirEnt.name);
|
||||
const srcPath = path.join(srcRoot, dirEnt.name);
|
||||
if (dirEnt.isDirectory()) {
|
||||
await fs.mkdir(dstPath);
|
||||
await copyFolder(srcPath, dstPath, filter, assets);
|
||||
} else if ((dirEnt.isFile() || dirEnt.isSymbolicLink()) && (!filter || filter(srcPath))) {
|
||||
const content = await fs.readFile(srcPath);
|
||||
await assets.write(dstPath, content);
|
||||
}
|
||||
}
|
||||
return assets;
|
||||
}
|
||||
|
||||
function contentHash(str) {
|
||||
var hasher = new xxhash.h32(0);
|
||||
hasher.update(str);
|
||||
return hasher.digest();
|
||||
}
|
||||
|
||||
class AssetMap {
|
||||
constructor(targetDir) {
|
||||
// remove last / if any, so substr in create works well
|
||||
this._targetDir = path.resolve(targetDir);
|
||||
this._assets = new Map();
|
||||
// hashes for unhashed resources so changes in these resources also contribute to the hashForAll
|
||||
this._unhashedHashes = [];
|
||||
}
|
||||
|
||||
_toRelPath(resourcePath) {
|
||||
let relPath = resourcePath;
|
||||
if (path.isAbsolute(resourcePath)) {
|
||||
if (!resourcePath.startsWith(this._targetDir)) {
|
||||
throw new Error(`absolute path ${resourcePath} that is not within target dir ${this._targetDir}`);
|
||||
}
|
||||
relPath = resourcePath.substr(this._targetDir.length + 1); // + 1 for the /
|
||||
}
|
||||
return relPath;
|
||||
}
|
||||
|
||||
_create(resourcePath, content) {
|
||||
const relPath = this._toRelPath(resourcePath);
|
||||
const hash = contentHash(Buffer.from(content));
|
||||
const dir = path.dirname(relPath);
|
||||
const extname = path.extname(relPath);
|
||||
const basename = path.basename(relPath, extname);
|
||||
const dstRelPath = path.join(dir, `${basename}-${hash}${extname}`);
|
||||
this._assets.set(relPath, dstRelPath);
|
||||
return dstRelPath;
|
||||
}
|
||||
|
||||
async write(resourcePath, content) {
|
||||
const relPath = this._create(resourcePath, content);
|
||||
const fullPath = path.join(this.directory, relPath);
|
||||
if (typeof content === "string") {
|
||||
await fs.writeFile(fullPath, content, "utf8");
|
||||
} else {
|
||||
await fs.writeFile(fullPath, content);
|
||||
}
|
||||
return relPath;
|
||||
}
|
||||
|
||||
async writeUnhashed(resourcePath, content) {
|
||||
const relPath = this._toRelPath(resourcePath);
|
||||
this._assets.set(relPath, relPath);
|
||||
const fullPath = path.join(this.directory, relPath);
|
||||
if (typeof content === "string") {
|
||||
await fs.writeFile(fullPath, content, "utf8");
|
||||
} else {
|
||||
await fs.writeFile(fullPath, content);
|
||||
}
|
||||
return relPath;
|
||||
}
|
||||
|
||||
get directory() {
|
||||
return this._targetDir;
|
||||
}
|
||||
|
||||
resolve(resourcePath) {
|
||||
const relPath = this._toRelPath(resourcePath);
|
||||
const result = this._assets.get(relPath);
|
||||
if (!result) {
|
||||
throw new Error(`unknown path: ${relPath}, only know ${Array.from(this._assets.keys()).join(", ")}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
addSubMap(assetMap) {
|
||||
if (!assetMap.directory.startsWith(this.directory)) {
|
||||
throw new Error(`map directory doesn't start with this directory: ${assetMap.directory} ${this.directory}`);
|
||||
}
|
||||
const relSubRoot = assetMap.directory.substr(this.directory.length + 1);
|
||||
for (const [key, value] of assetMap._assets.entries()) {
|
||||
this._assets.set(path.join(relSubRoot, key), path.join(relSubRoot, value));
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this._assets.entries();
|
||||
}
|
||||
|
||||
isUnhashed(relPath) {
|
||||
const resolvedPath = this._assets.get(relPath);
|
||||
if (!resolvedPath) {
|
||||
throw new Error("Unknown asset: " + relPath);
|
||||
}
|
||||
return relPath === resolvedPath;
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._assets.size;
|
||||
}
|
||||
|
||||
has(relPath) {
|
||||
return this._assets.has(relPath);
|
||||
}
|
||||
|
||||
hashForAll() {
|
||||
const globalHashAssets = Array.from(this).map(([, resolved]) => resolved);
|
||||
globalHashAssets.push(...this._unhashedHashes);
|
||||
globalHashAssets.sort();
|
||||
return contentHash(globalHashAssets.join(","));
|
||||
}
|
||||
|
||||
addToHashForAll(resourcePath, content) {
|
||||
this._unhashedHashes.push(`${resourcePath}-${contentHash(Buffer.from(content))}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function readImportOverrides(filename) {
|
||||
const json = await fs.readFile(filename, "utf8");
|
||||
const mapping = new Map(Object.entries(JSON.parse(json)));
|
||||
return {
|
||||
basedir: path.dirname(path.resolve(filename))+path.sep,
|
||||
mapping
|
||||
};
|
||||
}
|
||||
|
||||
function overridesAsRollupPlugin(importOverrides) {
|
||||
const {mapping, basedir} = importOverrides;
|
||||
return {
|
||||
name: "rewrite-imports",
|
||||
resolveId (source, importer) {
|
||||
let file;
|
||||
if (source.startsWith(path.sep)) {
|
||||
file = source;
|
||||
} else {
|
||||
file = path.join(path.dirname(importer), source);
|
||||
}
|
||||
if (file.startsWith(basedir)) {
|
||||
const searchPath = file.substr(basedir.length);
|
||||
const replacingPath = mapping.get(searchPath);
|
||||
if (replacingPath) {
|
||||
console.info(`replacing ${searchPath} with ${replacingPath}`);
|
||||
return path.join(basedir, replacingPath);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
build(parameters).catch(err => console.error(err));
|
|
@ -1,12 +0,0 @@
|
|||
import fsRoot from "fs";
|
||||
const fs = fsRoot.promises;
|
||||
|
||||
export async function removeDirIfExists(targetDir) {
|
||||
try {
|
||||
await fs.rmdir(targetDir, {recursive: true});
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
git checkout gh-pages
|
||||
cp -R target/* .
|
||||
git add $(find . -maxdepth 1 -type f)
|
||||
git add themes
|
||||
git commit -m "update hydrogen"
|
||||
git checkout master
|
|
@ -1,6 +1,7 @@
|
|||
module.exports = class Buffer {
|
||||
static isBuffer(array) {return array instanceof Uint8Array;}
|
||||
static from(arrayBuffer) {return arrayBuffer;}
|
||||
static allocUnsafe(size) {return Buffer.alloc(size);}
|
||||
static alloc(size) {return new Uint8Array(size);}
|
||||
var Buffer = {
|
||||
isBuffer: function(array) {return array instanceof Uint8Array;},
|
||||
from: function(arrayBuffer) {return arrayBuffer;},
|
||||
allocUnsafe: function(size) {return Buffer.alloc(size);},
|
||||
alloc: function(size) {return new Uint8Array(size);}
|
||||
};
|
||||
export default Buffer;
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
// we have our own main file for this module as we need both these symbols to
|
||||
// be exported, and we also don't want to auto behaviour that modifies global vars
|
||||
exports.FDBFactory = require("fake-indexeddb/lib/FDBFactory.js");
|
||||
exports.FDBKeyRange = require("fake-indexeddb/lib/FDBKeyRange.js");
|
|
@ -1 +1,2 @@
|
|||
module.exports.Buffer = require("buffer");
|
||||
import Buffer from "buffer";
|
||||
export {Buffer};
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const fsRoot = require("fs");
|
||||
const fs = fsRoot.promises;
|
||||
const path = require("path");
|
||||
const { rollup } = require('rollup');
|
||||
const { fileURLToPath } = require('url');
|
||||
const { dirname } = require('path');
|
||||
// needed to translate commonjs modules to esm
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const json = require('@rollup/plugin-json');
|
||||
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
||||
|
||||
const projectDir = path.join(__dirname, "../");
|
||||
|
||||
async function removeDirIfExists(targetDir) {
|
||||
try {
|
||||
await fs.rmdir(targetDir, {recursive: true});
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** function used to resolve common-js require calls below. */
|
||||
function packageIterator(request, start, defaultIterator) {
|
||||
// this is just working for bs58, would need to tune it further for other dependencies
|
||||
if (request === "safe-buffer") {
|
||||
return [path.join(projectDir, "/scripts/package-overrides/safe-buffer")];
|
||||
} else if (request === "buffer/") {
|
||||
return [path.join(projectDir, "/scripts/package-overrides/buffer")];
|
||||
} else {
|
||||
return defaultIterator();
|
||||
}
|
||||
}
|
||||
|
||||
async function commonjsToESM(src, dst) {
|
||||
// create js bundle
|
||||
const bundle = await rollup({
|
||||
treeshake: {moduleSideEffects: false},
|
||||
input: src,
|
||||
plugins: [commonjs(), json(), nodeResolve({
|
||||
browser: true,
|
||||
preferBuiltins: false,
|
||||
customResolveOptions: {packageIterator}
|
||||
})]
|
||||
});
|
||||
const {output} = await bundle.generate({
|
||||
format: 'es'
|
||||
});
|
||||
const code = output[0].code;
|
||||
await fs.writeFile(dst, code, "utf8");
|
||||
}
|
||||
|
||||
async function populateLib() {
|
||||
const libDir = path.join(projectDir, "lib/");
|
||||
await removeDirIfExists(libDir);
|
||||
await fs.mkdir(libDir);
|
||||
const olmSrcDir = path.dirname(require.resolve("@matrix-org/olm"));
|
||||
const olmDstDir = path.join(libDir, "olm/");
|
||||
await fs.mkdir(olmDstDir);
|
||||
for (const file of ["olm.js", "olm.wasm", "olm_legacy.js"]) {
|
||||
await fs.copyFile(path.join(olmSrcDir, file), path.join(olmDstDir, file));
|
||||
}
|
||||
// transpile node-html-parser to esm
|
||||
await fs.mkdir(path.join(libDir, "node-html-parser/"));
|
||||
await commonjsToESM(
|
||||
require.resolve('node-html-parser/dist/index.js'),
|
||||
path.join(libDir, "node-html-parser/index.js")
|
||||
);
|
||||
// transpile another-json to esm
|
||||
await fs.mkdir(path.join(libDir, "another-json/"));
|
||||
await commonjsToESM(
|
||||
require.resolve('another-json/another-json.js'),
|
||||
path.join(libDir, "another-json/index.js")
|
||||
);
|
||||
// transpile bs58 to esm
|
||||
await fs.mkdir(path.join(libDir, "bs58/"));
|
||||
await commonjsToESM(
|
||||
require.resolve('bs58/index.js'),
|
||||
path.join(libDir, "bs58/index.js")
|
||||
);
|
||||
// transpile base64-arraybuffer to esm
|
||||
await fs.mkdir(path.join(libDir, "base64-arraybuffer/"));
|
||||
await commonjsToESM(
|
||||
require.resolve('base64-arraybuffer/lib/base64-arraybuffer.js'),
|
||||
path.join(libDir, "base64-arraybuffer/index.js")
|
||||
);
|
||||
// this probably should no go in here, we can just import "aes-js" from legacy-extras.js
|
||||
// as that file is never loaded from a browser
|
||||
|
||||
// transpile aesjs to esm
|
||||
await fs.mkdir(path.join(libDir, "aes-js/"));
|
||||
await commonjsToESM(
|
||||
require.resolve('aes-js/index.js'),
|
||||
path.join(libDir, "aes-js/index.js")
|
||||
);
|
||||
// es6-promise is already written as an es module,
|
||||
// but it does need to be babelified, and current we don't babelify
|
||||
// anything in node_modules in the build script, so make a bundle that
|
||||
// is conveniently not placed in node_modules rather than symlinking.
|
||||
await fs.mkdir(path.join(libDir, "es6-promise/"));
|
||||
await commonjsToESM(
|
||||
require.resolve('es6-promise/lib/es6-promise/promise.js'),
|
||||
path.join(libDir, "es6-promise/index.js")
|
||||
);
|
||||
// fake-indexeddb, used for tests (but unresolvable bare imports also makes the build complain)
|
||||
// and might want to use it for in-memory storage too, although we probably do ts->es6 with esm
|
||||
// directly rather than ts->es5->es6 as we do now. The bundle is 240K currently.
|
||||
await fs.mkdir(path.join(libDir, "fake-indexeddb/"));
|
||||
await commonjsToESM(
|
||||
path.join(projectDir, "/scripts/package-overrides/fake-indexeddb.js"),
|
||||
path.join(libDir, "fake-indexeddb/index.js")
|
||||
);
|
||||
}
|
||||
|
||||
populateLib();
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const finalhandler = require('finalhandler')
|
||||
const http = require('http')
|
||||
const serveStatic = require('serve-static')
|
||||
const path = require('path');
|
||||
|
||||
// Serve up parent directory with cache disabled
|
||||
const serve = serveStatic(
|
||||
path.resolve(__dirname, "../"),
|
||||
{
|
||||
etag: false,
|
||||
setHeaders: res => {
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Expires", "Wed, 21 Oct 2015 07:28:00 GMT");
|
||||
},
|
||||
index: ['index.html', 'index.htm']
|
||||
}
|
||||
);
|
||||
|
||||
// Create server
|
||||
const server = http.createServer(function onRequest (req, res) {
|
||||
console.log(req.method, req.url);
|
||||
serve(req, res, finalhandler(req, res))
|
||||
});
|
||||
|
||||
// Listen
|
||||
server.listen(3000);
|
|
@ -1,37 +0,0 @@
|
|||
// Snowpack Configuration File
|
||||
// See all supported options: https://www.snowpack.dev/reference/configuration
|
||||
|
||||
/** @type {import("snowpack").SnowpackUserConfig } */
|
||||
module.exports = {
|
||||
mount: {
|
||||
// More specific paths before less specific paths (if they overlap)
|
||||
"src/platform/web/docroot": "/",
|
||||
"src": "/src",
|
||||
"lib": {url: "/lib", static: true },
|
||||
"assets": "/assets",
|
||||
/* ... */
|
||||
},
|
||||
exclude: [
|
||||
/* Avoid scanning scripts which use dev-dependencies and pull in babel, rollup, etc. */
|
||||
'**/node_modules/**/*',
|
||||
'**/scripts/**',
|
||||
'**/target/**',
|
||||
'**/prototypes/**',
|
||||
'**/src/platform/web/legacy-polyfill.js',
|
||||
'**/src/platform/web/worker/polyfill.js'
|
||||
],
|
||||
plugins: [
|
||||
/* ... */
|
||||
],
|
||||
packageOptions: {
|
||||
/* ... */
|
||||
},
|
||||
devOptions: {
|
||||
open: "none",
|
||||
hmr: false,
|
||||
/* ... */
|
||||
},
|
||||
buildOptions: {
|
||||
/* ... */
|
||||
},
|
||||
};
|
|
@ -352,9 +352,13 @@ export function parseHTMLBody(platform, mediaRepository, allowReplies, html) {
|
|||
return new MessageBody(html, parts);
|
||||
}
|
||||
|
||||
import parse from '../../../../../lib/node-html-parser/index.js';
|
||||
|
||||
export function tests() {
|
||||
export async function tests() {
|
||||
// don't import node-html-parser until it's safe to assume we're actually in a unit test,
|
||||
// as this is a devDependency
|
||||
const nodeHtmlParser = await import("node-html-parser");
|
||||
const {parse} = nodeHtmlParser.default;
|
||||
|
||||
class HTMLParseResult {
|
||||
constructor(bodyNode) {
|
||||
this._bodyNode = bodyNode;
|
||||
|
|
|
@ -99,13 +99,13 @@ export class LogItem implements ILogItem {
|
|||
|
||||
/**
|
||||
* Creates a new child item that finishes immediately
|
||||
* and can hence not be modified anymore.
|
||||
*
|
||||
* Hence, the child item is not returned.
|
||||
* Finished items should not be modified anymore as they can be serialized
|
||||
* at any stage, but using `set` on the return value in a synchronous way should still be safe.
|
||||
*/
|
||||
log(labelOrValues: LabelOrValues, logLevel?: LogLevel): void {
|
||||
log(labelOrValues: LabelOrValues, logLevel?: LogLevel): ILogItem {
|
||||
const item = this.child(labelOrValues, logLevel);
|
||||
item.end = item.start;
|
||||
return item;
|
||||
}
|
||||
|
||||
set(key: string | object, value?: unknown): void {
|
||||
|
|
|
@ -64,7 +64,9 @@ export class NullLogItem implements ILogItem {
|
|||
return callback(this);
|
||||
}
|
||||
|
||||
log(): void {}
|
||||
log(): ILogItem {
|
||||
return this;
|
||||
}
|
||||
set(): void {}
|
||||
|
||||
runDetached(_: LabelOrValues, callback: LogCallback<unknown>): ILogItem {
|
||||
|
|
|
@ -42,7 +42,7 @@ export interface ILogItem {
|
|||
readonly start?: number;
|
||||
readonly values: LogItemValues;
|
||||
wrap<T>(labelOrValues: LabelOrValues, callback: LogCallback<T>, logLevel?: LogLevel, filterCreator?: FilterCreator): T;
|
||||
log(labelOrValues: LabelOrValues, logLevel?: LogLevel): void;
|
||||
log(labelOrValues: LabelOrValues, logLevel?: LogLevel): ILogItem;
|
||||
set(key: string | object, value: unknown): void;
|
||||
runDetached(labelOrValues: LabelOrValues, callback: LogCallback<unknown>, logLevel?: LogLevel, filterCreator?: FilterCreator): ILogItem;
|
||||
wrapDetached(labelOrValues: LabelOrValues, callback: LogCallback<unknown>, logLevel?: LogLevel, filterCreator?: FilterCreator): void;
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "../../../lib/another-json/index.js";
|
||||
import anotherjson from "another-json";
|
||||
import {SESSION_E2EE_KEY_PREFIX, OLM_ALGORITHM, MEGOLM_ALGORITHM} from "./common.js";
|
||||
|
||||
// use common prefix so it's easy to clear properties that are not e2ee related during session clear
|
||||
|
|
|
@ -264,7 +264,7 @@ export class DeviceTracker {
|
|||
return false;
|
||||
}
|
||||
curve25519Keys.add(curve25519Key);
|
||||
const isValid = this._hasValidSignature(deviceKeys);
|
||||
const isValid = this._hasValidSignature(deviceKeys, parentLog);
|
||||
if (!isValid) {
|
||||
parentLog.log({
|
||||
l: "ignore device with invalid signature",
|
||||
|
@ -279,11 +279,11 @@ export class DeviceTracker {
|
|||
return verifiedKeys;
|
||||
}
|
||||
|
||||
_hasValidSignature(deviceSection) {
|
||||
_hasValidSignature(deviceSection, parentLog) {
|
||||
const deviceId = deviceSection["device_id"];
|
||||
const userId = deviceSection["user_id"];
|
||||
const ed25519Key = deviceSection?.keys?.[`${SIGNATURE_ALGORITHM}:${deviceId}`];
|
||||
return verifyEd25519Signature(this._olmUtil, userId, deviceId, ed25519Key, deviceSection);
|
||||
return verifyEd25519Signature(this._olmUtil, userId, deviceId, ed25519Key, deviceSection, parentLog);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "../../../lib/another-json/index.js";
|
||||
import anotherjson from "another-json";
|
||||
import {createEnum} from "../../utils/enum";
|
||||
|
||||
export const DecryptionSource = createEnum("Sync", "Timeline", "Retry");
|
||||
|
@ -35,7 +35,7 @@ export class DecryptionError extends Error {
|
|||
|
||||
export const SIGNATURE_ALGORITHM = "ed25519";
|
||||
|
||||
export function verifyEd25519Signature(olmUtil, userId, deviceOrKeyId, ed25519Key, value) {
|
||||
export function verifyEd25519Signature(olmUtil, userId, deviceOrKeyId, ed25519Key, value, log = undefined) {
|
||||
const clone = Object.assign({}, value);
|
||||
delete clone.unsigned;
|
||||
delete clone.signatures;
|
||||
|
@ -49,7 +49,11 @@ export function verifyEd25519Signature(olmUtil, userId, deviceOrKeyId, ed25519Ke
|
|||
olmUtil.ed25519_verify(ed25519Key, canonicalJson, signature);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn("Invalid signature, ignoring.", ed25519Key, canonicalJson, signature, err);
|
||||
if (log) {
|
||||
const logItem = log.log({l: "Invalid signature, ignoring.", ed25519Key, canonicalJson, signature});
|
||||
logItem.error = err;
|
||||
logItem.logLevel = log.level.Warn;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,10 +189,10 @@ export class Encryption {
|
|||
log.log({l: "failures", servers: Object.keys(claimResponse.failures)}, log.level.Warn);
|
||||
}
|
||||
const userKeyMap = claimResponse?.["one_time_keys"];
|
||||
return this._verifyAndCreateOTKTargets(userKeyMap, devicesByUser);
|
||||
return this._verifyAndCreateOTKTargets(userKeyMap, devicesByUser, log);
|
||||
}
|
||||
|
||||
_verifyAndCreateOTKTargets(userKeyMap, devicesByUser) {
|
||||
_verifyAndCreateOTKTargets(userKeyMap, devicesByUser, log) {
|
||||
const verifiedEncryptionTargets = [];
|
||||
for (const [userId, userSection] of Object.entries(userKeyMap)) {
|
||||
for (const [deviceId, deviceSection] of Object.entries(userSection)) {
|
||||
|
@ -202,7 +202,7 @@ export class Encryption {
|
|||
const device = devicesByUser.get(userId)?.get(deviceId);
|
||||
if (device) {
|
||||
const isValidSignature = verifyEd25519Signature(
|
||||
this._olmUtil, userId, deviceId, device.ed25519Key, keySection);
|
||||
this._olmUtil, userId, deviceId, device.ed25519Key, keySection, log);
|
||||
if (isValidSignature) {
|
||||
const target = EncryptionTarget.fromOTK(device, keySection.key);
|
||||
verifiedEncryptionTargets.push(target);
|
||||
|
|
|
@ -269,13 +269,18 @@ export class QueryTarget<T> {
|
|||
}
|
||||
}
|
||||
|
||||
import {createMockDatabase, MockIDBImpl} from "../../../mocks/Storage";
|
||||
import {createMockDatabase, createMockIDBFactory, getMockIDBKeyRange} from "../../../mocks/Storage";
|
||||
import {txnAsPromise} from "./utils";
|
||||
import {QueryTargetWrapper, Store} from "./Store";
|
||||
|
||||
export function tests() {
|
||||
export async function tests() {
|
||||
|
||||
class MockTransaction extends MockIDBImpl {
|
||||
class MockTransaction {
|
||||
constructor(public readonly idbFactory: IDBFactory, readonly idbKeyRangeType: typeof IDBKeyRange) {}
|
||||
|
||||
get IDBKeyRange(): typeof IDBKeyRange {
|
||||
return this.idbKeyRangeType;
|
||||
}
|
||||
get databaseName(): string { return "mockdb"; }
|
||||
addWriteError(error: StorageError, refItem: ILogItem | undefined, operationName: string, keys: IDBKey[] | undefined) {}
|
||||
}
|
||||
|
@ -285,10 +290,12 @@ export function tests() {
|
|||
}
|
||||
|
||||
async function createTestStore(): Promise<Store<TestEntry>> {
|
||||
const mockImpl = new MockTransaction();
|
||||
const idbFactory = await createMockIDBFactory();
|
||||
const idbKeyRangeType = await getMockIDBKeyRange();
|
||||
const mockImpl = new MockTransaction(idbFactory, idbKeyRangeType);
|
||||
const db = await createMockDatabase("findExistingKeys", (db: IDBDatabase) => {
|
||||
db.createObjectStore("test", {keyPath: "key"});
|
||||
}, mockImpl);
|
||||
}, idbFactory);
|
||||
const txn = db.transaction(["test"], "readwrite");
|
||||
return new Store<TestEntry>(txn.objectStore("test"), mockImpl);
|
||||
}
|
||||
|
|
|
@ -14,31 +14,35 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {FDBFactory, FDBKeyRange} from "../../lib/fake-indexeddb/index.js";
|
||||
import {StorageFactory} from "../matrix/storage/idb/StorageFactory";
|
||||
import {IDOMStorage} from "../matrix/storage/idb/types";
|
||||
import {Storage} from "../matrix/storage/idb/Storage";
|
||||
import {Instance as nullLogger} from "../logging/NullLogger";
|
||||
import {openDatabase, CreateObjectStore} from "../matrix/storage/idb/utils";
|
||||
|
||||
export function createMockStorage(): Promise<Storage> {
|
||||
return new StorageFactory(null as any, new FDBFactory(), FDBKeyRange, new MockLocalStorage()).create("1", nullLogger.item);
|
||||
export async function createMockStorage(): Promise<Storage> {
|
||||
const idbFactory = await createMockIDBFactory();
|
||||
const FDBKeyRange = await getMockIDBKeyRange();
|
||||
return new StorageFactory(null as any, idbFactory, FDBKeyRange, new MockLocalStorage()).create("1", nullLogger.item);
|
||||
}
|
||||
|
||||
export function createMockDatabase(name: string, createObjectStore: CreateObjectStore, impl: MockIDBImpl): Promise<IDBDatabase> {
|
||||
return openDatabase(name, createObjectStore, 1, impl.idbFactory);
|
||||
// don't import fake-indexeddb until it's safe to assume we're actually in a unit test,
|
||||
// as this is a devDependency
|
||||
export async function createMockIDBFactory(): Promise<IDBFactory> {
|
||||
// @ts-ignore
|
||||
const FDBFactory = (await import("fake-indexeddb/lib/FDBFactory.js")).default;
|
||||
return new FDBFactory();
|
||||
}
|
||||
|
||||
export class MockIDBImpl {
|
||||
idbFactory: FDBFactory;
|
||||
// don't import fake-indexeddb until it's safe to assume we're actually in a unit test,
|
||||
// as this is a devDependency
|
||||
export async function getMockIDBKeyRange(): Promise<typeof IDBKeyRange> {
|
||||
// @ts-ignore
|
||||
return (await import("fake-indexeddb/lib/FDBKeyRange.js")).default;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.idbFactory = new FDBFactory();
|
||||
}
|
||||
|
||||
get IDBKeyRange(): typeof IDBKeyRange {
|
||||
return FDBKeyRange;
|
||||
}
|
||||
export function createMockDatabase(name: string, createObjectStore: CreateObjectStore, idbFactory: IDBFactory): Promise<IDBDatabase> {
|
||||
return openDatabase(name, createObjectStore, 1, idbFactory);
|
||||
}
|
||||
|
||||
class MockLocalStorage implements IDOMStorage {
|
||||
|
|
|
@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import aesjs from "../../../lib/aes-js/index.js";
|
||||
import aesjs from "aes-js";
|
||||
import {hkdf} from "../../utils/crypto/hkdf";
|
||||
|
||||
import {Platform as ModernPlatform} from "./Platform.js";
|
||||
|
||||
export function Platform(container, paths) {
|
||||
|
|
|
@ -67,21 +67,22 @@ async function loadOlm(olmPaths) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// make path relative to basePath,
|
||||
// assuming it and basePath are relative to document
|
||||
function relPath(path, basePath) {
|
||||
const idx = basePath.lastIndexOf("/");
|
||||
const dir = idx === -1 ? "" : basePath.slice(0, idx);
|
||||
const dirCount = dir.length ? dir.split("/").length : 0;
|
||||
return "../".repeat(dirCount) + path;
|
||||
// turn asset path to absolute path if it isn't already
|
||||
// so it can be loaded independent of base
|
||||
function assetAbsPath(assetPath) {
|
||||
if (!assetPath.startsWith("/")) {
|
||||
return new URL(assetPath, document.location.href).pathname;
|
||||
}
|
||||
return assetPath;
|
||||
}
|
||||
|
||||
async function loadOlmWorker(config) {
|
||||
const workerPool = new WorkerPool(config.worker, 4);
|
||||
await workerPool.init();
|
||||
const path = relPath(config.olm.legacyBundle, config.worker);
|
||||
await workerPool.sendAll({type: "load_olm", path});
|
||||
await workerPool.sendAll({
|
||||
type: "load_olm",
|
||||
path: assetAbsPath(config.olm.legacyBundle)
|
||||
});
|
||||
const olmWorker = new OlmWorker(workerPool);
|
||||
return olmWorker;
|
||||
}
|
||||
|
@ -162,7 +163,7 @@ export class Platform {
|
|||
// Make sure that loginToken does not end up in the logs
|
||||
const transformer = (item) => {
|
||||
if (item.e?.stack) {
|
||||
item.e.stack = item.e.stack.replace(/(?<=\/\?loginToken=).+/, "<snip>");
|
||||
item.e.stack = item.e.stack.replace(/\/\?loginToken=(.+)/, "?loginToken=<snip>");
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
@ -276,7 +277,7 @@ export class Platform {
|
|||
}
|
||||
|
||||
get version() {
|
||||
return window.HYDROGEN_VERSION;
|
||||
return DEFINE_VERSION;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 968 B |
|
@ -3,10 +3,10 @@
|
|||
"short_name": "Hydrogen",
|
||||
"display": "standalone",
|
||||
"description": "Lightweight matrix client with legacy and mobile browser support",
|
||||
"start_url": "index.html",
|
||||
"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"
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hydrogen Chat</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta name="application-name" content="Hydrogen Chat"/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Hydrogen Chat">
|
||||
<meta name="description" content="A matrix chat application">
|
||||
<link rel="apple-touch-icon" href="assets/icon-maskable.png">
|
||||
<link rel="icon" type="image/png" href="assets/icon-maskable.png">
|
||||
<link rel="stylesheet" type="text/css" href="src/platform/web/ui/css/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="src/platform/web/ui/css/themes/element/theme.css" title="Element Theme">
|
||||
<link rel="alternate stylesheet" type="text/css" href="src/platform/web/ui/css/themes/bubbles/theme.css" title="Bubbles Theme">
|
||||
</head>
|
||||
<body class="hydrogen">
|
||||
<script id="version" type="disabled">
|
||||
window.HYDROGEN_VERSION = "%%VERSION%%";
|
||||
window.HYDROGEN_GLOBAL_HASH = "%%GLOBAL_HASH%%";
|
||||
</script>
|
||||
<script id="main" type="module">
|
||||
import {main} from "./src/main.js";
|
||||
import {Platform} from "./src/platform/web/Platform.js";
|
||||
main(new Platform(document.body, {
|
||||
worker: "src/worker.js",
|
||||
downloadSandbox: "assets/download-sandbox.html",
|
||||
defaultHomeServer: "matrix.org",
|
||||
// NOTE: uncomment this if you want the service worker for local development
|
||||
// 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
|
||||
// push: {...},
|
||||
olm: {
|
||||
wasm: "lib/olm/olm.wasm",
|
||||
legacyBundle: "lib/olm/olm_legacy.js",
|
||||
wasmBundle: "lib/olm/olm.js",
|
||||
}
|
||||
}, null, {development: true}));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import base64 from "../../../../lib/base64-arraybuffer/index.js";
|
||||
import base64 from "base64-arraybuffer";
|
||||
|
||||
// turn IE11 result into promise
|
||||
function subtleCryptoResult(promiseOrOp, method) {
|
||||
|
|
|
@ -181,11 +181,11 @@ export class ServiceWorkerHandler {
|
|||
}
|
||||
|
||||
get version() {
|
||||
return window.HYDROGEN_VERSION;
|
||||
return DEFINE_VERSION;
|
||||
}
|
||||
|
||||
get buildHash() {
|
||||
return window.HYDROGEN_GLOBAL_HASH;
|
||||
return DEFINE_GLOBAL_HASH;
|
||||
}
|
||||
|
||||
async preventConcurrentSessionAccess(sessionId) {
|
||||
|
|
41
src/platform/web/index.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hydrogen Chat</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta name="application-name" content="Hydrogen Chat"/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Hydrogen Chat">
|
||||
<meta name="description" content="A matrix chat application">
|
||||
<link rel="apple-touch-icon" href="./assets/icon-maskable.png">
|
||||
<link rel="icon" type="image/png" href="assets/icon-maskable.png">
|
||||
<link rel="stylesheet" type="text/css" href="./ui/css/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="./ui/css/themes/element/theme.css">
|
||||
</head>
|
||||
<body class="hydrogen">
|
||||
<script id="main" type="module">
|
||||
import {main} from "./main";
|
||||
import {Platform} from "./Platform";
|
||||
import configJSON from "./assets/config.json?raw";
|
||||
import {olmPaths, downloadSandboxPath, workerPath} from "./sdk/paths/vite";
|
||||
const paths = {
|
||||
olm: olmPaths,
|
||||
downloadSandbox: downloadSandboxPath,
|
||||
worker: workerPath,
|
||||
...JSON.parse(configJSON)
|
||||
};
|
||||
if (import.meta.env.PROD) {
|
||||
paths.serviceWorker = "sw.js";
|
||||
}
|
||||
const platform = new Platform(
|
||||
document.body,
|
||||
paths,
|
||||
null,
|
||||
{development: import.meta.env.DEV}
|
||||
);
|
||||
main(platform);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
// polyfills needed for IE11
|
||||
import Promise from "../../../lib/es6-promise/index.js";
|
||||
import Promise from "es6-promise/lib/es6-promise/promise.js";
|
||||
import {checkNeedsSyncPromise} from "../../matrix/storage/idb/utils";
|
||||
|
||||
if (typeof window.Promise === "undefined") {
|
||||
|
|
|
@ -16,9 +16,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay";
|
||||
import {SessionContainer} from "./matrix/SessionContainer.js";
|
||||
import {RootViewModel} from "./domain/RootViewModel.js";
|
||||
import {createNavigation, createRouter} from "./domain/navigation/index.js";
|
||||
import {SessionContainer} from "../../matrix/SessionContainer.js";
|
||||
import {RootViewModel} from "../../domain/RootViewModel.js";
|
||||
import {createNavigation, createRouter} from "../../domain/navigation/index.js";
|
||||
// Don't use a default export here, as we use multiple entries during legacy build,
|
||||
// which does not support default exports,
|
||||
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
|
19
src/platform/web/sdk/paths/vite.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
// @ts-ignore
|
||||
import _downloadSandboxPath from "../../assets/download-sandbox.html?url";
|
||||
// @ts-ignore
|
||||
import _workerPath from "../../worker/main.js?url";
|
||||
// @ts-ignore
|
||||
import olmWasmPath from "@matrix-org/olm/olm.wasm?url";
|
||||
// @ts-ignore
|
||||
import olmJsPath from "@matrix-org/olm/olm.js?url";
|
||||
// @ts-ignore
|
||||
import olmLegacyJsPath from "@matrix-org/olm/olm_legacy.js?url";
|
||||
|
||||
export const olmPaths = {
|
||||
wasm: olmWasmPath,
|
||||
legacyBundle: olmLegacyJsPath,
|
||||
wasmBundle: olmJsPath,
|
||||
};
|
||||
|
||||
export const downloadSandboxPath = _downloadSandboxPath;
|
||||
export const workerPath = _workerPath;
|
|
@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
const VERSION = "%%VERSION%%";
|
||||
const GLOBAL_HASH = "%%GLOBAL_HASH%%";
|
||||
const UNHASHED_PRECACHED_ASSETS = [];
|
||||
const HASHED_PRECACHED_ASSETS = [];
|
||||
const HASHED_CACHED_ON_REQUEST_ASSETS = [];
|
||||
const NOTIFICATION_BADGE_ICON = "assets/icon.png";
|
||||
const unhashedCacheName = `hydrogen-assets-${GLOBAL_HASH}`;
|
||||
import NOTIFICATION_BADGE_ICON from "./assets/icon.png?url";
|
||||
// replaced by the service worker build plugin
|
||||
const UNHASHED_PRECACHED_ASSETS = DEFINE_UNHASHED_PRECACHED_ASSETS;
|
||||
const HASHED_PRECACHED_ASSETS = DEFINE_HASHED_PRECACHED_ASSETS;
|
||||
const HASHED_CACHED_ON_REQUEST_ASSETS = DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS;
|
||||
|
||||
const unhashedCacheName = `hydrogen-assets-${DEFINE_GLOBAL_HASH}`;
|
||||
const hashedCacheName = `hydrogen-assets`;
|
||||
const mediaThumbnailCacheName = `hydrogen-media-thumbnails-v2`;
|
||||
|
||||
|
@ -175,7 +175,7 @@ self.addEventListener('message', (event) => {
|
|||
} else {
|
||||
switch (event.data?.type) {
|
||||
case "version":
|
||||
reply({version: VERSION, buildHash: GLOBAL_HASH});
|
||||
reply({version: DEFINE_VERSION, buildHash: DEFINE_GLOBAL_HASH});
|
||||
break;
|
||||
case "skipWaiting":
|
||||
self.skipWaiting();
|
|
@ -15,10 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
export function hydrogenGithubLink(t) {
|
||||
if (window.HYDROGEN_VERSION) {
|
||||
if (DEFINE_VERSION && DEFINE_GLOBAL_HASH) {
|
||||
return t.a({target: "_blank",
|
||||
href: `https://github.com/vector-im/hydrogen-web/releases/tag/v${window.HYDROGEN_VERSION}`},
|
||||
`Hydrogen v${window.HYDROGEN_VERSION} (${window.HYDROGEN_GLOBAL_HASH}) on Github`);
|
||||
href: `https://github.com/vector-im/hydrogen-web/releases/tag/v${DEFINE_VERSION}`},
|
||||
`Hydrogen v${DEFINE_VERSION} (${DEFINE_GLOBAL_HASH}) on Github`);
|
||||
} else {
|
||||
return t.a({target: "_blank", href: "https://github.com/vector-im/hydrogen-web"},
|
||||
"Hydrogen on Github");
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import bs58 from "../../../../lib/bs58/index.js";
|
||||
import bs58 from "bs58";
|
||||
|
||||
export class Base58 {
|
||||
encode(buffer) {
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import base64 from "../../../../lib/base64-arraybuffer/index.js";
|
||||
import base64 from "base64-arraybuffer";
|
||||
|
||||
export class Base64 {
|
||||
encodeUnpadded(buffer) {
|
||||
|
|
|
@ -108,7 +108,7 @@ class MessageHandler {
|
|||
self.window = self;
|
||||
self.document = {};
|
||||
self.importScripts(path);
|
||||
const olm = self.olm_exports;
|
||||
const olm = self.Olm;
|
||||
await olm.init();
|
||||
this._olm = olm;
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
|||
// just enough to run olm, have promises and async/await
|
||||
|
||||
// load this first just in case anything else depends on it
|
||||
import Promise from "../../../../lib/es6-promise/index.js";
|
||||
import Promise from "es6-promise/lib/es6-promise/promise.js";
|
||||
// not calling checkNeedsSyncPromise from here as we don't do any idb in the worker,
|
||||
// mainly because IE doesn't handle multiple concurrent connections well
|
||||
self.Promise = Promise;
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import _downloadSandboxPath from "../../../assets/download-sandbox.html?url";
|
||||
import olmWasmPath from "../../../lib/olm/olm.wasm?url";
|
||||
import olmJsPath from "../../../lib/olm/olm.js?url";
|
||||
import olmLegacyJsPath from "../../../lib/olm/olm_legacy.js?url";
|
||||
|
||||
export const olmPaths = {
|
||||
wasm: olmWasmPath,
|
||||
legacyBundle: olmLegacyJsPath,
|
||||
wasmBundle: olmJsPath,
|
||||
};
|
||||
|
||||
export const downloadSandboxPath = _downloadSandboxPath;
|
72
vite.config.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
const cssvariables = require("postcss-css-variables");
|
||||
const flexbugsFixes = require("postcss-flexbugs-fixes");
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const injectWebManifest = require("./scripts/build-plugins/manifest");
|
||||
const {injectServiceWorker, createPlaceholderValues} = require("./scripts/build-plugins/service-worker");
|
||||
const {defineConfig} = require('vite');
|
||||
const version = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")).version;
|
||||
|
||||
export default defineConfig(({mode}) => {
|
||||
const definePlaceholders = createPlaceholderValues(mode);
|
||||
return {
|
||||
public: false,
|
||||
root: "src/platform/web",
|
||||
base: "./",
|
||||
server: {
|
||||
hmr: false
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// these should only be imported by the base-x package in any runtime code
|
||||
// and works in the browser with a Uint8Array shim,
|
||||
// rather than including a ton of polyfill code
|
||||
"safe-buffer": "./scripts/package-overrides/safe-buffer/index.js",
|
||||
"buffer": "./scripts/package-overrides/buffer/index.js",
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: "../../../target",
|
||||
emptyOutDir: true,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
assetsInlineLimit: 0,
|
||||
polyfillModulePreload: false,
|
||||
},
|
||||
plugins: [
|
||||
// important this comes before service worker
|
||||
// otherwise the manifest and the icons it refers to won't be cached
|
||||
injectWebManifest("assets/manifest.json"),
|
||||
injectServiceWorker("./src/platform/web/sw.js", ["index.html"], {
|
||||
// placeholders to replace at end of build by chunk name
|
||||
"index": {DEFINE_GLOBAL_HASH: definePlaceholders.DEFINE_GLOBAL_HASH},
|
||||
"sw": definePlaceholders
|
||||
}),
|
||||
],
|
||||
define: {
|
||||
DEFINE_VERSION: JSON.stringify(version),
|
||||
...definePlaceholders
|
||||
},
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [
|
||||
cssvariables({
|
||||
preserve: (declaration) => {
|
||||
return declaration.value.indexOf("var(--ios-") == 0;
|
||||
}
|
||||
}),
|
||||
// the grid option creates some source fragment that causes the vite warning reporter to crash because
|
||||
// it wants to log a warning on a line that does not exist in the source fragment.
|
||||
// autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}),
|
||||
flexbugsFixes()
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function scriptTagPath(htmlFile, index) {
|
||||
return `${htmlFile}?html-proxy&index=${index}.js`;
|
||||
}
|