Skip to content

Commit

Permalink
feat: add ParagonWebpackPlugin to support design tokens (#546)
Browse files Browse the repository at this point in the history
* feat: expose PARAGON_VERSION as a global variable

fix: rely on paragon-theme.json from @edx/paragon

chore: clean up and add typedefs

fix: create undefined PARAGON global variable if no compatible paragon version

fix: moar better error handling

fix: updates

fix: update based on new schema for paragon-theme.json

fix: update setupTest.js

chore: clean up

chore: remove compressionplugin

chore: quality

fix: rename paragon-theme.json to theme-urls.json

chore: uninstall unused node_module

feat: add @edx/brand version and urls to PARAGON_THEME global variable

chore: update snapshot

* fix: PR feedback

fix: add comment to ParagonWebpackPlugin and update snapshots

feat: preload links from PARAGON_THEME_URLS config

fix: handle undefined this.paragonMetadata

fix: remove fallbackUrls

chore: snapshots and resolve .eslintrc error

* fix: updates

fix: typo in `alpha` CDN url within example env.config.js

Co-authored-by: Diana Olarte <dcoa@live.com>

* chore: update dependecies in example app

* chore: update add webpack-remonve-empty-scripts and parse5 dependencies

* fix: install paragon plugins and fix css compiler

* refactor: change paragon package name for openedx

* refactor: remove runtime config

* revert: example changes

* fix: add a try/catch to config loading in Paragon Plugin to avoid errors during the build

* refactor: split Paragon Plugin Utils file

* docs: update JSDoc in config/data/paragonUtils.js

Co-authored-by: Peter Kulko <93188219+PKulkoRaccoonGang@users.noreply.github.com>

* refactor: use @openedx/brand scope

this following the update here https://github.com/openedx/frontend-build/pull/490/files#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R158

* refactor: add utf-8 to readFileSync in paragonUtils

* perf: replace filter for reduce in getParagonThemeCss

* refactor: change entryPoints and CacheGroups variable definition.

* docs: add comment to describe the use of RemoveEmptyScriptsPlugin

* revert: restore env.development processing un webpack.dev.config.js

* refactor: remove process.env references in ParagonWebpackPlugin, no supported

* fix: use openedx scope for webpack.dev.config

* fix: allow processing edx and openedx brand scope

* refactor: support process.env.PARAGON_THEME_URLS

* fix: make PARAGON_THEME_URLS be defined only as env

---------

Co-authored-by: Adam Stankiewicz <agstanki@gmail.com>
Co-authored-by: Peter Kulko <93188219+PKulkoRaccoonGang@users.noreply.github.com>
  • Loading branch information
3 people committed Aug 13, 2024
1 parent e18ebb9 commit 031f51f
Show file tree
Hide file tree
Showing 19 changed files with 2,263 additions and 2,431 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ frontend-platform:
dist: The sub-directory of the source code where it puts its build artifact. Often "dist".
*/
localModules: [
{ moduleName: '@openedx/brand', dir: '../src/brand-openedx' }, // replace with your brand checkout
{ moduleName: '@edx/brand', dir: '../src/brand-openedx' }, // replace with your brand checkout
{ moduleName: '@openedx/paragon/scss/core', dir: '../src/paragon', dist: 'scss/core' },
{ moduleName: '@openedx/paragon/icons', dir: '../src/paragon', dist: 'icons' },
{ moduleName: '@openedx/paragon', dir: '../src/paragon', dist: 'dist' },
Expand Down
1 change: 1 addition & 0 deletions config/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module.exports = {
},
globals: {
newrelic: false,
PARAGON_THEME: false,
},
ignorePatterns: [
'module.config.js',
Expand Down
171 changes: 171 additions & 0 deletions config/data/paragonUtils.js
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,
};
35 changes: 35 additions & 0 deletions config/jest/setupTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,38 @@ const testEnvFile = path.resolve(process.cwd(), '.env.test');
if (fs.existsSync(testEnvFile)) {
dotenv.config({ path: testEnvFile });
}

global.PARAGON_THEME = {
paragon: {
version: '1.0.0',
themeUrls: {
core: {
fileName: 'core.min.css',
},
defaults: {
light: 'light',
},
variants: {
light: {
fileName: 'light.min.css',
},
},
},
},
brand: {
version: '1.0.0',
themeUrls: {
core: {
fileName: 'core.min.css',
},
defaults: {
light: 'light',
},
variants: {
light: {
fileName: 'light.min.css',
},
},
},
},
};
44 changes: 44 additions & 0 deletions config/webpack.common.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
const path = require('path');
const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts');

const ParagonWebpackPlugin = require('../lib/plugins/paragon-webpack-plugin/ParagonWebpackPlugin');
const {
getParagonThemeCss,
getParagonCacheGroups,
getParagonEntryPoints,
} = require('./data/paragonUtils');

const paragonThemeCss = getParagonThemeCss(process.cwd());
const brandThemeCss = getParagonThemeCss(process.cwd(), { isBrandOverride: true });

module.exports = {
entry: {
app: path.resolve(process.cwd(), './src/index'),
/**
* 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"
* }
*/
...getParagonEntryPoints(paragonThemeCss),
/**
* The entry points for the brand theme CSS. Example: ```
* {
* "paragon.theme.core": "/path/to/node_modules/@(open)edx/brand/dist/core.min.css",
* "paragon.theme.variants.light": "/path/to/node_modules/@(open)edx/brand/dist/light.min.css"
* }
*/
...getParagonEntryPoints(brandThemeCss),
},
output: {
path: path.resolve(process.cwd(), './dist'),
Expand All @@ -19,6 +46,23 @@ module.exports = {
},
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
...getParagonCacheGroups(paragonThemeCss),
...getParagonCacheGroups(brandThemeCss),
},
},
},
plugins: [
// RemoveEmptyScriptsPlugin get rid of empty scripts generated by webpack when using mini-css-extract-plugin
// This helps to clean up the final bundle application
// See: https://www.npmjs.com/package/webpack-remove-empty-scripts#usage-with-mini-css-extract-plugin

new RemoveEmptyScriptsPlugin(),
new ParagonWebpackPlugin(),
],
ignoreWarnings: [
// Ignore warnings raised by source-map-loader.
// some third party packages may ship miss-configured sourcemaps, that interrupts the build
Expand Down
1 change: 1 addition & 0 deletions config/webpack.dev-stage.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ module.exports = merge(commonConfig, {
new HtmlWebpackPlugin({
inject: true, // Appends script tags linking to the webpack bundles at the end of the body
template: path.resolve(process.cwd(), 'public/index.html'),
chunks: ['app'],
FAVICON_URL: process.env.FAVICON_URL || null,
OPTIMIZELY_PROJECT_ID: process.env.OPTIMIZELY_PROJECT_ID || null,
NODE_ENV: process.env.NODE_ENV || null,
Expand Down
Loading

0 comments on commit 031f51f

Please sign in to comment.