Merge pull request #760 from vector-im/refactor-rollup-plugin

Refactor theme builder plugin
This commit is contained in:
R Midhun Suresh 2022-07-11 16:54:18 +05:30 committed by GitHub
commit c9bca52e82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 98 additions and 44 deletions

View file

@ -43,29 +43,15 @@ function addThemesToConfig(bundle, manifestLocations, defaultThemes) {
} }
} }
function parseBundle(bundle) { /**
* 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) {
const chunkMap = new Map(); const chunkMap = new Map();
const assetMap = new Map();
let runtimeThemeChunk;
for (const [fileName, info] of Object.entries(bundle)) { for (const [fileName, info] of Object.entries(bundle)) {
if (!fileName.endsWith(".css")) { if (!fileName.endsWith(".css") || info.type === "asset" || info.facadeModuleId?.includes("type=runtime")) {
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; continue;
} }
const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1]; const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1];
@ -80,7 +66,56 @@ function parseBundle(bundle) {
array.push(info); array.push(info);
} }
} }
return { chunkMap, assetMap, runtimeThemeChunk }; 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) {
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);
}
}
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") {
continue;
}
const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1];
if (!location) {
throw new Error("Cannot find location of css chunk!");
}
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);
}
}
return runtimeThemeChunkMap;
} }
module.exports = function buildThemes(options) { module.exports = function buildThemes(options) {
@ -88,6 +123,7 @@ module.exports = function buildThemes(options) {
let isDevelopment = false; let isDevelopment = false;
const virtualModuleId = '@theme/' const virtualModuleId = '@theme/'
const resolvedVirtualModuleId = '\0' + virtualModuleId; const resolvedVirtualModuleId = '\0' + virtualModuleId;
const themeToManifestLocation = new Map();
return { return {
name: "build-themes", name: "build-themes",
@ -102,20 +138,22 @@ module.exports = function buildThemes(options) {
async buildStart() { async buildStart() {
if (isDevelopment) { return; } if (isDevelopment) { return; }
const { themeConfig } = options; const { themeConfig } = options;
for (const [name, location] of Object.entries(themeConfig.themes)) { for (const location of themeConfig.themes) {
manifest = require(`${location}/manifest.json`); manifest = require(`${location}/manifest.json`);
const themeCollectionId = manifest.id;
themeToManifestLocation.set(themeCollectionId, location);
variants = manifest.values.variants; variants = manifest.values.variants;
for (const [variant, details] of Object.entries(variants)) { for (const [variant, details] of Object.entries(variants)) {
const fileName = `theme-${name}-${variant}.css`; const fileName = `theme-${themeCollectionId}-${variant}.css`;
if (name === themeConfig.default && details.default) { if (themeCollectionId === themeConfig.default && details.default) {
// This is the default theme, stash the file name for later // This is the default theme, stash the file name for later
if (details.dark) { if (details.dark) {
defaultDark = fileName; defaultDark = fileName;
defaultThemes["dark"] = `${name}-${variant}`; defaultThemes["dark"] = `${themeCollectionId}-${variant}`;
} }
else { else {
defaultLight = fileName; defaultLight = fileName;
defaultThemes["light"] = `${name}-${variant}`; defaultThemes["light"] = `${themeCollectionId}-${variant}`;
} }
} }
// emit the css as built theme bundle // emit the css as built theme bundle
@ -129,7 +167,7 @@ module.exports = function buildThemes(options) {
this.emitFile({ this.emitFile({
type: "chunk", type: "chunk",
id: `${location}/theme.css?type=runtime`, id: `${location}/theme.css?type=runtime`,
fileName: `theme-${name}-runtime.css`, fileName: `theme-${themeCollectionId}-runtime.css`,
}); });
} }
}, },
@ -152,7 +190,7 @@ module.exports = function buildThemes(options) {
if (theme === "default") { if (theme === "default") {
theme = options.themeConfig.default; theme = options.themeConfig.default;
} }
const location = options.themeConfig.themes[theme]; const location = themeToManifestLocation.get(theme);
const manifest = require(`${location}/manifest.json`); const manifest = require(`${location}/manifest.json`);
const variants = manifest.values.variants; const variants = manifest.values.variants;
if (!variant || variant === "default") { if (!variant || variant === "default") {
@ -246,29 +284,36 @@ module.exports = function buildThemes(options) {
}, },
generateBundle(_, bundle) { generateBundle(_, bundle) {
// assetMap: Mapping from asset-name (eg: element-dark.css) to AssetInfo const assetMap = getMappingFromFileNameToAssetInfo(bundle);
// chunkMap: Mapping from theme-location (eg: hydrogen-web/src/.../css/themes/element) to a list of ChunkInfo const chunkMap = getMappingFromLocationToChunkArray(bundle);
// types of AssetInfo and ChunkInfo can be found at https://rollupjs.org/guide/en/#generatebundle const runtimeThemeChunkMap = getMappingFromLocationToRuntimeChunk(bundle);
const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle);
const manifestLocations = []; const manifestLocations = [];
// Location of the directory containing manifest relative to the root of the build output
const manifestLocation = "assets";
for (const [location, chunkArray] of chunkMap) { for (const [location, chunkArray] of chunkMap) {
const manifest = require(`${location}/manifest.json`); const manifest = require(`${location}/manifest.json`);
const compiledVariables = options.compiledVariables.get(location); const compiledVariables = options.compiledVariables.get(location);
const derivedVariables = compiledVariables["derived-variables"]; const derivedVariables = compiledVariables["derived-variables"];
const icon = compiledVariables["icon"]; const icon = compiledVariables["icon"];
const builtAssets = {}; const builtAssets = {};
let themeKey;
for (const chunk of chunkArray) { for (const chunk of chunkArray) {
const [, name, variant] = chunk.fileName.match(/theme-(.+)-(.+)\.css/); const [, name, variant] = chunk.fileName.match(/theme-(.+)-(.+)\.css/);
builtAssets[`${name}-${variant}`] = assetMap.get(chunk.fileName).fileName; themeKey = name;
const locationRelativeToBuildRoot = assetMap.get(chunk.fileName).fileName;
const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot);
builtAssets[`${name}-${variant}`] = locationRelativeToManifest;
} }
const runtimeThemeChunk = runtimeThemeChunkMap.get(location);
const runtimeAssetLocation = path.relative(manifestLocation, assetMap.get(runtimeThemeChunk.fileName).fileName);
manifest.source = { manifest.source = {
"built-assets": builtAssets, "built-assets": builtAssets,
"runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName, "runtime-asset": runtimeAssetLocation,
"derived-variables": derivedVariables, "derived-variables": derivedVariables,
"icon": icon "icon": icon
}; };
const name = `theme-${manifest.name}.json`; const name = `theme-${themeKey}.json`;
manifestLocations.push(`assets/${name}`); manifestLocations.push(`${manifestLocation}/${name}`);
this.emitFile({ this.emitFile({
type: "asset", type: "asset",
name, name,

View file

@ -338,7 +338,7 @@ export class Platform {
document.querySelectorAll(".theme").forEach(e => e.remove()); document.querySelectorAll(".theme").forEach(e => e.remove());
// add new theme // add new theme
const styleTag = document.createElement("link"); const styleTag = document.createElement("link");
styleTag.href = `./${newPath}`; styleTag.href = newPath;
styleTag.rel = "stylesheet"; styleTag.rel = "stylesheet";
styleTag.type = "text/css"; styleTag.type = "text/css";
styleTag.className = "theme"; styleTag.className = "theme";

View file

@ -61,11 +61,11 @@ export class ThemeLoader {
const results = await Promise.all( const results = await Promise.all(
manifestLocations.map( location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) manifestLocations.map( location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response())
); );
results.forEach(({ body }) => this._populateThemeMap(body, log)); results.forEach(({ body }, i) => this._populateThemeMap(body, manifestLocations[i], log));
}); });
} }
private _populateThemeMap(manifest, log: ILogItem) { private _populateThemeMap(manifest, manifestLocation: string, log: ILogItem) {
log.wrap("populateThemeMap", (l) => { log.wrap("populateThemeMap", (l) => {
/* /*
After build has finished, the source section of each theme manifest After build has finished, the source section of each theme manifest
@ -75,7 +75,17 @@ export class ThemeLoader {
const builtAssets: Record<string, string> = manifest.source?.["built-assets"]; const builtAssets: Record<string, string> = manifest.source?.["built-assets"];
const themeName = manifest.name; const themeName = manifest.name;
let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; let defaultDarkVariant: any = {}, defaultLightVariant: any = {};
for (const [themeId, cssLocation] of Object.entries(builtAssets)) { for (let [themeId, cssLocation] of Object.entries(builtAssets)) {
try {
/**
* This cssLocation is relative to the location of the manifest file.
* So we first need to resolve it relative to the root of this hydrogen instance.
*/
cssLocation = new URL(cssLocation, new URL(manifestLocation, window.location.origin)).href;
}
catch {
continue;
}
const variant = themeId.match(/.+-(.+)/)?.[1]; const variant = themeId.match(/.+-(.+)/)?.[1];
const { name: variantName, default: isDefault, dark } = manifest.values.variants[variant!]; const { name: variantName, default: isDefault, dark } = manifest.values.variants[variant!];
const themeDisplayName = `${themeName} ${variantName}`; const themeDisplayName = `${themeName} ${variantName}`;

View file

@ -1,6 +1,7 @@
{ {
"version": 1, "version": 1,
"name": "Element", "name": "Element",
"id": "element",
"values": { "values": {
"variants": { "variants": {
"light": { "light": {

View file

@ -33,9 +33,7 @@ export default defineConfig(({mode}) => {
plugins: [ plugins: [
themeBuilder({ themeBuilder({
themeConfig: { themeConfig: {
themes: { themes: ["./src/platform/web/ui/css/themes/element"],
element: "./src/platform/web/ui/css/themes/element",
},
default: "element", default: "element",
}, },
compiledVariables, compiledVariables,