Refactor extension to webpack
This change is the biggest in the history of the project. The entire
project has been refactored so it is built with webpack.
This involves:
- Creating webpack and npm config files.
- Fixing some bugs in the code due to the fact that webpack uses strict
mode.
- Merging some pieces of code which were shared throughout the codebase
(not exhaustive, more work should be done in this direction).
- Splitting the console_inject.js file into separate files (it had 1000+
lines).
- Adapting all the build-related files (Makefile, bash scripts, etc.)
- Changing the docs to explain the new build process.
- Changing the Zuul playbook/roles to adapt to the new build process.
Change-Id: I16476d47825461c3a318b3f1a1eddb06b2df2e89
diff --git a/src/common/api.js b/src/common/api.js
index 20a6c7d..065d84c 100644
--- a/src/common/api.js
+++ b/src/common/api.js
@@ -2,7 +2,7 @@
// Function to wrap calls to the Community Console API with intelligent error
// handling.
-function CCApi(method, data, authenticated, authuser = 0) {
+export function CCApi(method, data, authenticated, authuser = 0) {
var authuserPart =
authuser == '0' ? '' : '?authuser=' + encodeURIComponent(authuser);
diff --git a/src/common/commonUtils.js b/src/common/commonUtils.js
new file mode 100644
index 0000000..ee31037
--- /dev/null
+++ b/src/common/commonUtils.js
@@ -0,0 +1,17 @@
+export function parseUrl(url) {
+ var forum_a = url.match(/forum\/([0-9]+)/i);
+ var thread_a = url.match(/thread\/([0-9]+)/i);
+
+ if (forum_a === null || thread_a === null) {
+ return false;
+ }
+
+ return {
+ 'forum': forum_a[1],
+ 'thread': thread_a[1],
+ };
+}
+
+export function isEmpty(obj) {
+ return Object.keys(obj).length === 0;
+}
diff --git a/src/common/communityConsoleUtils.js b/src/common/communityConsoleUtils.js
new file mode 100644
index 0000000..05a283c
--- /dev/null
+++ b/src/common/communityConsoleUtils.js
@@ -0,0 +1,13 @@
+// Escapes username from HTML generated by the Community Console.
+export function escapeUsername(username) {
+ var quoteRegex = /"/g;
+ var commentRegex = /<!---->/g;
+ return username.replace(quoteRegex, '\\"').replace(commentRegex, '');
+}
+
+// Retrieves authuser from the data-startup attribute.
+export function getAuthUser() {
+ var startup =
+ JSON.parse(document.querySelector('html').getAttribute('data-startup'));
+ return startup?.[2]?.[1] || '0';
+}
diff --git a/src/common/console.css b/src/common/console.css
deleted file mode 100644
index 2eba28f..0000000
--- a/src/common/console.css
+++ /dev/null
@@ -1,159 +0,0 @@
-.TWPT-badge {
- width: calc(18/13*var(--icon-size, 16px));
- height: calc(18/13*var(--icon-size, 16px));
- border-radius: 50%;
-
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- justify-content: center;
- align-content: center;
- align-items: center;
-
- background-color: #009688;
- color: #fff;
- box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.22), 0 2px 2px 0 rgba(0, 0, 0, 0.12);
-
- user-select: none;
-}
-
-.TWPT-badge .material-icon-i {
- font-size: var(--icon-size, 16px);
-}
-
-.TWPT-btn--with-badge {
- position: relative;
- padding: 4px;
- cursor: pointer;
-}
-
-.TWPT-btn--with-badge .content {
- padding: 8px;
-}
-
-.TWPT-btn--with-badge .TWPT-badge {
- --icon-size: 13px;
- position: absolute;
- bottom: 6px;
- right: 5px;
-}
-
-.TWPT-dark-theme {
- padding: 4px 8px!important;
-}
-
-.TWPT-previous-posts {
- display: flex;
- flex-direction: row;
- align-items: center;
-}
-
-.TWPT-previous-posts .TWPT-badge {
- --icon-size: 18px;
- margin-right: 8px;
-}
-
-.TWPT-dialog {
- display: block!important;
- width: 600px;
- max-width: 100%;
- padding: 16px 0;
- background: white;
- box-shadow: 0 24px 38px 3px rgba(0,0,0,.14), 0 9px 46px 8px rgba(0,0,0,.12), 0 11px 15px -7px rgba(0,0,0,.2);
-}
-
-.TWPT-dialog-header {
- padding: 24px 24px 0;
- width: 100%;
- box-sizing: border-box;
-}
-
-.TWPT-dialog-header--title {
- color: #202124;
- font-family: 'Google Sans', sans-serif;
- font-size: 22px;
- font-weight: 400;
- line-height: 24px;
- margin-bottom: 4px;
- text-align: center;
-}
-
-.TWPT-dialog-main {
- font-size: 13px;
- font-weight: 400;
- color: rgba(0, 0, 0, .87);
- overflow: auto;
- padding: 0 24px;
-}
-
-.TWPT-dialog-footer {
- padding: 0 24px;
-}
-
-.TWPT-dialog-footer.is-hidden {
- display: none;
-}
-
-.TWPT-dialog-footer-btn {
- display: inline-block;
- float: right;
- position: relative;
- height: 36px;
- min-width: 64px;
- margin: 0 4px;
- cursor: pointer;
-}
-
-.TWPT-dialog-footer-btn:hover::after {
- content: "";
- display: block;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- background-color: currentColor;
- outline: 2px solid transparent;
- opacity: .12;
- border-radius: inherit;
- pointer-events: none;
-}
-
-.TWPT-dialog-footer-btn:not(.is-disabled) {
- color: #1a73e8!important;
-}
-
-.TWPT-dialog-footer-btn.is-disabled {
- color: #5f6368!important;
- cursor: not-allowed;
-}
-
-.TWPT-dialog-footer-btn--content {
- line-height: 36px;
- text-align: center;
-}
-
-.TWPT-log {
- max-height: 300px;
- padding: 0 8px;
- margin-bottom: 8px;
- overflow-y: auto;
- background-color: #e0e0e0;
-}
-
-.TWPT-log-entry {
- font-family: 'Roboto Mono', 'Courier New', monospace;
-}
-
-.TWPT-log-entry.TWPT-log-entry--error {
- color: #ff1744;
-}
-
-/*
- * Fix for the headers' right controls so the dark theme switch has space and
- * doesn't overlap the search bar.
- **/
-.material-content > header .right-control {
- width: auto!important;
- max-width: 252px!important;
-}
diff --git a/src/common/content_scripts.js b/src/common/contentScriptsUtils.js
similarity index 60%
rename from src/common/content_scripts.js
rename to src/common/contentScriptsUtils.js
index 1bc9a7d..00bc556 100644
--- a/src/common/content_scripts.js
+++ b/src/common/contentScriptsUtils.js
@@ -1,4 +1,4 @@
-function injectStylesheet(stylesheetName, attributes = {}) {
+export function injectStylesheet(stylesheetName, attributes = {}) {
var link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', stylesheetName);
@@ -6,18 +6,12 @@
document.head.appendChild(link);
}
-function injectStyles(css) {
+export function injectStyles(css) {
injectStylesheet('data:text/css;charset=UTF-8,' + encodeURIComponent(css));
}
-function injectScript(scriptName) {
+export function injectScript(scriptName) {
var script = document.createElement('script');
script.src = scriptName;
document.head.appendChild(script);
}
-
-function escapeUsername(username) {
- var quoteRegex = /"/g;
- var commentRegex = /<!---->/g;
- return username.replace(quoteRegex, '\\"').replace(commentRegex, '');
-}
diff --git a/src/common/csEventListener.js b/src/common/csEventListener.js
new file mode 100644
index 0000000..14b4295
--- /dev/null
+++ b/src/common/csEventListener.js
@@ -0,0 +1,43 @@
+// In order to pass i18n strings and settings values to the injected scripts,
+// which don't have access to the chrome.* APIs, we use event listeners.
+
+export function setUpListener() {
+ chrome.storage.sync.get(null, function(options) {
+ window.addEventListener('TWPT_sendRequest', evt => {
+ var request = evt.detail;
+ switch (request.data.action) {
+ case 'geti18nMessage':
+ var data = chrome.i18n.getMessage(
+ request.data.msg,
+ (Array.isArray(request.data.placeholders) ?
+ request.data.placeholders :
+ []));
+ break;
+
+ case 'getProfileIndicatorOptions':
+ var data = {
+ 'indicatorDot': options.profileindicator,
+ 'numPosts': options.profileindicatoralt
+ };
+ break;
+
+ case 'getNumPostMonths':
+ var data = options.profileindicatoralt_months;
+ break;
+
+ default:
+ var data = 'unknownAction';
+ console.warn('Unknown action ' + request.data.action + '.');
+ break;
+ }
+
+ var response = {
+ data,
+ requestId: request.id,
+ prefix: (request.prefix || 'TWPT'),
+ };
+
+ window.postMessage(response, '*');
+ });
+ });
+}
diff --git a/src/common/cs_event_listener.js b/src/common/cs_event_listener.js
deleted file mode 100644
index c7de08f..0000000
--- a/src/common/cs_event_listener.js
+++ /dev/null
@@ -1,40 +0,0 @@
-// In order to pass i18n strings and settings values to the injected scripts,
-// which don't have access to the chrome.* APIs, we use event listeners.
-chrome.storage.sync.get(null, function(options) {
- window.addEventListener('TWPT_sendRequest', evt => {
- var request = evt.detail;
- switch (request.data.action) {
- case 'geti18nMessage':
- var data = chrome.i18n.getMessage(
- request.data.msg,
- (Array.isArray(request.data.placeholders) ?
- request.data.placeholders :
- []));
- break;
-
- case 'getProfileIndicatorOptions':
- var data = {
- 'indicatorDot': options.profileindicator,
- 'numPosts': options.profileindicatoralt
- };
- break;
-
- case 'getNumPostMonths':
- var data = options.profileindicatoralt_months;
- break;
-
- default:
- var data = 'unknownAction';
- console.warn('Unknown action ' + request.data.action + '.');
- break;
- }
-
- var response = {
- data,
- requestId: request.id,
- prefix: (request.prefix || 'TWPT'),
- };
-
- window.postMessage(response, '*');
- });
-});
diff --git a/src/common/extUtils.js b/src/common/extUtils.js
new file mode 100644
index 0000000..a1b4d48
--- /dev/null
+++ b/src/common/extUtils.js
@@ -0,0 +1,13 @@
+// This method is based on the fact that when building the extension for Firefox
+// the browser_specific_settings.gecko entry is included.
+export function isFirefox() {
+ var manifest = chrome.runtime.getManifest();
+ return manifest.browser_specific_settings !== undefined &&
+ manifest.browser_specific_settings.gecko !== undefined;
+}
+
+// Returns whether the extension is a release version.
+export function isReleaseVersion() {
+ var manifest = chrome.runtime.getManifest();
+ return ('version' in manifest) && manifest.version != '0';
+}
diff --git a/src/common/forum.css b/src/common/forum.css
deleted file mode 100644
index 50316d7..0000000
--- a/src/common/forum.css
+++ /dev/null
@@ -1,36 +0,0 @@
-.TWPT-badge {
- width: calc(18/13*var(--icon-size, 16px));
- height: calc(18/13*var(--icon-size, 16px));
- border-radius: 50%;
- margin: 4px;
-
- display: inline-flex;
- flex-direction: row;
- flex-wrap: wrap;
- justify-content: center;
- align-content: center;
- align-items: center;
-
- background-color: #009688;
- color: #fff;
- box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.22), 0 2px 2px 0 rgba(0, 0, 0, 0.12);
-
- user-select: none;
-}
-
-.TWPT-badge img {
- height: var(--icon-size, 16px);
- filter: invert(1);
-}
-
-.TWPT-user-profile__user-links {
- margin-top: 8px;
-}
-
-.TWPT-user-link > * {
- vertical-align: middle;
-}
-
-.TWPT-user-link .TWPT-badge {
- margin-left: 0;
-}
diff --git a/src/common/common.js b/src/common/optionsPrototype.json5
similarity index 61%
rename from src/common/common.js
rename to src/common/optionsPrototype.json5
index 4fcae45..191d0e1 100644
--- a/src/common/common.js
+++ b/src/common/optionsPrototype.json5
@@ -1,4 +1,4 @@
-const optionsPrototype = {
+{
// Available options:
'list': {
defaultValue: true,
@@ -118,53 +118,4 @@
defaultValue: false,
context: 'deprecated',
},
-};
-
-const specialOptions = [
- 'profileindicatoralt_months',
- 'ccdarktheme_mode',
- 'ccdarktheme_switch_enabled',
- 'ccdragndropfix',
-];
-
-function isEmpty(obj) {
- return Object.keys(obj).length === 0;
-}
-
-// Adds missing options with their default value. If |dryRun| is set to false,
-// they are also saved to the sync storage area.
-function cleanUpOptions(options, dryRun = false) {
- console.log('[cleanUpOptions] Previous options', JSON.stringify(options));
-
- if (typeof options !== 'object' || options === null) options = {};
-
- var ok = true;
- for (const [opt, optMeta] of Object.entries(optionsPrototype)) {
- if (!(opt in options)) {
- ok = false;
- options[opt] = optMeta['defaultValue'];
- }
- }
-
- console.log('[cleanUpOptions] New options', JSON.stringify(options));
-
- if (!ok && !dryRun) {
- chrome.storage.sync.set(options);
- }
-
- return options;
-}
-
-// This method is based on the fact that when building the extension for Firefox
-// the browser_specific_settings.gecko entry is included.
-function isFirefox() {
- var manifest = chrome.runtime.getManifest();
- return manifest.browser_specific_settings !== undefined &&
- manifest.browser_specific_settings.gecko !== undefined;
-}
-
-// Returns whether the extension is a release version.
-function isReleaseVersion() {
- var manifest = chrome.runtime.getManifest();
- return ('version' in manifest) && manifest.version != '0';
}
diff --git a/src/common/optionsUtils.js b/src/common/optionsUtils.js
new file mode 100644
index 0000000..0efb6c9
--- /dev/null
+++ b/src/common/optionsUtils.js
@@ -0,0 +1,28 @@
+import optionsPrototype from './optionsPrototype.json5';
+import specialOptions from './specialOptions.json5';
+
+export {optionsPrototype, specialOptions};
+
+// Adds missing options with their default value. If |dryRun| is set to false,
+// they are also saved to the sync storage area.
+export function cleanUpOptions(options, dryRun = false) {
+ console.log('[cleanUpOptions] Previous options', JSON.stringify(options));
+
+ if (typeof options !== 'object' || options === null) options = {};
+
+ var ok = true;
+ for (const [opt, optMeta] of Object.entries(optionsPrototype)) {
+ if (!(opt in options)) {
+ ok = false;
+ options[opt] = optMeta['defaultValue'];
+ }
+ }
+
+ console.log('[cleanUpOptions] New options', JSON.stringify(options));
+
+ if (!ok && !dryRun) {
+ chrome.storage.sync.set(options);
+ }
+
+ return options;
+}
diff --git a/src/common/specialOptions.json5 b/src/common/specialOptions.json5
new file mode 100644
index 0000000..55dde20
--- /dev/null
+++ b/src/common/specialOptions.json5
@@ -0,0 +1,6 @@
+[
+ 'profileindicatoralt_months',
+ 'ccdarktheme_mode',
+ 'ccdarktheme_switch_enabled',
+ 'ccdragndropfix',
+]