-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'openedx-frontend-build/master'
# Conflicts: # .npmignore # README.md # catalog-info.yaml # config/.eslintrc.js # config/jest.config.js # config/jest/setupTest.js # config/webpack.common.config.js # config/webpack.dev-stage.config.js # example/package-lock.json # example/package.json # example/src/App.jsx # example/src/__snapshots__/App.test.jsx.snap # example/src/apple.jpg # package-lock.json # package.json # test-project/src/ParagonPreview.jsx # tools/webpack/webpack.build.config.ts # tools/webpack/webpack.dev.config.ts
- Loading branch information
Showing
18 changed files
with
1,161 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
const path = require('path'); | ||
const fs = require('fs'); | ||
|
||
/** | ||
* Retrieves the name of the brand package from the given directory. | ||
* | ||
* @param {string} dir - The directory path containing the package.json file. | ||
* @return {string} The name of the brand package, or an empty string if not found. | ||
*/ | ||
function getBrandPackageName(dir) { | ||
const appDependencies = JSON.parse(fs.readFileSync(path.resolve(dir, 'package.json'), 'utf-8')).dependencies; | ||
return Object.keys(appDependencies).find((key) => key.match(/@(open)?edx\/brand/)) || ''; | ||
} | ||
|
||
/** | ||
* Attempts to extract the Paragon version from the `node_modules` of | ||
* the consuming application. | ||
* | ||
* @param {string} dir Path to directory containing `node_modules`. | ||
* @returns {string} Paragon dependency version of the consuming application | ||
*/ | ||
function getParagonVersion(dir, { isBrandOverride = false } = {}) { | ||
const npmPackageName = isBrandOverride ? getBrandPackageName(dir) : '@openedx/paragon'; | ||
const pathToPackageJson = `${dir}/node_modules/${npmPackageName}/package.json`; | ||
if (!fs.existsSync(pathToPackageJson)) { | ||
return undefined; | ||
} | ||
return JSON.parse(fs.readFileSync(pathToPackageJson, 'utf-8')).version; | ||
} | ||
|
||
/** | ||
* @typedef {Object} ParagonThemeCssAsset | ||
* @property {string} filePath | ||
* @property {string} entryName | ||
* @property {string} outputChunkName | ||
*/ | ||
|
||
/** | ||
* @typedef {Object} ParagonThemeVariantCssAsset | ||
* @property {string} filePath | ||
* @property {string} entryName | ||
* @property {string} outputChunkName | ||
*/ | ||
|
||
/** | ||
* @typedef {Object} ParagonThemeCss | ||
* @property {ParagonThemeCssAsset} core The metadata about the core Paragon theme CSS | ||
* @property {Object.<string, ParagonThemeVariantCssAsset>} variants A collection of theme variants. | ||
*/ | ||
|
||
/** | ||
* Attempts to extract the Paragon theme CSS from the locally installed `@openedx/paragon` package. | ||
* @param {string} dir Path to directory containing `node_modules`. | ||
* @param {boolean} isBrandOverride | ||
* @returns {ParagonThemeCss} | ||
*/ | ||
function getParagonThemeCss(dir, { isBrandOverride = false } = {}) { | ||
const npmPackageName = isBrandOverride ? getBrandPackageName(dir) : '@openedx/paragon'; | ||
const pathToParagonThemeOutput = path.resolve(dir, 'node_modules', npmPackageName, 'dist', 'theme-urls.json'); | ||
|
||
if (!fs.existsSync(pathToParagonThemeOutput)) { | ||
return undefined; | ||
} | ||
const paragonConfig = JSON.parse(fs.readFileSync(pathToParagonThemeOutput, 'utf-8')); | ||
const { | ||
core: themeCore, | ||
variants: themeVariants, | ||
defaults, | ||
} = paragonConfig?.themeUrls || {}; | ||
|
||
const pathToCoreCss = path.resolve(dir, 'node_modules', npmPackageName, 'dist', themeCore.paths.minified); | ||
const coreCssExists = fs.existsSync(pathToCoreCss); | ||
|
||
const themeVariantResults = Object.entries(themeVariants || {}).reduce((themeVariantAcc, [themeVariant, value]) => { | ||
const themeVariantCssDefault = path.resolve(dir, 'node_modules', npmPackageName, 'dist', value.paths.default); | ||
const themeVariantCssMinified = path.resolve(dir, 'node_modules', npmPackageName, 'dist', value.paths.minified); | ||
|
||
if (!fs.existsSync(themeVariantCssDefault) && !fs.existsSync(themeVariantCssMinified)) { | ||
return themeVariantAcc; | ||
} | ||
|
||
return ({ | ||
...themeVariantAcc, | ||
[themeVariant]: { | ||
filePath: themeVariantCssMinified, | ||
entryName: isBrandOverride ? `brand.theme.variants.${themeVariant}` : `paragon.theme.variants.${themeVariant}`, | ||
outputChunkName: isBrandOverride ? `brand-theme-variants-${themeVariant}` : `paragon-theme-variants-${themeVariant}`, | ||
}, | ||
}); | ||
}, {}); | ||
|
||
if (!coreCssExists || themeVariantResults.length === 0) { | ||
return undefined; | ||
} | ||
|
||
const coreResult = { | ||
filePath: path.resolve(dir, pathToCoreCss), | ||
entryName: isBrandOverride ? 'brand.theme.core' : 'paragon.theme.core', | ||
outputChunkName: isBrandOverride ? 'brand-theme-core' : 'paragon-theme-core', | ||
}; | ||
|
||
return { | ||
core: fs.existsSync(pathToCoreCss) ? coreResult : undefined, | ||
variants: themeVariantResults, | ||
defaults, | ||
}; | ||
} | ||
|
||
/** | ||
* @typedef CacheGroup | ||
* @property {string} type The type of cache group. | ||
* @property {string|function} name The name of the cache group. | ||
* @property {function} chunks A function that returns true if the chunk should be included in the cache group. | ||
* @property {boolean} enforce If true, this cache group will be created even if it conflicts with default cache groups. | ||
*/ | ||
|
||
/** | ||
* @param {ParagonThemeCss} paragonThemeCss The Paragon theme CSS metadata. | ||
* @returns {Object.<string, CacheGroup>} The cache groups for the Paragon theme CSS. | ||
*/ | ||
function getParagonCacheGroups(paragonThemeCss) { | ||
if (!paragonThemeCss) { | ||
return {}; | ||
} | ||
const cacheGroups = { | ||
[paragonThemeCss.core.outputChunkName]: { | ||
type: 'css/mini-extract', | ||
name: paragonThemeCss.core.outputChunkName, | ||
chunks: chunk => chunk.name === paragonThemeCss.core.entryName, | ||
enforce: true, | ||
}, | ||
}; | ||
|
||
Object.values(paragonThemeCss.variants).forEach(({ entryName, outputChunkName }) => { | ||
cacheGroups[outputChunkName] = { | ||
type: 'css/mini-extract', | ||
name: outputChunkName, | ||
chunks: chunk => chunk.name === entryName, | ||
enforce: true, | ||
}; | ||
}); | ||
return cacheGroups; | ||
} | ||
|
||
/** | ||
* @param {ParagonThemeCss} paragonThemeCss The Paragon theme CSS metadata. | ||
* @returns {Object.<string, string>} The entry points for the Paragon theme CSS. Example: ``` | ||
* { | ||
* "paragon.theme.core": "/path/to/node_modules/@openedx/paragon/dist/core.min.css", | ||
* "paragon.theme.variants.light": "/path/to/node_modules/@openedx/paragon/dist/light.min.css" | ||
* } | ||
* ``` | ||
*/ | ||
function getParagonEntryPoints(paragonThemeCss) { | ||
if (!paragonThemeCss) { | ||
return {}; | ||
} | ||
|
||
const entryPoints = { [paragonThemeCss.core.entryName]: path.resolve(process.cwd(), paragonThemeCss.core.filePath) }; | ||
Object.values(paragonThemeCss.variants).forEach(({ filePath, entryName }) => { | ||
entryPoints[entryName] = path.resolve(process.cwd(), filePath); | ||
}); | ||
return entryPoints; | ||
} | ||
|
||
module.exports = { | ||
getParagonVersion, | ||
getParagonThemeCss, | ||
getParagonCacheGroups, | ||
getParagonEntryPoints, | ||
}; |
126 changes: 126 additions & 0 deletions
126
lib/plugins/paragon-webpack-plugin/ParagonWebpackPlugin.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
const { Compilation, sources } = require('webpack'); | ||
const { | ||
getParagonVersion, | ||
getParagonThemeCss, | ||
} = require('../../../config/data/paragonUtils'); | ||
const { | ||
injectMetadataIntoDocument, | ||
getParagonStylesheetUrls, | ||
injectParagonCoreStylesheets, | ||
injectParagonThemeVariantStylesheets, | ||
} = require('./utils'); | ||
|
||
// Get Paragon and brand versions / CSS files from disk. | ||
const paragonVersion = getParagonVersion(process.cwd()); | ||
const paragonThemeCss = getParagonThemeCss(process.cwd()); | ||
const brandVersion = getParagonVersion(process.cwd(), { isBrandOverride: true }); | ||
const brandThemeCss = getParagonThemeCss(process.cwd(), { isBrandOverride: true }); | ||
|
||
/** | ||
* 1. Injects `PARAGON_THEME` global variable into the HTML document during the Webpack compilation process. | ||
* 2. Injects `<link rel="preload" as="style">` element(s) for the Paragon and brand CSS into the HTML document. | ||
*/ | ||
class ParagonWebpackPlugin { | ||
constructor({ processAssetsHandlers = [] } = {}) { | ||
this.pluginName = 'ParagonWebpackPlugin'; | ||
this.paragonThemeUrlsConfig = {}; | ||
this.paragonMetadata = {}; | ||
|
||
// List of handlers to be executed after processing assets during the Webpack compilation. | ||
this.processAssetsHandlers = [ | ||
this.resolveParagonThemeUrlsFromConfig, | ||
this.injectParagonMetadataIntoDocument, | ||
this.injectParagonStylesheetsIntoDocument, | ||
...processAssetsHandlers, | ||
].map(handler => handler.bind(this)); | ||
} | ||
|
||
/** | ||
* Resolves the MFE configuration from ``PARAGON_THEME_URLS`` in the environment variables. ` | ||
* | ||
* @returns {Object} Metadata about the Paragon and brand theme URLs from configuration. | ||
*/ | ||
async resolveParagonThemeUrlsFromConfig() { | ||
try { | ||
this.paragonThemeUrlsConfig = JSON.parse(process.env.PARAGON_THEME_URLS); | ||
} catch (error) { | ||
console.info('Paragon Plugin cannot load PARAGON_THEME_URLS env variable, skipping.'); | ||
} | ||
} | ||
|
||
/** | ||
* Generates `PARAGON_THEME` global variable in HTML document. | ||
* @param {Object} compilation Webpack compilation object. | ||
*/ | ||
injectParagonMetadataIntoDocument(compilation) { | ||
const paragonMetadata = injectMetadataIntoDocument(compilation, { | ||
paragonThemeCss, | ||
paragonVersion, | ||
brandThemeCss, | ||
brandVersion, | ||
}); | ||
if (paragonMetadata) { | ||
this.paragonMetadata = paragonMetadata; | ||
} | ||
} | ||
|
||
injectParagonStylesheetsIntoDocument(compilation) { | ||
const file = compilation.getAsset('index.html'); | ||
|
||
// If the `index.html` hasn't loaded yet, or there are no Paragon theme URLs, then there is nothing to do yet. | ||
if (!file || Object.keys(this.paragonThemeUrlsConfig || {}).length === 0) { | ||
return; | ||
} | ||
|
||
// Generates `<link rel="preload" as="style">` element(s) for the Paragon and brand CSS files. | ||
const paragonStylesheetUrls = getParagonStylesheetUrls({ | ||
paragonThemeUrls: this.paragonThemeUrlsConfig, | ||
paragonVersion, | ||
brandVersion, | ||
}); | ||
const { | ||
core: paragonCoreCss, | ||
variants: paragonThemeVariantCss, | ||
} = paragonStylesheetUrls; | ||
|
||
const originalSource = file.source.source(); | ||
|
||
// Inject core CSS | ||
let newSource = injectParagonCoreStylesheets({ | ||
source: originalSource, | ||
paragonCoreCss, | ||
paragonThemeCss, | ||
brandThemeCss, | ||
}); | ||
|
||
// Inject theme variant CSS | ||
newSource = injectParagonThemeVariantStylesheets({ | ||
source: newSource.source(), | ||
paragonThemeVariantCss, | ||
paragonThemeCss, | ||
brandThemeCss, | ||
}); | ||
|
||
compilation.updateAsset('index.html', new sources.RawSource(newSource.source())); | ||
} | ||
|
||
apply(compiler) { | ||
compiler.hooks.thisCompilation.tap(this.pluginName, (compilation) => { | ||
compilation.hooks.processAssets.tap( | ||
{ | ||
name: this.pluginName, | ||
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, | ||
additionalAssets: true, | ||
}, | ||
() => { | ||
// Iterate through each configured handler, passing the compilation to each. | ||
this.processAssetsHandlers.forEach(async (handler) => { | ||
await handler(compilation); | ||
}); | ||
}, | ||
); | ||
}); | ||
} | ||
} | ||
|
||
module.exports = ParagonWebpackPlugin; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const ParagonWebpackPlugin = require('./ParagonWebpackPlugin'); | ||
|
||
module.exports = ParagonWebpackPlugin; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/** | ||
* Finds the core CSS asset from the given array of Paragon assets. | ||
* | ||
* @param {Array} paragonAssets - An array of Paragon assets. | ||
* @return {Object|undefined} The core CSS asset, or undefined if not found. | ||
*/ | ||
function findCoreCssAsset(paragonAssets) { | ||
return paragonAssets?.find((asset) => asset.name.includes('core') && asset.name.endsWith('.css')); | ||
} | ||
|
||
/** | ||
* Finds the theme variant CSS assets from the given Paragon assets based on the provided options. | ||
* | ||
* @param {Array} paragonAssets - An array of Paragon assets. | ||
* @param {Object} options - The options for finding the theme variant CSS assets. | ||
* @param {boolean} [options.isBrandOverride=false] - Indicates if the theme variant is a brand override. | ||
* @param {Object} [options.brandThemeCss] - The brand theme CSS object. | ||
* @param {Object} [options.paragonThemeCss] - The Paragon theme CSS object. | ||
* @return {Object} - The theme variant CSS assets. | ||
*/ | ||
function findThemeVariantCssAssets(paragonAssets, { | ||
isBrandOverride = false, | ||
brandThemeCss, | ||
paragonThemeCss, | ||
}) { | ||
const themeVariantsSource = isBrandOverride ? brandThemeCss?.variants : paragonThemeCss?.variants; | ||
const themeVariantCssAssets = {}; | ||
Object.entries(themeVariantsSource || {}).forEach(([themeVariant, value]) => { | ||
const foundThemeVariantAsset = paragonAssets.find((asset) => asset.name.includes(value.outputChunkName)); | ||
if (!foundThemeVariantAsset) { | ||
return; | ||
} | ||
themeVariantCssAssets[themeVariant] = { | ||
fileName: foundThemeVariantAsset.name, | ||
}; | ||
}); | ||
return themeVariantCssAssets; | ||
} | ||
|
||
/** | ||
* Retrieves the CSS assets from the compilation based on the provided options. | ||
* | ||
* @param {Object} compilation - The compilation object. | ||
* @param {Object} options - The options for retrieving the CSS assets. | ||
* @param {boolean} [options.isBrandOverride=false] - Indicates if the assets are for a brand override. | ||
* @param {Object} [options.brandThemeCss] - The brand theme CSS object. | ||
* @param {Object} [options.paragonThemeCss] - The Paragon theme CSS object. | ||
* @return {Object} - The CSS assets, including the core CSS asset and theme variant CSS assets. | ||
*/ | ||
function getCssAssetsFromCompilation(compilation, { | ||
isBrandOverride = false, | ||
brandThemeCss, | ||
paragonThemeCss, | ||
}) { | ||
const assetSubstring = isBrandOverride ? 'brand' : 'paragon'; | ||
const paragonAssets = compilation.getAssets().filter(asset => asset.name.includes(assetSubstring) && asset.name.endsWith('.css')); | ||
const coreCssAsset = findCoreCssAsset(paragonAssets); | ||
const themeVariantCssAssets = findThemeVariantCssAssets(paragonAssets, { | ||
isBrandOverride, | ||
paragonThemeCss, | ||
brandThemeCss, | ||
}); | ||
return { | ||
coreCssAsset: { | ||
fileName: coreCssAsset?.name, | ||
}, | ||
themeVariantCssAssets, | ||
}; | ||
} | ||
|
||
module.exports = { | ||
findCoreCssAsset, | ||
findThemeVariantCssAssets, | ||
getCssAssetsFromCompilation, | ||
}; |
Oops, something went wrong.