forked from mystiq/hydrogen-web
Merge pull request #760 from vector-im/refactor-rollup-plugin
Refactor theme builder plugin
This commit is contained in:
commit
c9bca52e82
5 changed files with 98 additions and 44 deletions
|
@ -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,
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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}`;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"name": "Element",
|
"name": "Element",
|
||||||
|
"id": "element",
|
||||||
"values": {
|
"values": {
|
||||||
"variants": {
|
"variants": {
|
||||||
"light": {
|
"light": {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue