diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 43a21623..6b8fdae0 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -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 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; + if (!fileName.endsWith(".css") || info.type === "asset" || info.facadeModuleId?.includes("type=runtime")) { continue; } const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1]; @@ -80,7 +66,56 @@ function parseBundle(bundle) { 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) { @@ -88,6 +123,7 @@ module.exports = function buildThemes(options) { let isDevelopment = false; const virtualModuleId = '@theme/' const resolvedVirtualModuleId = '\0' + virtualModuleId; + const themeToManifestLocation = new Map(); return { name: "build-themes", @@ -102,20 +138,22 @@ module.exports = function buildThemes(options) { async buildStart() { if (isDevelopment) { return; } const { themeConfig } = options; - for (const [name, location] of Object.entries(themeConfig.themes)) { + for (const location of themeConfig.themes) { manifest = require(`${location}/manifest.json`); + const themeCollectionId = manifest.id; + themeToManifestLocation.set(themeCollectionId, location); variants = manifest.values.variants; for (const [variant, details] of Object.entries(variants)) { - const fileName = `theme-${name}-${variant}.css`; - if (name === themeConfig.default && details.default) { + const fileName = `theme-${themeCollectionId}-${variant}.css`; + if (themeCollectionId === themeConfig.default && details.default) { // This is the default theme, stash the file name for later if (details.dark) { defaultDark = fileName; - defaultThemes["dark"] = `${name}-${variant}`; + defaultThemes["dark"] = `${themeCollectionId}-${variant}`; } else { defaultLight = fileName; - defaultThemes["light"] = `${name}-${variant}`; + defaultThemes["light"] = `${themeCollectionId}-${variant}`; } } // emit the css as built theme bundle @@ -129,7 +167,7 @@ module.exports = function buildThemes(options) { this.emitFile({ type: "chunk", 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") { theme = options.themeConfig.default; } - const location = options.themeConfig.themes[theme]; + const location = themeToManifestLocation.get(theme); const manifest = require(`${location}/manifest.json`); const variants = manifest.values.variants; if (!variant || variant === "default") { @@ -246,29 +284,36 @@ module.exports = function buildThemes(options) { }, generateBundle(_, bundle) { - // assetMap: Mapping from asset-name (eg: element-dark.css) to AssetInfo - // chunkMap: Mapping from theme-location (eg: hydrogen-web/src/.../css/themes/element) to a list of ChunkInfo - // types of AssetInfo and ChunkInfo can be found at https://rollupjs.org/guide/en/#generatebundle - const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle); + const assetMap = getMappingFromFileNameToAssetInfo(bundle); + const chunkMap = getMappingFromLocationToChunkArray(bundle); + const runtimeThemeChunkMap = getMappingFromLocationToRuntimeChunk(bundle); 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) { const manifest = require(`${location}/manifest.json`); const compiledVariables = options.compiledVariables.get(location); const derivedVariables = compiledVariables["derived-variables"]; const icon = compiledVariables["icon"]; const builtAssets = {}; + let themeKey; for (const chunk of chunkArray) { 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 = { "built-assets": builtAssets, - "runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName, + "runtime-asset": runtimeAssetLocation, "derived-variables": derivedVariables, "icon": icon }; - const name = `theme-${manifest.name}.json`; - manifestLocations.push(`assets/${name}`); + const name = `theme-${themeKey}.json`; + manifestLocations.push(`${manifestLocation}/${name}`); this.emitFile({ type: "asset", name, diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index e2ca2028..c2eef17e 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -338,7 +338,7 @@ export class Platform { document.querySelectorAll(".theme").forEach(e => e.remove()); // add new theme const styleTag = document.createElement("link"); - styleTag.href = `./${newPath}`; + styleTag.href = newPath; styleTag.rel = "stylesheet"; styleTag.type = "text/css"; styleTag.className = "theme"; diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index 8c9364bc..89430663 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -61,11 +61,11 @@ export class ThemeLoader { const results = await Promise.all( 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) => { /* After build has finished, the source section of each theme manifest @@ -75,7 +75,17 @@ export class ThemeLoader { const builtAssets: Record = manifest.source?.["built-assets"]; const themeName = manifest.name; 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 { name: variantName, default: isDefault, dark } = manifest.values.variants[variant!]; const themeDisplayName = `${themeName} ${variantName}`; diff --git a/src/platform/web/ui/css/themes/element/manifest.json b/src/platform/web/ui/css/themes/element/manifest.json index e183317c..cb21eaad 100644 --- a/src/platform/web/ui/css/themes/element/manifest.json +++ b/src/platform/web/ui/css/themes/element/manifest.json @@ -1,6 +1,7 @@ { "version": 1, "name": "Element", + "id": "element", "values": { "variants": { "light": { diff --git a/vite.config.js b/vite.config.js index 72be0184..10348218 100644 --- a/vite.config.js +++ b/vite.config.js @@ -33,9 +33,7 @@ export default defineConfig(({mode}) => { plugins: [ themeBuilder({ themeConfig: { - themes: { - element: "./src/platform/web/ui/css/themes/element", - }, + themes: ["./src/platform/web/ui/css/themes/element"], default: "element", }, compiledVariables,