From ff98ef44655f4c0c7dbf74c0537b289e0979ef36 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 10 Apr 2022 14:49:19 +0530 Subject: [PATCH 01/31] Support theming in dev server --- .../rollup-plugin-build-themes.js | 198 ++++++++++++------ 1 file changed, 137 insertions(+), 61 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 74fe4daf..29fd91e1 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -13,17 +13,45 @@ 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 resolvedLocation = path.resolve(__dirname, "../../", `${location}/theme.css`); const data = await fs.readFile(resolvedLocation); return data; } -async function appendVariablesToCSS(variables, cssSource) { - return cssSource + `:root{\n${Object.entries(variables).reduce((acc, [key, value]) => acc + `--${key}: ${value};\n`, "")} }\n\n`; +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 findLocationFromThemeName(name, locations) { + const themeLocation = locations.find(location => { + const manifest = require(`${location}/manifest.json`); + if (manifest.name === name) { + return true; + } + }); + if (!themeLocation) { + throw new Error(`Cannot find location from theme name "${name}"`); + } + return themeLocation; +} + +function findManifestFromThemeName(name, locations) { + for (const location of locations) { + const manifest = require(`${location}/manifest.json`); + if (manifest.name === name) { + return manifest; + } + } + throw new Error(`Cannot find manifest from theme name "${name}"`); } function parseBundle(bundle) { @@ -68,11 +96,20 @@ function parseBundle(bundle) { 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() { const { manifestLocations } = options; for (const location of manifestLocations) { @@ -106,69 +143,108 @@ module.exports = function buildThemes(options) { } }, + resolveId(id) { + if (id.startsWith(virtualModuleId)) { + return isDevelopment? '\0' + id: false; + } + }, + async load(id) { - const result = id.match(/(.+)\/theme.css\?variant=(.+)/); - if (result) { - const [, location, variant] = result; - const cssSource = await readCSSSource(location); - const config = variants[variant]; - return await appendVariablesToCSS(config.variables, cssSource); - } - return null; - }, - - transformIndexHtml(_, ctx) { - let darkThemeLocation, lightThemeLocation; - for (const [, bundle] of Object.entries(ctx.bundle)) { - if (bundle.name === defaultDark) { - darkThemeLocation = bundle.fileName; - } - if (bundle.name === defaultLight) { - lightThemeLocation = bundle.fileName; + if (isDevelopment) { + if (id.startsWith(resolvedVirtualModuleId)) { + let [theme, variant, file] = id.substr(resolvedVirtualModuleId.length).split("/"); + if (theme === "default") { + theme = "Element"; + } + if (!variant || variant === "default") { + variant = "light"; + } + if (!file) { + file = "index.js"; + } + switch (file) { + case "index.js": { + const location = findLocationFromThemeName(theme, options.manifestLocations); + return `import "${path.resolve(`${location}/theme.css`)}";` + + `import "@theme/${theme}/${variant}/variables.css"`; + } + case "variables.css": { + const manifest = findManifestFromThemeName(theme, options.manifestLocations); + const variables = manifest.values.variants[variant].variables; + const css = getRootSectionWithVariables(variables); + return css; + } + } } } - return [ - { - tag: "link", - attrs: { - rel: "stylesheet", - type: "text/css", - media: "(prefers-color-scheme: dark)", - href: `./${darkThemeLocation}`, - } - }, - { - tag: "link", - attrs: { - rel: "stylesheet", - type: "text/css", - media: "(prefers-color-scheme: light)", - href: `./${lightThemeLocation}`, - } - }, - ]; - }, + else { + const result = id.match(/(.+)\/theme.css\?variant=(.+)/); + if (result) { + const [, location, variant] = result; + const cssSource = await readCSSSource(location); + const config = variants[variant]; + return await appendVariablesToCSS(config.variables, cssSource); + } + return null; + } +}, - generateBundle(_, bundle) { - const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle); - 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"]; - manifest.source = { - "built-asset": chunkArray.map(chunk => assetMap.get(chunk.fileName).fileName), - "runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName, - "derived-variables": derivedVariables, - "icon": icon - }; - const name = `theme-${manifest.name}.json`; - this.emitFile({ - type: "asset", - name, - source: JSON.stringify(manifest), - }); + 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; + } + if (bundle.name === defaultLight) { + lightThemeLocation = bundle.fileName; } } + return [ + { + tag: "link", + attrs: { + rel: "stylesheet", + type: "text/css", + media: "(prefers-color-scheme: dark)", + href: `./${darkThemeLocation}`, + } + }, + { + tag: "link", + attrs: { + rel: "stylesheet", + type: "text/css", + media: "(prefers-color-scheme: light)", + href: `./${lightThemeLocation}`, + } + }, + ]; +}, + +generateBundle(_, bundle) { + const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle); + 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"]; + manifest.source = { + "built-asset": chunkArray.map(chunk => assetMap.get(chunk.fileName).fileName), + "runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName, + "derived-variables": derivedVariables, + "icon": icon + }; + const name = `theme-${manifest.name}.json`; + this.emitFile({ + type: "asset", + name, + source: JSON.stringify(manifest), + }); + } +} } } From 0a95eb09405816c0cbd73883eb8b06e8c5d48e9f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 10 Apr 2022 14:52:26 +0530 Subject: [PATCH 02/31] Fix formatting --- .../rollup-plugin-build-themes.js | 110 +++++++++--------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 29fd91e1..eb063c72 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -187,64 +187,64 @@ module.exports = function buildThemes(options) { } return null; } -}, + }, - 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; - } - if (bundle.name === defaultLight) { - lightThemeLocation = bundle.fileName; - } - } - return [ - { - tag: "link", - attrs: { - rel: "stylesheet", - type: "text/css", - media: "(prefers-color-scheme: dark)", - href: `./${darkThemeLocation}`, + 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; } - }, - { - tag: "link", - attrs: { - rel: "stylesheet", - type: "text/css", - media: "(prefers-color-scheme: light)", - href: `./${lightThemeLocation}`, + if (bundle.name === defaultLight) { + lightThemeLocation = bundle.fileName; } - }, - ]; -}, + } + return [ + { + tag: "link", + attrs: { + rel: "stylesheet", + type: "text/css", + media: "(prefers-color-scheme: dark)", + href: `./${darkThemeLocation}`, + } + }, + { + tag: "link", + attrs: { + rel: "stylesheet", + type: "text/css", + media: "(prefers-color-scheme: light)", + href: `./${lightThemeLocation}`, + } + }, + ]; + }, -generateBundle(_, bundle) { - const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle); - 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"]; - manifest.source = { - "built-asset": chunkArray.map(chunk => assetMap.get(chunk.fileName).fileName), - "runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName, - "derived-variables": derivedVariables, - "icon": icon - }; - const name = `theme-${manifest.name}.json`; - this.emitFile({ - type: "asset", - name, - source: JSON.stringify(manifest), - }); - } -} + generateBundle(_, bundle) { + const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle); + 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"]; + manifest.source = { + "built-asset": chunkArray.map(chunk => assetMap.get(chunk.fileName).fileName), + "runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName, + "derived-variables": derivedVariables, + "icon": icon + }; + const name = `theme-${manifest.name}.json`; + this.emitFile({ + type: "asset", + name, + source: JSON.stringify(manifest), + }); + } + }, } } From 49535807bf1b60c3e8d4dbabacd6108c80e98d04 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 10 Apr 2022 14:59:08 +0530 Subject: [PATCH 03/31] Do not run plugin on runtime theme --- scripts/postcss/css-url-processor.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/postcss/css-url-processor.js b/scripts/postcss/css-url-processor.js index 1a0e4fdf..c050a898 100644 --- a/scripts/postcss/css-url-processor.js +++ b/scripts/postcss/css-url-processor.js @@ -68,6 +68,11 @@ module.exports = (opts = {}) => { postcssPlugin: "postcss-url-to-variable", Once(root, {result}) { + const cssFileLocation = root.source.input.from; + if (cssFileLocation.includes("type=runtime")) { + // If this is a runtime theme, don't process urls. + return; + } /* postcss-compile-variables should have sent the list of resolved colours down via results */ From 6456d4ef7627ec51f1a8e3bdce618e0e9ffc21d3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 10 Apr 2022 14:59:42 +0530 Subject: [PATCH 04/31] Cache cssPath --- scripts/postcss/css-url-processor.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/postcss/css-url-processor.js b/scripts/postcss/css-url-processor.js index c050a898..3ae7c60d 100644 --- a/scripts/postcss/css-url-processor.js +++ b/scripts/postcss/css-url-processor.js @@ -16,6 +16,7 @@ limitations under the License. const valueParser = require("postcss-value-parser"); const resolve = require("path").resolve; +let cssPath; function colorsFromURL(url, colorMap) { const params = new URL(`file://${url}`).searchParams; @@ -44,7 +45,6 @@ function processURL(decl, replacer, colorMap) { } const urlStringNode = node.nodes[0]; const oldURL = urlStringNode.value; - const cssPath = decl.source?.input.file.replace(/[^/]*$/, ""); const oldURLAbsolute = resolve(cssPath, oldURL); const colors = colorsFromURL(oldURLAbsolute, colorMap); if (!colors) { @@ -84,6 +84,7 @@ module.exports = (opts = {}) => { Go through each declaration and if it contains an URL, replace the url with the result of running replacer(url) */ + cssPath = root.source?.input.file.replace(/[^/]*$/, ""); root.walkDecls(decl => processURL(decl, opts.replacer, colorMap)); }, }; From 36782fb4feb83d31f57567f12eb175325608ee7f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 12 Apr 2022 19:44:29 +0530 Subject: [PATCH 05/31] Use unique filenames Otherwise newly produced svgs will replace other svgs produced earlier in the build. --- scripts/postcss/svg-colorizer.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/postcss/svg-colorizer.js b/scripts/postcss/svg-colorizer.js index 7d527ddb..a3110693 100644 --- a/scripts/postcss/svg-colorizer.js +++ b/scripts/postcss/svg-colorizer.js @@ -16,6 +16,7 @@ limitations under the License. const fs = require("fs"); const path = require("path"); +const {randomUUID} = require('crypto'); /** * Builds a new svg with the colors replaced and returns its location. * @param {string} svgLocation The location of the input svg file @@ -30,6 +31,8 @@ module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondar throw new Error("svg-colorizer made no color replacements! The input svg should only contain colors #ff00ff (primary, case-sensitive) and #00ffff (secondary, case-sensitive)."); } const fileName = svgLocation.match(/.+\/(.+\.svg)/)[1]; + // give unique names so that this svg does not replace other versions of the same svg + const outputName = `${fileName.substring(0, fileName.length - 4)}-${randomUUID()}.svg`; const outputPath = path.resolve(__dirname, "../../.tmp"); try { fs.mkdirSync(outputPath); @@ -39,7 +42,7 @@ module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondar throw e; } } - const outputFile = `${outputPath}/${fileName}`; + const outputFile = `${outputPath}/${outputName}`; fs.writeFileSync(outputFile, coloredSVGCode); return outputFile; } From 25a8521efcd7751db4ae8b01c60a21b5f5bf70ea Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 12 Apr 2022 20:15:14 +0530 Subject: [PATCH 06/31] Use hash instead of UUID --- scripts/postcss/svg-colorizer.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/postcss/svg-colorizer.js b/scripts/postcss/svg-colorizer.js index a3110693..95355ea8 100644 --- a/scripts/postcss/svg-colorizer.js +++ b/scripts/postcss/svg-colorizer.js @@ -16,7 +16,14 @@ limitations under the License. const fs = require("fs"); const path = require("path"); -const {randomUUID} = require('crypto'); +const xxhash = require('xxhashjs'); + +function createHash(content) { + const hasher = new xxhash.h32(0); + hasher.update(content); + return hasher.digest(); +} + /** * Builds a new svg with the colors replaced and returns its location. * @param {string} svgLocation The location of the input svg file @@ -31,8 +38,7 @@ module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondar throw new Error("svg-colorizer made no color replacements! The input svg should only contain colors #ff00ff (primary, case-sensitive) and #00ffff (secondary, case-sensitive)."); } const fileName = svgLocation.match(/.+\/(.+\.svg)/)[1]; - // give unique names so that this svg does not replace other versions of the same svg - const outputName = `${fileName.substring(0, fileName.length - 4)}-${randomUUID()}.svg`; + const outputName = `${fileName.substring(0, fileName.length - 4)}-${createHash(coloredSVGCode)}.svg`; const outputPath = path.resolve(__dirname, "../../.tmp"); try { fs.mkdirSync(outputPath); From 743bd0db1c6496e359561630230d62fd64305046 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 12 Apr 2022 20:39:04 +0530 Subject: [PATCH 07/31] Support dark mode and remove dev script tag --- .../rollup-plugin-build-themes.js | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index eb063c72..14d306e8 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -111,6 +111,7 @@ module.exports = function buildThemes(options) { }, async buildStart() { + if (isDevelopment) { return; } const { manifestLocations } = options; for (const location of manifestLocations) { manifest = require(`${location}/manifest.json`); @@ -130,7 +131,7 @@ module.exports = function buildThemes(options) { // emit the css as built theme bundle this.emitFile({ type: "chunk", - id: `${location}/theme.css?variant=${variant}`, + id: `${location}/theme.css?variant=${variant}${details.dark? "&dark=true": ""}`, fileName, }); } @@ -145,7 +146,7 @@ module.exports = function buildThemes(options) { resolveId(id) { if (id.startsWith(virtualModuleId)) { - return isDevelopment? '\0' + id: false; + return '\0' + id; } }, @@ -165,7 +166,9 @@ module.exports = function buildThemes(options) { switch (file) { case "index.js": { const location = findLocationFromThemeName(theme, options.manifestLocations); - return `import "${path.resolve(`${location}/theme.css`)}";` + + const manifest = findManifestFromThemeName(theme, options.manifestLocations); + const isDark = manifest.values.variants[variant].dark; + return `import "${path.resolve(`${location}/theme.css`)}${isDark? "?dark=true": ""}";` + `import "@theme/${theme}/${variant}/variables.css"`; } case "variables.css": { @@ -178,7 +181,7 @@ module.exports = function buildThemes(options) { } } else { - const result = id.match(/(.+)\/theme.css\?variant=(.+)/); + const result = id.match(/(.+)\/theme.css\?variant=([^&]+)/); if (result) { const [, location, variant] = result; const cssSource = await readCSSSource(location); @@ -189,6 +192,18 @@ module.exports = function buildThemes(options) { } }, + transform(code, id) { + if (isDevelopment) { + return; + } + // Removes develop-only script tag; this cannot be done in transformIndexHtml hook. + const devScriptTag = /