Skip to content

Commit

Permalink
Merge pull request #583 from openedx/master
Browse files Browse the repository at this point in the history
sync: master to alpha
  • Loading branch information
edx-requirements-bot committed Aug 13, 2024
2 parents 03166be + 031f51f commit 5873aa8
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 5873aa8

Please sign in to comment.