diff --git a/scripts/build.mjs b/scripts/build.mjs
index a8f86fe9..f18c04a4 100644
--- a/scripts/build.mjs
+++ b/scripts/build.mjs
@@ -41,10 +41,6 @@ import postcssUrl from "postcss-url";
import cssvariables from "postcss-css-variables";
import flexbugsFixes from "postcss-flexbugs-fixes";
-const PROJECT_ID = "hydrogen";
-const PROJECT_SHORT_NAME = "Hydrogen";
-const PROJECT_NAME = "Hydrogen Chat";
-
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectDir = path.join(__dirname, "../");
@@ -55,21 +51,10 @@ const program = new commander.Command();
program
.option("--no-offline", "make a build without a service worker or appcache manifest")
program.parse(process.argv);
-const {debug, noOffline} = program;
+const {noOffline} = program;
const offline = !noOffline;
-const olmFiles = {
- wasm: "olm-4289088762.wasm",
- legacyBundle: "olm_legacy-3232457086.js",
- wasmBundle: "olm-1421970081.js",
-};
-
-// IDEA: how about instead of assetPaths we maintain a mapping between the source file and the target file
-// so throughout the build script we can refer to files by their source name
-
async function build() {
- // only used for CSS for now, using legacy for all targets for now
- const legacy = true;
// get version number
const version = JSON.parse(await fs.readFile(path.join(projectDir, "package.json"), "utf8")).version;
@@ -82,46 +67,23 @@ async function build() {
// clear target dir
await removeDirIfExists(targetDir);
await createDirs(targetDir, themes);
- // copy assets
- await copyFolder(path.join(projectDir, "lib/olm/"), targetDir);
- // also creates the directories where the theme css bundles are placed in,
- // so do it first
- const themeAssets = await copyThemeAssets(themes, legacy);
- const jsBundlePath = await buildJs("src/main.js", `${PROJECT_ID}.js`);
- const jsLegacyBundlePath = await buildJsLegacy("src/main.js", `${PROJECT_ID}-legacy.js`, 'src/legacy-extras.js');
- const jsWorkerPath = await buildWorkerJsLegacy("src/worker.js", `worker.js`);
- const cssBundlePaths = await buildCssBundles(legacy ? buildCssLegacy : buildCss, themes, themeAssets);
-
- let manifestPath;
-
- const assetPaths = createAssetPaths(jsBundlePath, jsLegacyBundlePath, jsWorkerPath,
- cssBundlePaths, themeAssets);
-
+ const assets = new AssetMap(targetDir);
+ // copy olm assets
+ const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory);
+ assets.addSubMap(olmAssets);
+ await buildJs("src/main.js", code => assets.create(`hydrogen.js`, code));
+ await buildJsLegacy("src/main.js", code => assets.create(`hydrogen-legacy.js`, code), 'src/legacy-extras.js');
+ await buildWorkerJsLegacy("src/worker.js", code => assets.create(`worker.js`, code));
+ // 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);
if (offline) {
- manifestPath = await buildOffline(version, assetPaths);
+ await buildOffline(version, assets);
}
- await buildHtml(doc, version, assetPaths, manifestPath);
-
- console.log(`built ${PROJECT_ID} ${version} successfully`);
-}
-
-function trim(path) {
- if (!path.startsWith(targetDir)) {
- throw new Error("invalid target path: " + targetDir);
- }
- return path.substr(targetDir.length);
-}
-
-function createAssetPaths(jsBundlePath, jsLegacyBundlePath, jsWorkerPath, cssBundlePaths, themeAssets) {
- return {
- jsBundle: () => trim(jsBundlePath),
- jsLegacyBundle: () => trim(jsLegacyBundlePath),
- jsWorker: () => trim(jsWorkerPath),
- cssMainBundle: () => trim(cssBundlePaths.main),
- cssThemeBundle: themeName => trim(cssBundlePaths.themes[themeName]),
- cssThemeBundles: () => Object.values(cssBundlePaths.themes).map(a => trim(a)),
- otherAssets: () => Object.values(themeAssets).map(a => trim(a)),
- };
+ await buildHtml(doc, version, assets);
+ // 3 unhashed assets: index.html, manifest.appcache and sw.js
+ console.log(`built hydrogen ${version} successfully with ${assets.all().length + 3} files`);
}
async function findThemes(doc, callback) {
@@ -148,36 +110,38 @@ async function createDirs(targetDir, themes) {
}
}
-async function copyThemeAssets(themes, legacy) {
- const assets = {};
+async function copyThemeAssets(themes, assets) {
for (const theme of themes) {
const themeDstFolder = path.join(targetDir, `themes/${theme}`);
const themeSrcFolder = path.join(cssSrcDir, `themes/${theme}`);
const themeAssets = await copyFolder(themeSrcFolder, themeDstFolder, file => {
- const isUnneededFont = legacy ? file.endsWith(".woff2") : file.endsWith(".woff");
- return !file.endsWith(".css") && !isUnneededFont;
+ return !file.endsWith(".css");
});
- Object.assign(assets, themeAssets);
+ assets.addSubMap(themeAssets);
}
return assets;
}
-async function buildHtml(doc, version, assetPaths, manifestPath) {
+async function buildHtml(doc, version, assets) {
// transform html file
// change path to main.css to css bundle
- doc("link[rel=stylesheet]:not([title])").attr("href", assetPaths.cssMainBundle());
+ doc("link[rel=stylesheet]:not([title])").attr("href", assets.resolve(`hydrogen.css`));
// change paths to all theme stylesheets
findThemes(doc, (themeName, theme) => {
- theme.attr("href", assetPaths.cssThemeBundle(themeName));
+ theme.attr("href", assets.resolve(`themes/${themeName}/bundle.css`));
});
const pathsJSON = JSON.stringify({
- worker: assetPaths.jsWorker(),
- olm: olmFiles
+ worker: assets.resolve(`worker.js`),
+ olm: {
+ wasm: assets.resolve("olm.wasm"),
+ legacyBundle: assets.resolve("olm_legacy.js"),
+ wasmBundle: assets.resolve("olm.js"),
+ }
});
doc("script#main").replaceWith(
- `` +
- `` +
- ``);
+ `` +
+ `` +
+ ``);
removeOrEnableScript(doc("script#service-worker"), offline);
const versionScript = doc("script#version");
@@ -188,12 +152,12 @@ async function buildHtml(doc, version, assetPaths, manifestPath) {
if (offline) {
doc("html").attr("manifest", "manifest.appcache");
- doc("head").append(``);
+ doc("head").append(``);
}
await fs.writeFile(path.join(targetDir, "index.html"), doc.html(), "utf8");
}
-async function buildJs(inputFile, outputName) {
+async function buildJs(inputFile, outputNameFn) {
// create js bundle
const bundle = await rollup({
input: inputFile,
@@ -202,15 +166,13 @@ async function buildJs(inputFile, outputName) {
const {output} = await bundle.generate({
format: 'es',
// TODO: can remove this?
- name: `${PROJECT_ID}Bundle`
+ name: `hydrogenBundle`
});
const code = output[0].code;
- const bundlePath = resource(outputName, code);
- await fs.writeFile(bundlePath, code, "utf8");
- return bundlePath;
+ await fs.writeFile(outputNameFn(code), code, "utf8");
}
-async function buildJsLegacy(inputFile, outputName, extraFile, polyfillFile) {
+async function buildJsLegacy(inputFile, outputNameFn, extraFile, polyfillFile) {
// compile down to whatever IE 11 needs
const babelPlugin = babel.babel({
babelHelpers: 'bundled',
@@ -245,35 +207,26 @@ async function buildJsLegacy(inputFile, outputName, extraFile, polyfillFile) {
const bundle = await rollup(rollupConfig);
const {output} = await bundle.generate({
format: 'iife',
- name: `${PROJECT_ID}Bundle`
+ name: `hydrogenBundle`
});
const code = output[0].code;
- const bundlePath = resource(outputName, code);
- await fs.writeFile(bundlePath, code, "utf8");
- return bundlePath;
+ await fs.writeFile(outputNameFn(code), code, "utf8");
}
-function buildWorkerJsLegacy(inputFile, outputName) {
+function buildWorkerJsLegacy(inputFile, outputNameFn) {
const polyfillFile = 'src/worker-polyfill.js';
- return buildJsLegacy(inputFile, outputName, null, polyfillFile);
+ return buildJsLegacy(inputFile, outputNameFn, null, polyfillFile);
}
-async function buildOffline(version, assetPaths) {
- // write web manifest
+async function buildOffline(version, 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));
- let iconPath = resource(path.basename(icon.src), iconData);
- await fs.writeFile(iconPath, iconData);
- icon.src = trim(iconPath);
+ const iconTargetPath = path.basename(icon.src);
+ await fs.writeFile(assets.create(iconTargetPath, iconData), iconData);
+ icon.src = assets.resolve(iconTargetPath);
}
- // write offline availability
- const offlineFiles = [
- assetPaths.cssMainBundle(),
- "index.html",
- ].concat(assetPaths.cssThemeBundles())
- .concat(webManifest.icons.map(i => i.src));
-
// write appcache manifest
const appCacheLines = [
`CACHE MANIFEST`,
@@ -282,53 +235,51 @@ async function buildOffline(version, assetPaths) {
`"*"`,
`CACHE`,
];
- appCacheLines.push(assetPaths.jsLegacyBundle(), ...offlineFiles);
- const swOfflineFiles = [assetPaths.jsBundle(), ...offlineFiles];
+ appCacheLines.push(...assets.allWithout(["hydrogen.js"]));
+ const swOfflineFiles = assets.allWithout(["hydrogen-legacy.js", "olm_legacy.js"]);
const appCacheManifest = appCacheLines.join("\n") + "\n";
await fs.writeFile(path.join(targetDir, "manifest.appcache"), appCacheManifest, "utf8");
// write service worker
let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8");
swSource = swSource.replace(`"%%VERSION%%"`, `"${version}"`);
swSource = swSource.replace(`"%%OFFLINE_FILES%%"`, JSON.stringify(swOfflineFiles));
- swSource = swSource.replace(`"%%CACHE_FILES%%"`, JSON.stringify(assetPaths.otherAssets()));
+ // service worker should not have a hashed name as it is polled by the browser for updates
await fs.writeFile(path.join(targetDir, "sw.js"), swSource, "utf8");
const manifestJson = JSON.stringify(webManifest);
- const manifestPath = resource("manifest.json", manifestJson);
- await fs.writeFile(manifestPath, manifestJson, "utf8");
- return manifestPath;
+ await fs.writeFile(assets.create("manifest.json", manifestJson), manifestJson, "utf8");
}
-async function buildCssBundles(buildFn, themes, themeAssets) {
+async function buildCssBundles(buildFn, themes, assets) {
const bundleCss = await buildFn(path.join(cssSrcDir, "main.css"));
- const mainDstPath = resource(`${PROJECT_ID}.css`, bundleCss);
- await fs.writeFile(mainDstPath, bundleCss, "utf8");
- const bundlePaths = {main: mainDstPath, themes: {}};
+ await fs.writeFile(assets.create(`hydrogen.css`, bundleCss), bundleCss, "utf8");
for (const theme of themes) {
- const urlBase = path.join(targetDir, `themes/${theme}/`);
+ const themeRelPath = `themes/${theme}/`;
+ const themeRoot = path.join(cssSrcDir, themeRelPath);
const assetUrlMapper = ({absolutePath}) => {
- const hashedDstPath = themeAssets[absolutePath];
- if (hashedDstPath && hashedDstPath.startsWith(urlBase)) {
- return hashedDstPath.substr(urlBase.length);
+ 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(cssSrcDir, `themes/${theme}/theme.css`), assetUrlMapper);
- const themeDstPath = resource(`themes/${theme}/bundle.css`, themeCss);
- await fs.writeFile(themeDstPath, themeCss, "utf8");
- bundlePaths.themes[theme] = themeDstPath;
+ const themeCss = await buildFn(path.join(themeRoot, `theme.css`), assetUrlMapper);
+ await fs.writeFile(assets.create(path.join(themeRelPath, `bundle.css`), themeCss), themeCss, "utf8");
}
- return bundlePaths;
}
-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 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");
@@ -363,35 +314,21 @@ async function removeDirIfExists(targetDir) {
}
}
-async function copyFolder(srcRoot, dstRoot, filter) {
- const assetPaths = {};
+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);
- Object.assign(assetPaths, await copyFolder(srcPath, dstPath, filter));
+ await copyFolder(srcPath, dstPath, filter, assets);
} else if ((dirEnt.isFile() || dirEnt.isSymbolicLink()) && (!filter || filter(srcPath))) {
const content = await fs.readFile(srcPath);
- const hashedDstPath = resource(dstPath, content);
- await fs.writeFile(hashedDstPath, content);
- assetPaths[srcPath] = hashedDstPath;
+ await fs.writeFile(assets.create(dstPath, content), content);
}
}
- return assetPaths;
-}
-
-function resource(relPath, content) {
- let fullPath = relPath;
- if (!path.isAbsolute(relPath)) {
- fullPath = path.join(targetDir, relPath);
- }
- const hash = contentHash(Buffer.from(content));
- const dir = path.dirname(fullPath);
- const extname = path.extname(fullPath);
- const basename = path.basename(fullPath, extname);
- return path.join(dir, `${basename}-${hash}${extname}`);
+ return assets;
}
function contentHash(str) {
@@ -400,5 +337,69 @@ function contentHash(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();
+ }
+
+ _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 path.join(this._targetDir, dstRelPath);
+ }
+
+ 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}`);
+ }
+ console.log("adding submap from", 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));
+ }
+ }
+
+ all() {
+ return Array.from(this._assets.values());
+ }
+
+ allWithout(excluded) {
+ excluded = excluded.map(p => this.resolve(p));
+ return this.all().filter(p => excluded.indexOf(p) === -1);
+ }
+}
+
+
build().catch(err => console.error(err));