Skip to content

Commit 5873aa8

Browse files
Merge pull request #583 from openedx/master
sync: master to alpha
2 parents 03166be + 031f51f commit 5873aa8

19 files changed

+2263
-2431
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ frontend-platform:
155155
dist: The sub-directory of the source code where it puts its build artifact. Often "dist".
156156
*/
157157
localModules: [
158-
{ moduleName: '@openedx/brand', dir: '../src/brand-openedx' }, // replace with your brand checkout
158+
{ moduleName: '@edx/brand', dir: '../src/brand-openedx' }, // replace with your brand checkout
159159
{ moduleName: '@openedx/paragon/scss/core', dir: '../src/paragon', dist: 'scss/core' },
160160
{ moduleName: '@openedx/paragon/icons', dir: '../src/paragon', dist: 'icons' },
161161
{ moduleName: '@openedx/paragon', dir: '../src/paragon', dist: 'dist' },

config/.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ module.exports = {
3939
},
4040
globals: {
4141
newrelic: false,
42+
PARAGON_THEME: false,
4243
},
4344
ignorePatterns: [
4445
'module.config.js',

config/data/paragonUtils.js

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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+
};

config/jest/setupTest.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,38 @@ const testEnvFile = path.resolve(process.cwd(), '.env.test');
88
if (fs.existsSync(testEnvFile)) {
99
dotenv.config({ path: testEnvFile });
1010
}
11+
12+
global.PARAGON_THEME = {
13+
paragon: {
14+
version: '1.0.0',
15+
themeUrls: {
16+
core: {
17+
fileName: 'core.min.css',
18+
},
19+
defaults: {
20+
light: 'light',
21+
},
22+
variants: {
23+
light: {
24+
fileName: 'light.min.css',
25+
},
26+
},
27+
},
28+
},
29+
brand: {
30+
version: '1.0.0',
31+
themeUrls: {
32+
core: {
33+
fileName: 'core.min.css',
34+
},
35+
defaults: {
36+
light: 'light',
37+
},
38+
variants: {
39+
light: {
40+
fileName: 'light.min.css',
41+
},
42+
},
43+
},
44+
},
45+
};

config/webpack.common.config.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,35 @@
11
const path = require('path');
2+
const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts');
3+
4+
const ParagonWebpackPlugin = require('../lib/plugins/paragon-webpack-plugin/ParagonWebpackPlugin');
5+
const {
6+
getParagonThemeCss,
7+
getParagonCacheGroups,
8+
getParagonEntryPoints,
9+
} = require('./data/paragonUtils');
10+
11+
const paragonThemeCss = getParagonThemeCss(process.cwd());
12+
const brandThemeCss = getParagonThemeCss(process.cwd(), { isBrandOverride: true });
213

314
module.exports = {
415
entry: {
516
app: path.resolve(process.cwd(), './src/index'),
17+
/**
18+
* The entry points for the Paragon theme CSS. Example: ```
19+
* {
20+
* "paragon.theme.core": "/path/to/node_modules/@openedx/paragon/dist/core.min.css",
21+
* "paragon.theme.variants.light": "/path/to/node_modules/@openedx/paragon/dist/light.min.css"
22+
* }
23+
*/
24+
...getParagonEntryPoints(paragonThemeCss),
25+
/**
26+
* The entry points for the brand theme CSS. Example: ```
27+
* {
28+
* "paragon.theme.core": "/path/to/node_modules/@(open)edx/brand/dist/core.min.css",
29+
* "paragon.theme.variants.light": "/path/to/node_modules/@(open)edx/brand/dist/light.min.css"
30+
* }
31+
*/
32+
...getParagonEntryPoints(brandThemeCss),
633
},
734
output: {
835
path: path.resolve(process.cwd(), './dist'),
@@ -19,6 +46,23 @@ module.exports = {
1946
},
2047
extensions: ['.js', '.jsx', '.ts', '.tsx'],
2148
},
49+
optimization: {
50+
splitChunks: {
51+
chunks: 'all',
52+
cacheGroups: {
53+
...getParagonCacheGroups(paragonThemeCss),
54+
...getParagonCacheGroups(brandThemeCss),
55+
},
56+
},
57+
},
58+
plugins: [
59+
// RemoveEmptyScriptsPlugin get rid of empty scripts generated by webpack when using mini-css-extract-plugin
60+
// This helps to clean up the final bundle application
61+
// See: https://www.npmjs.com/package/webpack-remove-empty-scripts#usage-with-mini-css-extract-plugin
62+
63+
new RemoveEmptyScriptsPlugin(),
64+
new ParagonWebpackPlugin(),
65+
],
2266
ignoreWarnings: [
2367
// Ignore warnings raised by source-map-loader.
2468
// some third party packages may ship miss-configured sourcemaps, that interrupts the build

config/webpack.dev-stage.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ module.exports = merge(commonConfig, {
157157
new HtmlWebpackPlugin({
158158
inject: true, // Appends script tags linking to the webpack bundles at the end of the body
159159
template: path.resolve(process.cwd(), 'public/index.html'),
160+
chunks: ['app'],
160161
FAVICON_URL: process.env.FAVICON_URL || null,
161162
OPTIMIZELY_PROJECT_ID: process.env.OPTIMIZELY_PROJECT_ID || null,
162163
NODE_ENV: process.env.NODE_ENV || null,

0 commit comments

Comments
 (0)