vector-im-hydrogen-web/scripts/build-plugins/rollup-plugin-build-themes.js

375 lines
16 KiB
JavaScript
Raw Normal View History

2022-03-25 11:35:27 +05:30
/*
2025-01-17 17:05:05 +00:00
Copyright 2025 New Vector Ltd.
2022-03-25 11:35:27 +05:30
Copyright 2021 The Matrix.org Foundation C.I.C.
2025-01-17 17:05:05 +00:00
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
2022-03-25 11:35:27 +05:30
*/
Fix SDK asset build failing on Windows (#859) Fix: ```sh $ yarn run vite build -c vite.sdk-assets-config.js yarn run v1.22.18 $ C:\Users\MLM\Documents\GitHub\element\hydrogen-web\node_modules\.bin\vite build -c vite.sdk-assets-config.js locally linked postcss cleanUrl(id) C:/Users/MLM/Documents/GitHub/element/hydrogen-web/src/platform/web/ui/css/themes/element/theme.css C:/Users/MLM/Documents/GitHub/element/hydrogen-web/src/platform/web/ui/css/themes/element/theme.css?type=runtime [build-themes] Could not load C:/Users/MLM/Documents/GitHub/element/hydrogen-web/src/platform/web/ui/css/themes/element/theme.css?variant=light: ENOENT: no such file or directory, open 'C:\Users\MLM\Documents\GitHub\element\C:\Users\MLM\Documents\GitHub\element\hydrogen-web\src\platform\web\ui\css\themes\element\theme.css' error during build: Error: Could not load C:/Users/MLM/Documents/GitHub/element/hydrogen-web/src/platform/web/ui/css/themes/element/theme.css?variant=light: ENOENT: no such file or directory, open 'C:\Users\MLM\Documents\GitHub\element\C:\Users\MLM\Documents\GitHub\element\hydrogen-web\src\platform\web\ui\css\themes\element\theme.css' error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. ``` Regressed in: https://github.com/vector-im/hydrogen-web/pull/769/files#diff-5432b565e86d2514c825ed9972c37ea19820bf12b5d8d3203fc9d4ea4654bd34L20 where the `const path = require('path');` was removed but we also started using `path` in more places which needed the same treatment. When making the fix, we also have to make sure we don't also regress: https://github.com/vector-im/hydrogen-web/pull/750
2022-09-19 12:20:50 -05:00
// Use the path implementation native to the platform so paths from disk play
// well with resolving against the relative location (think Windows `C:\` and
// backslashes).
const path = require('path');
// Use the posix (forward slash) implementation when working with `import` paths
// to reference resources
const posixPath = require('path').posix;
2022-07-04 16:42:56 +05:30
const {optimize} = require('svgo');
2022-03-25 11:35:27 +05:30
async function readCSSSource(location) {
const fs = require("fs").promises;
2022-04-10 14:49:19 +05:30
const resolvedLocation = path.resolve(__dirname, "../../", `${location}/theme.css`);
2022-03-25 11:35:27 +05:30
const data = await fs.readFile(resolvedLocation);
return data;
}
2022-04-10 14:49:19 +05:30
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);
}
2022-05-11 12:40:32 +05:30
function addThemesToConfig(bundle, manifestLocations, defaultThemes) {
2022-04-25 14:29:31 +05:30
for (const [fileName, info] of Object.entries(bundle)) {
2022-05-12 16:02:03 +05:30
if (fileName === "config.json") {
2022-04-25 14:29:31 +05:30
const source = new TextDecoder().decode(info.source);
const config = JSON.parse(source);
2022-05-11 12:40:32 +05:30
config["themeManifests"] = manifestLocations;
2022-04-25 15:56:45 +05:30
config["defaultTheme"] = defaultThemes;
info.source = new TextEncoder().encode(JSON.stringify(config, undefined, 2));
2022-04-25 14:29:31 +05:30
}
}
}
2022-07-04 16:42:56 +05:30
/**
* Returns an object where keys are the svg file names and the values
* are the svg code (optimized)
* @param {*} icons Object where keys are css variable names and values are locations of the svg
* @param {*} manifestLocation Location of manifest used for resolving path
*/
async function generateIconSourceMap(icons, manifestLocation) {
const sources = {};
2022-07-21 12:05:10 +05:30
const fileNames = [];
const promises = [];
2022-07-04 16:42:56 +05:30
const fs = require("fs").promises;
for (const icon of Object.values(icons)) {
const [location] = icon.split("?");
2022-07-29 16:45:25 +05:30
// resolve location against manifestLocation
const resolvedLocation = path.resolve(manifestLocation, location);
2022-07-21 12:05:10 +05:30
const iconData = fs.readFile(resolvedLocation);
promises.push(iconData);
const fileName = path.basename(resolvedLocation);
fileNames.push(fileName);
}
const results = await Promise.all(promises);
for (let i = 0; i < results.length; ++i) {
const svgString = results[i].toString();
2022-07-15 14:59:50 +05:30
const result = optimize(svgString, {
plugins: [
{
name: "preset-default",
params: {
overrides: { convertColors: false, },
},
},
],
});
2022-07-04 16:42:56 +05:30
const optimizedSvgString = result.data;
2022-07-21 12:05:10 +05:30
sources[fileNames[i]] = optimizedSvgString;
2022-07-04 16:42:56 +05:30
}
return sources;
}
2022-06-20 21:10:11 +05:30
/**
* Returns a mapping from location (of manifest file) to an array containing all the chunks (of css files) generated from that location.
* To understand what chunk means in this context, see https://rollupjs.org/guide/en/#generatebundle.
* @param {*} bundle Mapping from fileName to AssetInfo | ChunkInfo
*/
function getMappingFromLocationToChunkArray(bundle) {
2022-04-01 14:27:24 +05:30
const chunkMap = new Map();
2022-06-20 21:10:11 +05:30
for (const [fileName, info] of Object.entries(bundle)) {
if (!fileName.endsWith(".css") || info.type === "asset" || info.facadeModuleId?.includes("type=runtime")) {
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;
}
/**
* Returns a mapping from unhashed file name (of css files) to AssetInfo.
* To understand what AssetInfo means in this context, see https://rollupjs.org/guide/en/#generatebundle.
* @param {*} bundle Mapping from fileName to AssetInfo | ChunkInfo
*/
function getMappingFromFileNameToAssetInfo(bundle) {
2022-04-01 14:27:24 +05:30
const assetMap = new Map();
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);
2022-06-20 21:10:11 +05:30
}
}
return assetMap;
}
/**
* Returns a mapping from location (of manifest file) to ChunkInfo of the runtime css asset
* To understand what ChunkInfo means in this context, see https://rollupjs.org/guide/en/#generatebundle.
* @param {*} bundle Mapping from fileName to AssetInfo | ChunkInfo
*/
function getMappingFromLocationToRuntimeChunk(bundle) {
let runtimeThemeChunkMap = new Map();
for (const [fileName, info] of Object.entries(bundle)) {
if (!fileName.endsWith(".css") || info.type === "asset") {
2022-04-01 14:27:24 +05:30
continue;
}
const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1];
if (!location) {
throw new Error("Cannot find location of css chunk!");
}
2022-04-01 14:27:24 +05:30
if (info.facadeModuleId?.includes("type=runtime")) {
/**
* We have a separate field in manifest.source just for the runtime theme,
* so store this separately.
*/
runtimeThemeChunkMap.set(location, info);
2022-04-01 14:27:24 +05:30
}
}
2022-06-20 21:10:11 +05:30
return runtimeThemeChunkMap;
2022-04-01 14:27:24 +05:30
}
2022-03-25 11:35:27 +05:30
module.exports = function buildThemes(options) {
2022-04-25 16:01:02 +05:30
let manifest, variants, defaultDark, defaultLight, defaultThemes = {};
2022-04-10 14:49:19 +05:30
let isDevelopment = false;
const virtualModuleId = '@theme/'
const resolvedVirtualModuleId = '\0' + virtualModuleId;
const themeToManifestLocation = new Map();
2022-03-25 11:35:27 +05:30
return {
name: "build-themes",
enforce: "pre",
2022-04-10 14:49:19 +05:30
configResolved(config) {
if (config.command === "serve") {
isDevelopment = true;
}
},
2022-03-25 11:35:27 +05:30
async buildStart() {
2022-04-13 12:40:49 +05:30
const { themeConfig } = options;
for (const location of themeConfig.themes) {
2022-03-25 11:35:27 +05:30
manifest = require(`${location}/manifest.json`);
const themeCollectionId = manifest.id;
themeToManifestLocation.set(themeCollectionId, location);
2022-03-25 11:35:27 +05:30
variants = manifest.values.variants;
2022-03-28 18:02:53 +05:30
for (const [variant, details] of Object.entries(variants)) {
const fileName = `theme-${themeCollectionId}-${variant}.css`;
if (themeCollectionId === themeConfig.default && details.default) {
2022-04-13 12:40:49 +05:30
// This is the default theme, stash the file name for later
2022-03-28 18:02:53 +05:30
if (details.dark) {
defaultDark = fileName;
defaultThemes["dark"] = `${themeCollectionId}-${variant}`;
2022-03-28 18:02:53 +05:30
}
else {
defaultLight = fileName;
defaultThemes["light"] = `${themeCollectionId}-${variant}`;
2022-03-28 18:02:53 +05:30
}
}
2022-03-25 11:35:27 +05:30
// emit the css as built theme bundle
2022-07-19 15:50:01 +05:30
if (!isDevelopment) {
this.emitFile({ type: "chunk", id: `${location}/theme.css?variant=${variant}${details.dark ? "&dark=true" : ""}`, fileName, });
}
2022-03-25 11:35:27 +05:30
}
2022-03-29 11:46:06 +05:30
// emit the css as runtime theme bundle
2022-07-19 15:50:01 +05:30
if (!isDevelopment) {
this.emitFile({ type: "chunk", id: `${location}/theme.css?type=runtime`, fileName: `theme-${themeCollectionId}-runtime.css`, });
}
2022-03-25 11:35:27 +05:30
}
},
2022-04-10 14:49:19 +05:30
resolveId(id) {
if (id.startsWith(virtualModuleId)) {
return '\0' + id;
2022-03-25 11:35:27 +05:30
}
},
2022-03-28 18:02:53 +05:30
2022-04-10 14:49:19 +05:30
async load(id) {
if (isDevelopment) {
2022-04-13 13:39:20 +05:30
/**
* 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).
*/
2022-04-10 14:49:19 +05:30
if (id.startsWith(resolvedVirtualModuleId)) {
let [theme, variant, file] = id.substr(resolvedVirtualModuleId.length).split("/");
if (theme === "default") {
2022-04-13 12:56:14 +05:30
theme = options.themeConfig.default;
2022-04-10 14:49:19 +05:30
}
const location = themeToManifestLocation.get(theme);
2022-04-13 12:56:14 +05:30
const manifest = require(`${location}/manifest.json`);
const variants = manifest.values.variants;
2022-04-10 14:49:19 +05:30
if (!variant || variant === "default") {
2022-04-13 12:56:14 +05:30
// 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);
2022-04-10 14:49:19 +05:30
}
if (!file) {
file = "index.js";
}
switch (file) {
case "index.js": {
2022-04-13 12:56:14 +05:30
const isDark = variants[variant].dark;
Fix SDK asset build failing on Windows (#859) Fix: ```sh $ yarn run vite build -c vite.sdk-assets-config.js yarn run v1.22.18 $ C:\Users\MLM\Documents\GitHub\element\hydrogen-web\node_modules\.bin\vite build -c vite.sdk-assets-config.js locally linked postcss cleanUrl(id) C:/Users/MLM/Documents/GitHub/element/hydrogen-web/src/platform/web/ui/css/themes/element/theme.css C:/Users/MLM/Documents/GitHub/element/hydrogen-web/src/platform/web/ui/css/themes/element/theme.css?type=runtime [build-themes] Could not load C:/Users/MLM/Documents/GitHub/element/hydrogen-web/src/platform/web/ui/css/themes/element/theme.css?variant=light: ENOENT: no such file or directory, open 'C:\Users\MLM\Documents\GitHub\element\C:\Users\MLM\Documents\GitHub\element\hydrogen-web\src\platform\web\ui\css\themes\element\theme.css' error during build: Error: Could not load C:/Users/MLM/Documents/GitHub/element/hydrogen-web/src/platform/web/ui/css/themes/element/theme.css?variant=light: ENOENT: no such file or directory, open 'C:\Users\MLM\Documents\GitHub\element\C:\Users\MLM\Documents\GitHub\element\hydrogen-web\src\platform\web\ui\css\themes\element\theme.css' error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. ``` Regressed in: https://github.com/vector-im/hydrogen-web/pull/769/files#diff-5432b565e86d2514c825ed9972c37ea19820bf12b5d8d3203fc9d4ea4654bd34L20 where the `const path = require('path');` was removed but we also started using `path` in more places which needed the same treatment. When making the fix, we also have to make sure we don't also regress: https://github.com/vector-im/hydrogen-web/pull/750
2022-09-19 12:20:50 -05:00
return `import "${posixPath.resolve(`${location}/theme.css`)}${isDark? "?dark=true": ""}";` +
2022-04-10 14:49:19 +05:30
`import "@theme/${theme}/${variant}/variables.css"`;
}
case "variables.css": {
2022-04-13 12:56:14 +05:30
const variables = variants[variant].variables;
2022-04-10 14:49:19 +05:30
const css = getRootSectionWithVariables(variables);
return css;
}
}
2022-03-28 18:02:53 +05:30
}
2022-04-10 14:49:19 +05:30
}
else {
const result = id.match(/(.+)\/theme.css\?variant=([^&]+)/);
2022-04-10 14:49:19 +05:30
if (result) {
const [, location, variant] = result;
const cssSource = await readCSSSource(location);
const config = variants[variant];
2022-04-13 12:56:14 +05:30
return appendVariablesToCSS(config.variables, cssSource);
2022-03-28 18:02:53 +05:30
}
2022-04-10 14:49:19 +05:30
return null;
2022-03-28 18:02:53 +05:30
}
2022-04-10 14:52:26 +05:30
},
transform(code, id) {
if (isDevelopment) {
return;
}
2022-04-13 13:39:20 +05:30
/**
* 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 =
/<script type="module"> import "@theme\/.+"; <\/script>/;
if (id.endsWith("index.html")) {
const htmlWithoutDevScript = code.replace(devScriptTag, "");
2022-04-13 13:39:20 +05:30
return htmlWithoutDevScript;
}
},
2022-04-10 14:52:26 +05:30
transformIndexHtml(_, ctx) {
if (isDevelopment) {
// Don't add default stylesheets to index.html on dev
return;
}
let darkThemeLocation, lightThemeLocation;
for (const [, bundle] of Object.entries(ctx.bundle)) {
if (bundle.name === defaultDark) {
darkThemeLocation = bundle.fileName;
2022-04-10 14:49:19 +05:30
}
2022-04-10 14:52:26 +05:30
if (bundle.name === defaultLight) {
lightThemeLocation = bundle.fileName;
2022-04-10 14:49:19 +05:30
}
2022-04-10 14:52:26 +05:30
}
return [
{
tag: "link",
attrs: {
rel: "stylesheet",
type: "text/css",
media: "(prefers-color-scheme: dark)",
href: `./${darkThemeLocation}`,
class: "theme",
2022-04-10 14:52:26 +05:30
}
},
{
tag: "link",
attrs: {
rel: "stylesheet",
type: "text/css",
media: "(prefers-color-scheme: light)",
href: `./${lightThemeLocation}`,
class: "theme",
2022-04-10 14:52:26 +05:30
}
},
];
2022-04-13 12:56:14 +05:30
},
2022-04-10 14:49:19 +05:30
2022-07-04 16:42:56 +05:30
async generateBundle(_, bundle) {
2022-06-20 21:10:11 +05:30
const assetMap = getMappingFromFileNameToAssetInfo(bundle);
const chunkMap = getMappingFromLocationToChunkArray(bundle);
const runtimeThemeChunkMap = getMappingFromLocationToRuntimeChunk(bundle);
2022-05-11 12:40:32 +05:30
const manifestLocations = [];
2022-06-20 20:35:06 +05:30
// Location of the directory containing manifest relative to the root of the build output
const manifestLocation = "assets";
2022-04-10 14:52:26 +05:30
for (const [location, chunkArray] of chunkMap) {
const manifest = require(`${location}/manifest.json`);
const compiledVariables = options.compiledVariables.get(location);
const derivedVariables = compiledVariables["derived-variables"];
const icon = compiledVariables["icon"];
2022-05-12 14:31:28 +05:30
const builtAssets = {};
let themeKey;
for (const chunk of chunkArray) {
const [, name, variant] = chunk.fileName.match(/theme-(.+)-(.+)\.css/);
themeKey = name;
2022-06-20 20:35:06 +05:30
const locationRelativeToBuildRoot = assetMap.get(chunk.fileName).fileName;
const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot);
builtAssets[`${name}-${variant}`] = locationRelativeToManifest;
}
2022-07-04 16:42:56 +05:30
// Emit the base svg icons as asset
const nameToAssetHashedLocation = [];
const nameToSource = await generateIconSourceMap(icon, location);
for (const [name, source] of Object.entries(nameToSource)) {
const ref = this.emitFile({ type: "asset", name, source });
const assetHashedName = this.getFileName(ref);
nameToAssetHashedLocation[name] = assetHashedName;
}
2022-07-19 19:46:36 +05:30
// Update icon section in output manifest with paths to the icon in build output
2022-07-04 16:42:56 +05:30
for (const [variable, location] of Object.entries(icon)) {
const [locationWithoutQueryParameters, queryParameters] = location.split("?");
const name = path.basename(locationWithoutQueryParameters);
const locationRelativeToBuildRoot = nameToAssetHashedLocation[name];
const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot);
icon[variable] = `${locationRelativeToManifest}?${queryParameters}`;
}
const runtimeThemeChunk = runtimeThemeChunkMap.get(location);
2022-06-20 20:35:06 +05:30
const runtimeAssetLocation = path.relative(manifestLocation, assetMap.get(runtimeThemeChunk.fileName).fileName);
2022-04-10 14:52:26 +05:30
manifest.source = {
2022-05-12 14:31:28 +05:30
"built-assets": builtAssets,
2022-06-20 20:35:06 +05:30
"runtime-asset": runtimeAssetLocation,
2022-04-10 14:52:26 +05:30
"derived-variables": derivedVariables,
2022-07-04 16:42:56 +05:30
"icon": icon,
2022-04-10 14:52:26 +05:30
};
const name = `theme-${themeKey}.json`;
2022-06-20 20:35:06 +05:30
manifestLocations.push(`${manifestLocation}/${name}`);
2022-04-10 14:52:26 +05:30
this.emitFile({
type: "asset",
name,
source: JSON.stringify(manifest),
});
}
2022-05-11 12:40:32 +05:30
addThemesToConfig(bundle, manifestLocations, defaultThemes);
2022-04-10 14:52:26 +05:30
},
2022-03-25 11:35:27 +05:30
}
}