@openedx/frontend-base 1.0.0-alpha.1 → 1.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/config/eslint/base.eslint.config.js +1 -1
- package/config/jest/jest.config.js +1 -0
- package/config/types.js +0 -2
- package/config/webpack/common-config/all/getStylesheetRule.js +1 -1
- package/config/webpack/webpack.config.build.js +1 -11
- package/config/webpack/webpack.config.dev.js +5 -11
- package/config/webpack/webpack.config.dev.shell.js +5 -11
- package/package.json +8 -4
- package/runtime/config/index.ts +2 -3
- package/runtime/index.ts +5 -0
- package/runtime/jest.config.js +1 -0
- package/runtime/react/SiteProvider.tsx +26 -3
- package/runtime/react/constants.ts +3 -0
- package/runtime/react/hooks/index.ts +8 -0
- package/runtime/react/hooks/theme/index.ts +2 -0
- package/runtime/react/hooks/theme/useTheme.test.ts +221 -0
- package/runtime/react/hooks/theme/useTheme.ts +179 -0
- package/runtime/react/hooks/theme/useThemeConfig.test.ts +107 -0
- package/runtime/react/hooks/theme/useThemeConfig.ts +34 -0
- package/runtime/react/hooks/theme/useThemeCore.test.ts +65 -0
- package/runtime/react/hooks/theme/useThemeCore.ts +52 -0
- package/runtime/react/hooks/theme/useThemeVariants.test.ts +97 -0
- package/runtime/react/hooks/theme/useThemeVariants.ts +116 -0
- package/runtime/react/hooks/theme/useTrackColorSchemeChoice.test.ts +54 -0
- package/runtime/react/hooks/theme/useTrackColorSchemeChoice.ts +30 -0
- package/runtime/react/hooks/theme/utils.ts +11 -0
- package/runtime/react/hooks/useActiveRoles.ts +15 -0
- package/runtime/react/hooks/useActiveRouteRoleWatcher.ts +31 -0
- package/runtime/react/hooks/useAppConfig.ts +9 -0
- package/runtime/react/hooks/useAuthenticatedUser.test.tsx +41 -0
- package/runtime/react/hooks/useAuthenticatedUser.ts +9 -0
- package/runtime/react/hooks/useSiteConfig.test.tsx +13 -0
- package/runtime/react/hooks/useSiteConfig.ts +9 -0
- package/runtime/react/hooks/useSiteEvent.ts +24 -0
- package/runtime/react/reducers.ts +40 -0
- package/runtime/setupTest.js +0 -35
- package/runtime/slots/widget/iframe/hooks.ts +1 -1
- package/runtime/testing/initializeMockApp.ts +5 -0
- package/shell/app.scss +2 -1
- package/shell/jest.config.js +1 -0
- package/shell/setupTest.js +0 -35
- package/shell/site.tsx +1 -1
- package/tools/dist/cli/openedx.js +1 -15
- package/tools/dist/cli/utils/printUsage.js +0 -9
- package/tools/dist/eslint/base.eslint.config.js +1 -1
- package/tools/dist/jest/jest.config.js +1 -0
- package/tools/dist/types.js +0 -2
- package/tools/dist/webpack/common-config/all/getStylesheetRule.js +1 -1
- package/tools/dist/webpack/webpack.config.build.js +1 -11
- package/tools/dist/webpack/webpack.config.dev.js +5 -11
- package/tools/dist/webpack/webpack.config.dev.shell.js +5 -11
- package/types.ts +20 -0
- package/config/webpack/plugins/paragon-webpack-plugin/ParagonWebpackPlugin.js +0 -108
- package/config/webpack/plugins/paragon-webpack-plugin/index.js +0 -7
- package/config/webpack/plugins/paragon-webpack-plugin/utils/assetUtils.js +0 -64
- package/config/webpack/plugins/paragon-webpack-plugin/utils/htmlUtils.js +0 -53
- package/config/webpack/plugins/paragon-webpack-plugin/utils/index.js +0 -9
- package/config/webpack/plugins/paragon-webpack-plugin/utils/paragonStylesheetUtils.js +0 -114
- package/config/webpack/plugins/paragon-webpack-plugin/utils/scriptUtils.js +0 -146
- package/config/webpack/plugins/paragon-webpack-plugin/utils/stylesheetUtils.js +0 -126
- package/config/webpack/plugins/paragon-webpack-plugin/utils/tagUtils.js +0 -57
- package/config/webpack/types.js +0 -2
- package/config/webpack/utils/paragonUtils.js +0 -138
- package/runtime/react/hooks.test.jsx +0 -104
- package/runtime/react/hooks.ts +0 -106
- package/tools/dist/cli/commands/pack.js +0 -14
- package/tools/dist/cli/commands/release.js +0 -28
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/ParagonWebpackPlugin.js +0 -108
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/index.js +0 -7
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/assetUtils.js +0 -64
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/htmlUtils.js +0 -53
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/index.js +0 -9
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/paragonStylesheetUtils.js +0 -114
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/scriptUtils.js +0 -146
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/stylesheetUtils.js +0 -126
- package/tools/dist/webpack/plugins/paragon-webpack-plugin/utils/tagUtils.js +0 -57
- package/tools/dist/webpack/types.js +0 -2
- package/tools/dist/webpack/utils/paragonUtils.js +0 -138
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.findStylesheetInsertionPoint = findStylesheetInsertionPoint;
|
|
37
|
-
exports.insertStylesheetsIntoDocument = insertStylesheetsIntoDocument;
|
|
38
|
-
const parse5 = __importStar(require("parse5"));
|
|
39
|
-
const webpack_1 = require("webpack");
|
|
40
|
-
const tagUtils_1 = require("./tagUtils");
|
|
41
|
-
/**
|
|
42
|
-
* Finds the insertion point for a stylesheet in an HTML document.
|
|
43
|
-
*
|
|
44
|
-
* @param {Object} options - The options object.
|
|
45
|
-
* @param {Object} options.document - The parsed HTML document.
|
|
46
|
-
* @param {string} options.source - The original source code of the HTML document.
|
|
47
|
-
* @throws {Error} If the head element is missing in the HTML document.
|
|
48
|
-
* @return {number} The insertion point for the stylesheet in the HTML document.
|
|
49
|
-
*/
|
|
50
|
-
function findStylesheetInsertionPoint({ document, source }) {
|
|
51
|
-
const headElement = (0, tagUtils_1.getDescendantByTag)(document, 'head');
|
|
52
|
-
if (!headElement) {
|
|
53
|
-
throw new Error('Missing head element in index.html.');
|
|
54
|
-
}
|
|
55
|
-
// determine script insertion point
|
|
56
|
-
if (headElement.sourceCodeLocation?.startTag) {
|
|
57
|
-
return headElement.sourceCodeLocation.startTag.endOffset;
|
|
58
|
-
}
|
|
59
|
-
// less accurate fallback
|
|
60
|
-
const headTagString = '<head>';
|
|
61
|
-
const headTagIndex = source.indexOf(headTagString);
|
|
62
|
-
return headTagIndex + headTagString.length;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Inserts stylesheets into an HTML document.
|
|
66
|
-
*
|
|
67
|
-
* @param {object} options - The options for inserting stylesheets.
|
|
68
|
-
* @param {string} options.source - The HTML source code.
|
|
69
|
-
* @param {object} options.urls - The URLs of the stylesheets to be inserted.
|
|
70
|
-
* @param {string} options.urls.default - The URL of the default stylesheet.
|
|
71
|
-
* @param {string} options.urls.brandOverride - The URL of the brand override stylesheet.
|
|
72
|
-
* @return {object|undefined} The new source code with the stylesheets inserted.
|
|
73
|
-
*/
|
|
74
|
-
function insertStylesheetsIntoDocument({ source, urls, }) {
|
|
75
|
-
// parse file as html document
|
|
76
|
-
const document = parse5.parse(source, {
|
|
77
|
-
sourceCodeLocationInfo: true,
|
|
78
|
-
});
|
|
79
|
-
if (!(0, tagUtils_1.getDescendantByTag)(document, 'head')) {
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
const newSource = new webpack_1.sources.ReplaceSource(new webpack_1.sources.RawSource(source), 'index.html');
|
|
83
|
-
// insert the brand overrides styles into the HTML document
|
|
84
|
-
const stylesheetInsertionPoint = findStylesheetInsertionPoint({
|
|
85
|
-
// @ts-expect-error Typescript complains this document instance is missing properties. Is parse5.parse not returning a valid Document instance?
|
|
86
|
-
document,
|
|
87
|
-
// @ts-expect-error We're passing a ReplaceSource here, when we expect a string in the function.
|
|
88
|
-
source: newSource,
|
|
89
|
-
});
|
|
90
|
-
/**
|
|
91
|
-
* Creates a new stylesheet link element.
|
|
92
|
-
*
|
|
93
|
-
* @param {string} url - The URL of the stylesheet.
|
|
94
|
-
* @return {string} The HTML code for the stylesheet link element.
|
|
95
|
-
*/
|
|
96
|
-
function createNewStylesheet(url) {
|
|
97
|
-
const baseLink = `<link
|
|
98
|
-
type="text/css"
|
|
99
|
-
rel="preload"
|
|
100
|
-
as="style"
|
|
101
|
-
href="${url}"
|
|
102
|
-
onload="this.rel='stylesheet';"
|
|
103
|
-
onerror="this.remove();"
|
|
104
|
-
/>`;
|
|
105
|
-
return baseLink;
|
|
106
|
-
}
|
|
107
|
-
if (urls.default) {
|
|
108
|
-
// @ts-expect-error getDescendantByTag requires two parameters.
|
|
109
|
-
const existingDefaultLink = (0, tagUtils_1.getDescendantByTag)(`link[href='${urls.default}']`);
|
|
110
|
-
if (!existingDefaultLink) {
|
|
111
|
-
// create link to inject into the HTML document
|
|
112
|
-
const stylesheetLink = createNewStylesheet(urls.default);
|
|
113
|
-
newSource.insert(stylesheetInsertionPoint, stylesheetLink);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (urls.brandOverride) {
|
|
117
|
-
// @ts-expect-error getDescendantByTag requires two parameters.
|
|
118
|
-
const existingBrandLink = (0, tagUtils_1.getDescendantByTag)(`link[href='${urls.brandOverride}']`);
|
|
119
|
-
if (!existingBrandLink) {
|
|
120
|
-
// create link to inject into the HTML document
|
|
121
|
-
const stylesheetLink = createNewStylesheet(urls.brandOverride);
|
|
122
|
-
newSource.insert(stylesheetInsertionPoint, stylesheetLink);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return newSource;
|
|
126
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getDescendantByTag = getDescendantByTag;
|
|
4
|
-
exports.handleVersionSubstitution = handleVersionSubstitution;
|
|
5
|
-
exports.minifyScript = minifyScript;
|
|
6
|
-
/**
|
|
7
|
-
* Recursively searches for a descendant node with the specified tag name.
|
|
8
|
-
*
|
|
9
|
-
* @param {Object} node - The root node to start the search from.
|
|
10
|
-
* @param {string} tag - The tag name to search for.
|
|
11
|
-
* @return {Object|null} The first descendant node with the specified tag name, or null if not found.
|
|
12
|
-
*/
|
|
13
|
-
function getDescendantByTag(node, tag) {
|
|
14
|
-
for (let i = 0; i < node.childNodes?.length; i++) {
|
|
15
|
-
if (node.childNodes[i].tagName === tag) {
|
|
16
|
-
return node.childNodes[i];
|
|
17
|
-
}
|
|
18
|
-
const result = getDescendantByTag(node.childNodes[i], tag);
|
|
19
|
-
if (result) {
|
|
20
|
-
return result;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Replaces a wildcard keyword in a URL with a local version.
|
|
27
|
-
*
|
|
28
|
-
* @param {Object} options - The options object.
|
|
29
|
-
* @param {string} options.url - The URL to substitute the keyword in.
|
|
30
|
-
* @param {string} options.wildcardKeyword - The wildcard keyword to replace.
|
|
31
|
-
* @param {string} options.localVersion - The local version to substitute the keyword with.
|
|
32
|
-
* @return {string} The URL with the wildcard keyword substituted with the local version,
|
|
33
|
-
* or the original URL if no substitution is needed.
|
|
34
|
-
*/
|
|
35
|
-
function handleVersionSubstitution({ url, wildcardKeyword, localVersion }) {
|
|
36
|
-
if (!url || !url.includes(wildcardKeyword) || !localVersion) {
|
|
37
|
-
return url;
|
|
38
|
-
}
|
|
39
|
-
return url.replaceAll(wildcardKeyword, localVersion);
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Minifies a script by removing unnecessary whitespace and line breaks.
|
|
43
|
-
*
|
|
44
|
-
* @param {string} script - The script to be minified.
|
|
45
|
-
* @return {string} The minified script.
|
|
46
|
-
*/
|
|
47
|
-
function minifyScript(script) {
|
|
48
|
-
return script
|
|
49
|
-
.replace(/>[\r\n ]+</g, '><')
|
|
50
|
-
.replace(/(<.*?>)|\s+/g, (m, $1) => {
|
|
51
|
-
if ($1) {
|
|
52
|
-
return $1;
|
|
53
|
-
}
|
|
54
|
-
return ' ';
|
|
55
|
-
})
|
|
56
|
-
.trim();
|
|
57
|
-
}
|
package/config/webpack/types.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getParagonVersion = getParagonVersion;
|
|
7
|
-
exports.getParagonThemeCss = getParagonThemeCss;
|
|
8
|
-
exports.getParagonCacheGroups = getParagonCacheGroups;
|
|
9
|
-
exports.getParagonEntryPoints = getParagonEntryPoints;
|
|
10
|
-
const fs_1 = __importDefault(require("fs"));
|
|
11
|
-
const path_1 = __importDefault(require("path"));
|
|
12
|
-
/**
|
|
13
|
-
* Retrieves the name of the brand package from the given directory.
|
|
14
|
-
*
|
|
15
|
-
* @param {string} dir - The directory path containing the package.json file.
|
|
16
|
-
* @return {string} The name of the brand package, or an empty string if not found.
|
|
17
|
-
*/
|
|
18
|
-
function getBrandPackageName(dir) {
|
|
19
|
-
const appDependencies = JSON.parse(fs_1.default.readFileSync(path_1.default.resolve(dir, 'package.json'), 'utf-8')).dependencies;
|
|
20
|
-
return Object.keys(appDependencies).find((key) => /@(open)?edx\/brand/.exec(key)) ?? '';
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Attempts to extract the Paragon version from the `node_modules` of
|
|
24
|
-
* the consuming application.
|
|
25
|
-
*
|
|
26
|
-
* @param {string} dir Path to directory containing `node_modules`.
|
|
27
|
-
* @returns {string} Paragon dependency version of the consuming application
|
|
28
|
-
*/
|
|
29
|
-
function getParagonVersion(dir, { isBrandOverride = false } = {}) {
|
|
30
|
-
const npmPackageName = isBrandOverride ? getBrandPackageName(dir) : '@openedx/paragon';
|
|
31
|
-
const pathToPackageJson = `${dir}/node_modules/${npmPackageName}/package.json`;
|
|
32
|
-
if (!fs_1.default.existsSync(pathToPackageJson)) {
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
return JSON.parse(fs_1.default.readFileSync(pathToPackageJson, 'utf-8')).version;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Attempts to extract the Paragon theme CSS from the locally installed `@openedx/paragon` package.
|
|
39
|
-
* @param {string} dir Path to directory containing `node_modules`.
|
|
40
|
-
* @param {boolean} isBrandOverride
|
|
41
|
-
* @returns {ParagonThemeCss}
|
|
42
|
-
*/
|
|
43
|
-
function getParagonThemeCss(dir, { isBrandOverride = false } = {}) {
|
|
44
|
-
const npmPackageName = isBrandOverride ? getBrandPackageName(dir) : '@openedx/paragon';
|
|
45
|
-
const pathToParagonThemeOutput = path_1.default.resolve(dir, 'node_modules', npmPackageName, 'dist', 'theme-urls.json');
|
|
46
|
-
if (!fs_1.default.existsSync(pathToParagonThemeOutput)) {
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
const paragonConfig = JSON.parse(fs_1.default.readFileSync(pathToParagonThemeOutput, 'utf-8'));
|
|
50
|
-
const { core: themeCore, variants: themeVariants, defaults, } = paragonConfig?.themeUrls || {};
|
|
51
|
-
const pathToCoreCss = path_1.default.resolve(dir, 'node_modules', npmPackageName, 'dist', themeCore.paths.minified);
|
|
52
|
-
const coreCssExists = fs_1.default.existsSync(pathToCoreCss);
|
|
53
|
-
const themeVariantResults = Object.entries(themeVariants || {}).reduce((themeVariantAcc, [themeVariant, value]) => {
|
|
54
|
-
const themeVariantCssDefault = path_1.default.resolve(dir, 'node_modules', npmPackageName, 'dist', value.paths.default);
|
|
55
|
-
const themeVariantCssMinified = path_1.default.resolve(dir, 'node_modules', npmPackageName, 'dist', value.paths.minified);
|
|
56
|
-
if (!fs_1.default.existsSync(themeVariantCssDefault) && !fs_1.default.existsSync(themeVariantCssMinified)) {
|
|
57
|
-
return themeVariantAcc;
|
|
58
|
-
}
|
|
59
|
-
return ({
|
|
60
|
-
...themeVariantAcc,
|
|
61
|
-
[themeVariant]: {
|
|
62
|
-
filePath: themeVariantCssMinified,
|
|
63
|
-
entryName: isBrandOverride ? `brand.theme.variants.${themeVariant}` : `paragon.theme.variants.${themeVariant}`,
|
|
64
|
-
outputChunkName: isBrandOverride ? `brand-theme-variants-${themeVariant}` : `paragon-theme-variants-${themeVariant}`,
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
}, {});
|
|
68
|
-
if (!coreCssExists || Object.keys(themeVariantResults).length === 0) {
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
const coreResult = {
|
|
72
|
-
filePath: path_1.default.resolve(dir, pathToCoreCss),
|
|
73
|
-
entryName: isBrandOverride ? 'brand.theme.core' : 'paragon.theme.core',
|
|
74
|
-
outputChunkName: isBrandOverride ? 'brand-theme-core' : 'paragon-theme-core',
|
|
75
|
-
};
|
|
76
|
-
return {
|
|
77
|
-
core: fs_1.default.existsSync(pathToCoreCss) ? coreResult : undefined,
|
|
78
|
-
variants: themeVariantResults,
|
|
79
|
-
defaults,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* @typedef CacheGroup
|
|
84
|
-
* @property {string} type The type of cache group.
|
|
85
|
-
* @property {string|function} name The name of the cache group.
|
|
86
|
-
* @property {function} chunks A function that returns true if the chunk should be included in the cache group.
|
|
87
|
-
* @property {boolean} enforce If true, this cache group will be created even if it conflicts with default cache groups.
|
|
88
|
-
*/
|
|
89
|
-
/**
|
|
90
|
-
* @param {ParagonThemeCss} paragonThemeCss The Paragon theme CSS metadata.
|
|
91
|
-
* @returns {Object.<string, CacheGroup>} The cache groups for the Paragon theme CSS.
|
|
92
|
-
*/
|
|
93
|
-
function getParagonCacheGroups(paragonThemeCss) {
|
|
94
|
-
if (!paragonThemeCss) {
|
|
95
|
-
return {};
|
|
96
|
-
}
|
|
97
|
-
const cacheGroups = {};
|
|
98
|
-
if (paragonThemeCss.core !== undefined) {
|
|
99
|
-
const { core } = paragonThemeCss;
|
|
100
|
-
cacheGroups[paragonThemeCss.core.outputChunkName] = {
|
|
101
|
-
type: 'css/mini-extract',
|
|
102
|
-
name: paragonThemeCss.core.outputChunkName,
|
|
103
|
-
chunks: (chunk) => chunk.name === core.entryName,
|
|
104
|
-
enforce: true,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
Object.values(paragonThemeCss.variants).forEach(({ entryName, outputChunkName }) => {
|
|
108
|
-
cacheGroups[outputChunkName] = {
|
|
109
|
-
type: 'css/mini-extract',
|
|
110
|
-
name: outputChunkName,
|
|
111
|
-
chunks: (chunk) => chunk.name === entryName,
|
|
112
|
-
enforce: true,
|
|
113
|
-
};
|
|
114
|
-
});
|
|
115
|
-
return cacheGroups;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* @param {ParagonThemeCss} paragonThemeCss The Paragon theme CSS metadata.
|
|
119
|
-
* @returns {Object.<string, string>} The entry points for the Paragon theme CSS. Example: ```
|
|
120
|
-
* {
|
|
121
|
-
* "paragon.theme.core": "/path/to/node_modules/@openedx/paragon/dist/core.min.css",
|
|
122
|
-
* "paragon.theme.variants.light": "/path/to/node_modules/@openedx/paragon/dist/light.min.css"
|
|
123
|
-
* }
|
|
124
|
-
* ```
|
|
125
|
-
*/
|
|
126
|
-
function getParagonEntryPoints(paragonThemeCss) {
|
|
127
|
-
if (!paragonThemeCss) {
|
|
128
|
-
return {};
|
|
129
|
-
}
|
|
130
|
-
const entryPoints = {};
|
|
131
|
-
if (paragonThemeCss.core !== undefined) {
|
|
132
|
-
entryPoints[paragonThemeCss.core.entryName] = path_1.default.resolve(process.cwd(), paragonThemeCss.core.filePath);
|
|
133
|
-
}
|
|
134
|
-
Object.values(paragonThemeCss.variants).forEach(({ filePath, entryName }) => {
|
|
135
|
-
entryPoints[entryName] = path_1.default.resolve(process.cwd(), filePath);
|
|
136
|
-
});
|
|
137
|
-
return entryPoints;
|
|
138
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { act, renderHook } from '@testing-library/react';
|
|
2
|
-
import siteConfig from 'site.config';
|
|
3
|
-
import { EnvironmentTypes } from '../../types';
|
|
4
|
-
import { sendTrackEvent } from '../analytics';
|
|
5
|
-
import { setAuthenticatedUser } from '../auth';
|
|
6
|
-
import { initializeMockApp } from '../testing';
|
|
7
|
-
import SiteProvider from './SiteProvider';
|
|
8
|
-
import { useAuthenticatedUser, useSiteConfig, useTrackColorSchemeChoice } from './hooks';
|
|
9
|
-
|
|
10
|
-
jest.mock('../analytics');
|
|
11
|
-
|
|
12
|
-
const mockAddEventListener = jest.fn();
|
|
13
|
-
const mockRemoveEventListener = jest.fn();
|
|
14
|
-
let matchesMock;
|
|
15
|
-
|
|
16
|
-
Object.defineProperty(window, 'matchMedia', {
|
|
17
|
-
value: jest.fn(() => ({
|
|
18
|
-
addEventListener: mockAddEventListener,
|
|
19
|
-
removeEventListener: mockRemoveEventListener,
|
|
20
|
-
matches: matchesMock,
|
|
21
|
-
})),
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('useTrackColorSchemeChoice hook', () => {
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
mockAddEventListener.mockClear();
|
|
27
|
-
mockRemoveEventListener.mockClear();
|
|
28
|
-
sendTrackEvent.mockClear();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('sends dark preferred color schema event if query matches', async () => {
|
|
32
|
-
matchesMock = true;
|
|
33
|
-
renderHook(() => useTrackColorSchemeChoice());
|
|
34
|
-
|
|
35
|
-
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
|
36
|
-
expect(sendTrackEvent).toHaveBeenCalledWith(
|
|
37
|
-
'openedx.ui.frontend-base.prefers-color-scheme.selected',
|
|
38
|
-
{ preferredColorScheme: 'dark' },
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('sends light preferred color schema event if query does not match', async () => {
|
|
43
|
-
matchesMock = false;
|
|
44
|
-
renderHook(() => useTrackColorSchemeChoice());
|
|
45
|
-
|
|
46
|
-
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
|
47
|
-
expect(sendTrackEvent).toHaveBeenCalledWith(
|
|
48
|
-
'openedx.ui.frontend-base.prefers-color-scheme.selected',
|
|
49
|
-
{ preferredColorScheme: 'light' },
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('adds change event listener to matchMedia query', async () => {
|
|
54
|
-
renderHook(() => useTrackColorSchemeChoice());
|
|
55
|
-
|
|
56
|
-
expect(mockAddEventListener).toHaveBeenCalledTimes(1);
|
|
57
|
-
expect(mockAddEventListener).toHaveBeenCalledWith('change', expect.any(Function));
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('useAuthenticatedUser', () => {
|
|
62
|
-
it('returns null when the user is anonymous', () => {
|
|
63
|
-
const { result } = renderHook(() => useAuthenticatedUser());
|
|
64
|
-
expect(result.current).toBeNull();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('with a user', () => {
|
|
68
|
-
const user = {
|
|
69
|
-
administrator: true,
|
|
70
|
-
email: 'admin@example.com',
|
|
71
|
-
name: 'Admin',
|
|
72
|
-
roles: ['admin'],
|
|
73
|
-
userId: 1,
|
|
74
|
-
username: 'admin-user',
|
|
75
|
-
avatar: 'http://localhost/admin.png',
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
beforeEach(() => {
|
|
79
|
-
initializeMockApp({
|
|
80
|
-
authenticatedUser: user,
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
afterEach(() => {
|
|
85
|
-
act(() => {
|
|
86
|
-
setAuthenticatedUser(null);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('returns a User when the user exists', () => {
|
|
91
|
-
const { result } = renderHook(() => useAuthenticatedUser(), { wrapper: SiteProvider });
|
|
92
|
-
expect(result.current).toBe(user);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('useSiteConfig', () => {
|
|
98
|
-
it('returns the site config', () => {
|
|
99
|
-
const { result } = renderHook(() => useSiteConfig());
|
|
100
|
-
expect(result.current).toHaveProperty('apps', siteConfig.apps);
|
|
101
|
-
expect(result.current).toHaveProperty('environment', EnvironmentTypes.TEST);
|
|
102
|
-
expect(result.current).toHaveProperty('baseUrl', 'http://localhost:8080');
|
|
103
|
-
});
|
|
104
|
-
});
|
package/runtime/react/hooks.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { useCallback, useContext, useEffect, useState } from 'react';
|
|
2
|
-
import { useMatches } from 'react-router';
|
|
3
|
-
|
|
4
|
-
import { sendTrackEvent } from '../analytics';
|
|
5
|
-
import { getActiveRoles, setActiveRouteRoles } from '../config';
|
|
6
|
-
import { ACTIVE_ROLES_CHANGED } from '../constants';
|
|
7
|
-
import { isRoleRouteObject } from '../routing';
|
|
8
|
-
import { subscribe, unsubscribe } from '../subscriptions';
|
|
9
|
-
|
|
10
|
-
import SiteContext from './SiteContext';
|
|
11
|
-
import CurrentAppContext from './CurrentAppContext';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A React hook that allows functional components to subscribe to application events. This should
|
|
15
|
-
* be used sparingly - for the most part, Context should be used higher-up in the application to
|
|
16
|
-
* provide necessary data to a given component, rather than utilizing a non-React-like Pub/Sub
|
|
17
|
-
* mechanism.
|
|
18
|
-
*
|
|
19
|
-
* @memberof module:React
|
|
20
|
-
* @param {string} type
|
|
21
|
-
* @param {function} callback
|
|
22
|
-
*/
|
|
23
|
-
export const useSiteEvent = (type, callback) => {
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
subscribe(type, callback);
|
|
26
|
-
|
|
27
|
-
return () => {
|
|
28
|
-
unsubscribe(type, callback);
|
|
29
|
-
};
|
|
30
|
-
}, [callback, type]);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* A React hook that tracks user's preferred color scheme (light or dark) and sends respective
|
|
35
|
-
* event to the tracking service.
|
|
36
|
-
*
|
|
37
|
-
* @memberof module:React
|
|
38
|
-
*/
|
|
39
|
-
export const useTrackColorSchemeChoice = () => {
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
const trackColorSchemeChoice = ({ matches }) => {
|
|
42
|
-
const preferredColorScheme = matches ? 'dark' : 'light';
|
|
43
|
-
sendTrackEvent('openedx.ui.frontend-base.prefers-color-scheme.selected', { preferredColorScheme });
|
|
44
|
-
};
|
|
45
|
-
const colorSchemeQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
46
|
-
if (colorSchemeQuery) {
|
|
47
|
-
// send user's initial choice
|
|
48
|
-
trackColorSchemeChoice(colorSchemeQuery);
|
|
49
|
-
colorSchemeQuery.addEventListener('change', trackColorSchemeChoice);
|
|
50
|
-
}
|
|
51
|
-
return () => {
|
|
52
|
-
if (colorSchemeQuery) {
|
|
53
|
-
colorSchemeQuery.removeEventListener('change', trackColorSchemeChoice);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
}, []);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export function useAuthenticatedUser() {
|
|
60
|
-
const { authenticatedUser } = useContext(SiteContext);
|
|
61
|
-
return authenticatedUser;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function useSiteConfig() {
|
|
65
|
-
const { siteConfig } = useContext(SiteContext);
|
|
66
|
-
return siteConfig;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function useAppConfig() {
|
|
70
|
-
const { appConfig } = useContext(CurrentAppContext);
|
|
71
|
-
return appConfig;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function useActiveRouteRoleWatcher() {
|
|
75
|
-
const matches = useMatches();
|
|
76
|
-
|
|
77
|
-
// We create this callback so we can use it right away to populate the default state value.
|
|
78
|
-
const findActiveRouteRoles = useCallback(() => {
|
|
79
|
-
// Starts with the widget roles and adds the others in.
|
|
80
|
-
const roles: string[] = [];
|
|
81
|
-
|
|
82
|
-
// Route roles
|
|
83
|
-
for (const match of matches) {
|
|
84
|
-
if (isRoleRouteObject(match)) {
|
|
85
|
-
if (!roles.includes(match.handle.role)) {
|
|
86
|
-
roles.push(match.handle.role);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return roles;
|
|
92
|
-
}, [matches]);
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
setActiveRouteRoles(findActiveRouteRoles());
|
|
96
|
-
}, [matches, findActiveRouteRoles]);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function useActiveRoles() {
|
|
100
|
-
const [roles, setRoles] = useState<string[]>(getActiveRoles());
|
|
101
|
-
useSiteEvent(ACTIVE_ROLES_CHANGED, () => {
|
|
102
|
-
setRoles(getActiveRoles());
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
return roles;
|
|
106
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.default = pack;
|
|
7
|
-
const child_process_1 = require("child_process");
|
|
8
|
-
const path_1 = __importDefault(require("path"));
|
|
9
|
-
function pack() {
|
|
10
|
-
const destination = process.argv[2];
|
|
11
|
-
(0, child_process_1.execSync)('npm run release', { stdio: 'inherit' });
|
|
12
|
-
const { filename } = JSON.parse((0, child_process_1.execSync)('npm pack --json').toString())[0];
|
|
13
|
-
(0, child_process_1.execSync)(`npm --prefix ../${destination} install ${path_1.default.resolve(process.cwd(), filename)}`, { stdio: 'inherit' });
|
|
14
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.default = release;
|
|
7
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
-
const child_process_1 = require("child_process");
|
|
9
|
-
const fs_1 = require("fs");
|
|
10
|
-
const path_1 = __importDefault(require("path"));
|
|
11
|
-
function release() {
|
|
12
|
-
const tsconfigPath = path_1.default.resolve(process.cwd(), './tsconfig.json');
|
|
13
|
-
if (!(0, fs_1.existsSync)(tsconfigPath)) {
|
|
14
|
-
console.error(chalk_1.default.red('openedx release: the library must include a tsconfig.json. Aborting.'));
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
17
|
-
// Clean up our dist folder.
|
|
18
|
-
(0, fs_1.rmSync)(path_1.default.resolve(process.cwd(), 'dist'), { recursive: true, force: true });
|
|
19
|
-
(0, child_process_1.execSync)(`tsc --project ${path_1.default.resolve(process.cwd(), './tsconfig.json')}`, { stdio: 'inherit' });
|
|
20
|
-
// Copy all non JS/TS files from src into dist. This is so imports of our assets still work.
|
|
21
|
-
(0, child_process_1.execSync)(`rsync -aR src/**/* --exclude='*.tsx' --exclude='*.ts' --exclude='*.js' --exclude='*.jsx' dist/`);
|
|
22
|
-
// The above rsync command will put the files in dist/src - move them up a folder into dist,
|
|
23
|
-
// merging them into the compiled code there, and then delete dist/src.
|
|
24
|
-
if ((0, fs_1.existsSync)(path_1.default.resolve(process.cwd(), 'dist/src'))) {
|
|
25
|
-
(0, child_process_1.execSync)('cp -R dist/src/* dist');
|
|
26
|
-
(0, child_process_1.execSync)('rm -rf dist/src');
|
|
27
|
-
}
|
|
28
|
-
}
|