diff --git a/.gitignore b/.gitignore index 7f6220cf..089600eb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ bundle.js target lib *.tar.gz -.eslintcache \ No newline at end of file +.eslintcache +.tmp diff --git a/doc/SDK.md b/doc/SDK.md index 8ce0b304..54e37cca 100644 --- a/doc/SDK.md +++ b/doc/SDK.md @@ -47,7 +47,8 @@ const assetPaths = { wasmBundle: olmJsPath } }; -import "hydrogen-view-sdk/style.css"; +import "hydrogen-view-sdk/theme-element-light.css"; +// OR import "hydrogen-view-sdk/theme-element-dark.css"; async function main() { const app = document.querySelector('#app')! diff --git a/package.json b/package.json index 4e0b0643..dce5380e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.26", + "version": "0.2.28", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" @@ -10,7 +10,7 @@ "lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts", "lint-ci": "eslint src/", "test": "impunity --entry-point src/platform/web/main.js src/platform/web/Platform.js --force-esm-dirs lib/ src/ --root-dir src/", - "test:postcss": "impunity --entry-point scripts/postcss/test.js ", + "test:postcss": "impunity --entry-point scripts/postcss/tests/css-compile-variables.test.js scripts/postcss/tests/css-url-to-variables.test.js", "test:sdk": "cd ./scripts/sdk/test/ && yarn --no-lockfile && node test-sdk-in-esm-vite-build-env.js && node test-sdk-in-commonjs-env.js", "start": "vite --port 3000", "build": "vite build", @@ -45,6 +45,7 @@ "node-html-parser": "^4.0.0", "postcss-css-variables": "^0.18.0", "postcss-flexbugs-fixes": "^5.0.2", + "postcss-value-parser": "^4.2.0", "regenerator-runtime": "^0.13.7", "text-encoding": "^0.7.0", "typescript": "^4.3.5", @@ -56,7 +57,6 @@ "another-json": "^0.2.0", "base64-arraybuffer": "^0.2.0", "dompurify": "^2.3.0", - "off-color": "^2.0.0", - "postcss-value-parser": "^4.2.0" + "off-color": "^2.0.0" } } diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js new file mode 100644 index 00000000..e7a2bb2b --- /dev/null +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -0,0 +1,254 @@ +/* +Copyright 2021 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 path = require('path'); + +async function readCSSSource(location) { + const fs = require("fs").promises; + const path = require("path"); + const resolvedLocation = path.resolve(__dirname, "../../", `${location}/theme.css`); + const data = await fs.readFile(resolvedLocation); + return data; +} + +function getRootSectionWithVariables(variables) { + return `:root{\n${Object.entries(variables).reduce((acc, [key, value]) => acc + `--${key}: ${value};\n`, "")} }\n\n`; +} + +function appendVariablesToCSS(variables, cssSource) { + return cssSource + getRootSectionWithVariables(variables); +} + +function parseBundle(bundle) { + const chunkMap = new Map(); + const assetMap = new Map(); + let runtimeThemeChunk; + for (const [fileName, info] of Object.entries(bundle)) { + if (!fileName.endsWith(".css")) { + continue; + } + if (info.type === "asset") { + /** + * So this is the css assetInfo that contains the asset hashed file name. + * We'll store it in a separate map indexed via fileName (unhashed) to avoid + * searching through the bundle array later. + */ + assetMap.set(info.name, info); + continue; + } + if (info.facadeModuleId?.includes("type=runtime")) { + /** + * We have a separate field in manifest.source just for the runtime theme, + * so store this separately. + */ + runtimeThemeChunk = info; + continue; + } + const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1]; + if (!location) { + throw new Error("Cannot find location of css chunk!"); + } + const array = chunkMap.get(location); + if (!array) { + chunkMap.set(location, [info]); + } + else { + array.push(info); + } + } + return { chunkMap, assetMap, runtimeThemeChunk }; +} + +module.exports = function buildThemes(options) { + let manifest, variants, defaultDark, defaultLight; + let isDevelopment = false; + const virtualModuleId = '@theme/' + const resolvedVirtualModuleId = '\0' + virtualModuleId; + + return { + name: "build-themes", + enforce: "pre", + + configResolved(config) { + if (config.command === "serve") { + isDevelopment = true; + } + }, + + async buildStart() { + if (isDevelopment) { return; } + const { themeConfig } = options; + for (const [name, location] of Object.entries(themeConfig.themes)) { + manifest = require(`${location}/manifest.json`); + variants = manifest.values.variants; + for (const [variant, details] of Object.entries(variants)) { + const fileName = `theme-${name}-${variant}.css`; + if (name === themeConfig.default && details.default) { + // This is the default theme, stash the file name for later + if (details.dark) { + defaultDark = fileName; + } + else { + defaultLight = fileName; + } + } + // emit the css as built theme bundle + this.emitFile({ + type: "chunk", + id: `${location}/theme.css?variant=${variant}${details.dark? "&dark=true": ""}`, + fileName, + }); + } + // emit the css as runtime theme bundle + this.emitFile({ + type: "chunk", + id: `${location}/theme.css?type=runtime`, + fileName: `theme-${name}-runtime.css`, + }); + } + }, + + resolveId(id) { + if (id.startsWith(virtualModuleId)) { + return '\0' + id; + } + }, + + async load(id) { + if (isDevelopment) { + /** + * To load the theme during dev, we need to take a different approach because emitFile is not supported in dev. + * We solve this by resolving virtual file "@theme/name/variant" into the necessary css import. + * This virtual file import is removed when hydrogen is built (see transform hook). + */ + if (id.startsWith(resolvedVirtualModuleId)) { + let [theme, variant, file] = id.substr(resolvedVirtualModuleId.length).split("/"); + if (theme === "default") { + theme = options.themeConfig.default; + } + const location = options.themeConfig.themes[theme]; + const manifest = require(`${location}/manifest.json`); + const variants = manifest.values.variants; + if (!variant || variant === "default") { + // choose the first default variant for now + // this will need to support light/dark variants as well + variant = Object.keys(variants).find(variantName => variants[variantName].default); + } + if (!file) { + file = "index.js"; + } + switch (file) { + case "index.js": { + const isDark = variants[variant].dark; + return `import "${path.resolve(`${location}/theme.css`)}${isDark? "?dark=true": ""}";` + + `import "@theme/${theme}/${variant}/variables.css"`; + } + case "variables.css": { + const variables = variants[variant].variables; + const css = getRootSectionWithVariables(variables); + return css; + } + } + } + } + else { + const result = id.match(/(.+)\/theme.css\?variant=([^&]+)/); + if (result) { + const [, location, variant] = result; + const cssSource = await readCSSSource(location); + const config = variants[variant]; + return appendVariablesToCSS(config.variables, cssSource); + } + return null; + } + }, + + transform(code, id) { + if (isDevelopment) { + return; + } + /** + * Removes develop-only script tag; this cannot be done in transformIndexHtml hook because + * by the time that hook runs, the import is added to the bundled js file which would + * result in a runtime error. + */ + + const devScriptTag = + /