|
| 1 | +const path = require('path'); |
| 2 | +const fs = require('fs'); |
| 3 | + |
| 4 | +/** |
| 5 | + * Retrieves the name of the brand package from the given directory. |
| 6 | + * |
| 7 | + * @param {string} dir - The directory path containing the package.json file. |
| 8 | + * @return {string} The name of the brand package, or an empty string if not found. |
| 9 | + */ |
| 10 | +function getBrandPackageName(dir) { |
| 11 | + const appDependencies = JSON.parse(fs.readFileSync(path.resolve(dir, 'package.json'), 'utf-8')).dependencies; |
| 12 | + return Object.keys(appDependencies).find((key) => key.match(/@(open)?edx\/brand/)) || ''; |
| 13 | +} |
| 14 | + |
| 15 | +/** |
| 16 | + * Attempts to extract the Paragon version from the `node_modules` of |
| 17 | + * the consuming application. |
| 18 | + * |
| 19 | + * @param {string} dir Path to directory containing `node_modules`. |
| 20 | + * @returns {string} Paragon dependency version of the consuming application |
| 21 | + */ |
| 22 | +function getParagonVersion(dir, { isBrandOverride = false } = {}) { |
| 23 | + const npmPackageName = isBrandOverride ? getBrandPackageName(dir) : '@openedx/paragon'; |
| 24 | + const pathToPackageJson = `${dir}/node_modules/${npmPackageName}/package.json`; |
| 25 | + if (!fs.existsSync(pathToPackageJson)) { |
| 26 | + return undefined; |
| 27 | + } |
| 28 | + return JSON.parse(fs.readFileSync(pathToPackageJson, 'utf-8')).version; |
| 29 | +} |
| 30 | + |
| 31 | +/** |
| 32 | + * @typedef {Object} ParagonThemeCssAsset |
| 33 | + * @property {string} filePath |
| 34 | + * @property {string} entryName |
| 35 | + * @property {string} outputChunkName |
| 36 | + */ |
| 37 | + |
| 38 | +/** |
| 39 | + * @typedef {Object} ParagonThemeVariantCssAsset |
| 40 | + * @property {string} filePath |
| 41 | + * @property {string} entryName |
| 42 | + * @property {string} outputChunkName |
| 43 | + */ |
| 44 | + |
| 45 | +/** |
| 46 | + * @typedef {Object} ParagonThemeCss |
| 47 | + * @property {ParagonThemeCssAsset} core The metadata about the core Paragon theme CSS |
| 48 | + * @property {Object.<string, ParagonThemeVariantCssAsset>} variants A collection of theme variants. |
| 49 | + */ |
| 50 | + |
| 51 | +/** |
| 52 | + * Attempts to extract the Paragon theme CSS from the locally installed `@openedx/paragon` package. |
| 53 | + * @param {string} dir Path to directory containing `node_modules`. |
| 54 | + * @param {boolean} isBrandOverride |
| 55 | + * @returns {ParagonThemeCss} |
| 56 | + */ |
| 57 | +function getParagonThemeCss(dir, { isBrandOverride = false } = {}) { |
| 58 | + const npmPackageName = isBrandOverride ? getBrandPackageName(dir) : '@openedx/paragon'; |
| 59 | + const pathToParagonThemeOutput = path.resolve(dir, 'node_modules', npmPackageName, 'dist', 'theme-urls.json'); |
| 60 | + |
| 61 | + if (!fs.existsSync(pathToParagonThemeOutput)) { |
| 62 | + return undefined; |
| 63 | + } |
| 64 | + const paragonConfig = JSON.parse(fs.readFileSync(pathToParagonThemeOutput, 'utf-8')); |
| 65 | + const { |
| 66 | + core: themeCore, |
| 67 | + variants: themeVariants, |
| 68 | + defaults, |
| 69 | + } = paragonConfig?.themeUrls || {}; |
| 70 | + |
| 71 | + const pathToCoreCss = path.resolve(dir, 'node_modules', npmPackageName, 'dist', themeCore.paths.minified); |
| 72 | + const coreCssExists = fs.existsSync(pathToCoreCss); |
| 73 | + |
| 74 | + const themeVariantResults = Object.entries(themeVariants || {}).reduce((themeVariantAcc, [themeVariant, value]) => { |
| 75 | + const themeVariantCssDefault = path.resolve(dir, 'node_modules', npmPackageName, 'dist', value.paths.default); |
| 76 | + const themeVariantCssMinified = path.resolve(dir, 'node_modules', npmPackageName, 'dist', value.paths.minified); |
| 77 | + |
| 78 | + if (!fs.existsSync(themeVariantCssDefault) && !fs.existsSync(themeVariantCssMinified)) { |
| 79 | + return themeVariantAcc; |
| 80 | + } |
| 81 | + |
| 82 | + return ({ |
| 83 | + ...themeVariantAcc, |
| 84 | + [themeVariant]: { |
| 85 | + filePath: themeVariantCssMinified, |
| 86 | + entryName: isBrandOverride ? `brand.theme.variants.${themeVariant}` : `paragon.theme.variants.${themeVariant}`, |
| 87 | + outputChunkName: isBrandOverride ? `brand-theme-variants-${themeVariant}` : `paragon-theme-variants-${themeVariant}`, |
| 88 | + }, |
| 89 | + }); |
| 90 | + }, {}); |
| 91 | + |
| 92 | + if (!coreCssExists || themeVariantResults.length === 0) { |
| 93 | + return undefined; |
| 94 | + } |
| 95 | + |
| 96 | + const coreResult = { |
| 97 | + filePath: path.resolve(dir, pathToCoreCss), |
| 98 | + entryName: isBrandOverride ? 'brand.theme.core' : 'paragon.theme.core', |
| 99 | + outputChunkName: isBrandOverride ? 'brand-theme-core' : 'paragon-theme-core', |
| 100 | + }; |
| 101 | + |
| 102 | + return { |
| 103 | + core: fs.existsSync(pathToCoreCss) ? coreResult : undefined, |
| 104 | + variants: themeVariantResults, |
| 105 | + defaults, |
| 106 | + }; |
| 107 | +} |
| 108 | + |
| 109 | +/** |
| 110 | + * @typedef CacheGroup |
| 111 | + * @property {string} type The type of cache group. |
| 112 | + * @property {string|function} name The name of the cache group. |
| 113 | + * @property {function} chunks A function that returns true if the chunk should be included in the cache group. |
| 114 | + * @property {boolean} enforce If true, this cache group will be created even if it conflicts with default cache groups. |
| 115 | + */ |
| 116 | + |
| 117 | +/** |
| 118 | + * @param {ParagonThemeCss} paragonThemeCss The Paragon theme CSS metadata. |
| 119 | + * @returns {Object.<string, CacheGroup>} The cache groups for the Paragon theme CSS. |
| 120 | + */ |
| 121 | +function getParagonCacheGroups(paragonThemeCss) { |
| 122 | + if (!paragonThemeCss) { |
| 123 | + return {}; |
| 124 | + } |
| 125 | + const cacheGroups = { |
| 126 | + [paragonThemeCss.core.outputChunkName]: { |
| 127 | + type: 'css/mini-extract', |
| 128 | + name: paragonThemeCss.core.outputChunkName, |
| 129 | + chunks: chunk => chunk.name === paragonThemeCss.core.entryName, |
| 130 | + enforce: true, |
| 131 | + }, |
| 132 | + }; |
| 133 | + |
| 134 | + Object.values(paragonThemeCss.variants).forEach(({ entryName, outputChunkName }) => { |
| 135 | + cacheGroups[outputChunkName] = { |
| 136 | + type: 'css/mini-extract', |
| 137 | + name: outputChunkName, |
| 138 | + chunks: chunk => chunk.name === entryName, |
| 139 | + enforce: true, |
| 140 | + }; |
| 141 | + }); |
| 142 | + return cacheGroups; |
| 143 | +} |
| 144 | + |
| 145 | +/** |
| 146 | + * @param {ParagonThemeCss} paragonThemeCss The Paragon theme CSS metadata. |
| 147 | + * @returns {Object.<string, string>} The entry points for the Paragon theme CSS. Example: ``` |
| 148 | + * { |
| 149 | + * "paragon.theme.core": "/path/to/node_modules/@openedx/paragon/dist/core.min.css", |
| 150 | + * "paragon.theme.variants.light": "/path/to/node_modules/@openedx/paragon/dist/light.min.css" |
| 151 | + * } |
| 152 | + * ``` |
| 153 | + */ |
| 154 | +function getParagonEntryPoints(paragonThemeCss) { |
| 155 | + if (!paragonThemeCss) { |
| 156 | + return {}; |
| 157 | + } |
| 158 | + |
| 159 | + const entryPoints = { [paragonThemeCss.core.entryName]: path.resolve(process.cwd(), paragonThemeCss.core.filePath) }; |
| 160 | + Object.values(paragonThemeCss.variants).forEach(({ filePath, entryName }) => { |
| 161 | + entryPoints[entryName] = path.resolve(process.cwd(), filePath); |
| 162 | + }); |
| 163 | + return entryPoints; |
| 164 | +} |
| 165 | + |
| 166 | +module.exports = { |
| 167 | + getParagonVersion, |
| 168 | + getParagonThemeCss, |
| 169 | + getParagonCacheGroups, |
| 170 | + getParagonEntryPoints, |
| 171 | +}; |
0 commit comments