diff --git a/.gitignore b/.gitignore index 5148e52..7c8234f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# IDE +.vscode +.idea + # Logs logs *.log @@ -29,6 +33,7 @@ build/Release # Dependency directories node_modules jspm_packages +package-lock.json # Optional npm cache directory .npm diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..722a0c9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "always" +} + diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..45f1ca6 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 24.10.0 diff --git a/manifest.json b/manifest.json index 14804d7..6cfcf1e 100644 --- a/manifest.json +++ b/manifest.json @@ -13,8 +13,7 @@ "declarativeNetRequestWithHostAccess", "webRequest" ], - "host_permissions": ["*://*/*"], - "optional_host_permissions": ["*://*/*"], + "host_permissions": ["*://*/*"], "options_ui": { "page": "options.html", "browser_style": true diff --git a/options.html b/options.html index 38ddb1f..e023be0 100644 --- a/options.html +++ b/options.html @@ -1,27 +1,54 @@ - redwood - - + redwood + + -
- - - - - - -
+
+
+ Redirect MongoCDN assets from: + + + +
+
+ + +
diff --git a/options.js b/options.js index 3fd93e2..ceaab28 100644 --- a/options.js +++ b/options.js @@ -1,28 +1,37 @@ +setInputs = (items) => { + document.querySelector('input[name=redirect-mongo-cdn-development]').checked = items.redirectMongoCdnDevelopment; + document.querySelector('input[name=redirect-mongo-cdn-qa]').checked = items.redirectMongoCdnQa; + document.querySelector('input[name=redirect-mongo-cdn-production]').checked = items.redirectMongoCdnProduction; + document.querySelector('input[name=local-assets-url]').value = items.localAssetsUrl; +}; + document.addEventListener('DOMContentLoaded', () => { - // Use default value color = 'red' and likesColor = true. - chrome.storage.sync.get({ - match: 'https://example.com', - replace: 'http://127.0.0.1:80', - wds: true, - webServerPort: 8080, - compressed: false - }, items => { - document.querySelector('input[name=match]').value = items.match - document.querySelector('input[name=replace]').value = items.replace - document.querySelector('input[name=wds]').value = items.wds - document.querySelector('input[name=webServerPort]').value = items.webServerPort - document.querySelector('input[name=compressed]').value = items.compressed - }) + // Use default value color = 'red' and likesColor = true. + chrome.storage.sync.get( + { + redirectMongoCdnDevelopment: true, + redirectMongoCdnQa: false, + redirectMongoCdnProduction: false, + localAssetsUrl: 'http://localhost:8081', + }, + (items) => { + setInputs(items); + } + ); - const getInput = name => document.querySelector(`input[name=${name}]`).value + const getInput = (name) => document.querySelector(`input[name=${name}]`).value; + const getCheckbox = (name) => document.querySelector(`input[name=${name}]`).checked; - document.querySelector('button[name=save]').addEventListener('click', () => { - chrome.storage.sync.set({ - match: getInput('match'), - replace: getInput('replace'), - wds: getInput('wds'), - webServerPort: getInput('webServerPort'), - compressed: getInput('compressed') - }, chrome.runtime.reload) - }) -}) + document.querySelector('button[name=save]').addEventListener('click', () => { + chrome.storage.sync.set( + { + redirectMongoCdnDevelopment: getCheckbox('redirect-mongo-cdn-development'), + redirectMongoCdnQa: getCheckbox('redirect-mongo-cdn-qa'), + redirectMongoCdnProduction: getCheckbox('redirect-mongo-cdn-production'), + localAssetsUrl: getInput('local-assets-url'), + }, + chrome.runtime.reload + ); + window.close(); + }); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..c7e7d44 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "redwood", + "version": "1.0.0", + "description": "Redirect network requests for assets to test production services with local changes.", + "private": true, + "type": "module", + "scripts": { + "test": "node --test", + "test:watch": "node --test --watch" + }, + "devDependencies": { + "@types/chrome": "^0.0.270", + "@types/node": "^24.8.1", + "prettier": "^3.6.2" + } +} + diff --git a/regex_simplifier.js b/regex_simplifier.js new file mode 100644 index 0000000..ea7614b --- /dev/null +++ b/regex_simplifier.js @@ -0,0 +1,182 @@ +/** + * A registry for named capture groups that tracks their numeric indices. + */ +class NamedGroupRegistry { + constructor() { + this.nameToNumber = new Map(); + } + + /** + * Registers a named capture group with its position number. + * @param {string} name - The name of the capture group. + * @param {number} position - The position/number of this capture group. + * @throws {Error} If the named group is already registered. + */ + registerNamedGroup(name, position) { + if (this.nameToNumber.has(name)) { + throw new Error(`Named capture group '${name}' is already defined!`); + } + this.nameToNumber.set(name, position); + } + + /** + * Returns the numeric index for a named capture group. + * @param {string} name - The name of the capture group. + * @returns {number} The numeric index. + * @throws {Error} If the named group is not registered. + */ + getGroupNumber(name) { + if (!this.nameToNumber.has(name)) { + throw new Error(`Named capture group '${name}' is not defined in the search pattern!`); + } + return this.nameToNumber.get(name); + } +} + +/** + * Converts named capture groups to numbered capture groups. + * + * This function takes a regex pattern with .NET-style named capture groups ((?...)) + * and a replacement pattern with JavaScript-style named references (${name}), and converts + * them to numbered capture groups and backreferences. + * + * @param {string} searchRegex - The regex pattern with (?...) syntax. + * @param {string} replaceRegex - The replacement pattern with ${name} syntax. + * @param {Object} [options={}] - Configuration options. + * @param {string} [options.backReferenceSymbol='\\'] - Symbol for backreferences ('\\' or '$'). + * + * @returns {Object} An object containing: + * - searchRegex {string}: The regex pattern with numbered groups. + * - replaceRegex {string}: The replacement pattern with numbered backreferences. + * + * @throws {Error} If duplicate named groups are found or undefined groups are referenced. + * + * @example + * const result = simplifyNamedGroups( + * '(?https?)://(?[^/]+)/(?.*)', + * '${protocol}://localhost/${path}', + * { backReferenceSymbol: '\\' } + * ); + * // Result: + * // searchRegex: '(https?)://([^/]+)/(.*)' + * // replaceRegex: '\\1://localhost/\\3' + */ +export function simplifyNamedGroups(searchRegex, replaceRegex, options = {}) { + options.backReferenceSymbol = options.backReferenceSymbol || '\\'; + + const registry = new NamedGroupRegistry(); + + // Step 1: Scan the entire pattern to count ALL capture groups (both named and regular) + // and register named groups with their correct positions + let groupCounter = 0; + let pos = 0; + + while (pos < searchRegex.length) { + const char = searchRegex[pos]; + + // Skip escaped characters + if (char === '\\') { + pos += 2; + continue; + } + + // Check if this is a capture group + if (char === '(') { + // Check if it's a named group (?...) + if (searchRegex[pos + 1] === '?' && searchRegex[pos + 2] === '<') { + // Extract the name + const nameMatch = searchRegex.slice(pos).match(/^\(\?<([^>]+)>/); + if (nameMatch) { + groupCounter++; + registry.registerNamedGroup(nameMatch[1], groupCounter); + pos += nameMatch[0].length; + continue; + } + } + // Check if it's a non-capturing group (?:...) + else if (searchRegex[pos + 1] === '?' && searchRegex[pos + 2] === ':') { + pos++; + continue; + } + // Check for other non-capturing syntax like (?=...), (?!...), etc. + else if (searchRegex[pos + 1] === '?') { + pos++; + continue; + } + // It's a regular capturing group + else { + groupCounter++; + pos++; + continue; + } + } + + pos++; + } + + // Step 2: Convert search pattern - replace (?...) with (...) + let simplifiedSearch = searchRegex; + let match; + let offset = 0; + + // Match named groups + const namedGroupStart = /\(\?<([^>]+)>/g; + + while ((match = namedGroupStart.exec(searchRegex)) !== null) { + const groupName = match[1]; + const startPos = match.index; + const contentStart = match.index + match[0].length; + + // Find the matching closing parenthesis + let depth = 1; + let endPos = contentStart; + + while (depth > 0 && endPos < searchRegex.length) { + const char = searchRegex[endPos]; + + if (char === '\\') { + // Skip escaped characters + endPos += 2; + continue; + } + + if (char === '(') { + depth++; + } else if (char === ')') { + depth--; + } + + endPos++; + } + + if (depth !== 0) { + throw new Error(`Unmatched parentheses for named group '${groupName}'`); + } + + // Replace (?...) with (...) + const groupContent = searchRegex.slice(contentStart, endPos - 1); + const replacement = `(${groupContent})`; + + simplifiedSearch = + simplifiedSearch.slice(0, startPos + offset) + + replacement + + simplifiedSearch.slice(startPos + offset + (endPos - startPos)); + + // Adjust offset for the length difference + offset += replacement.length - (endPos - startPos); + } + + // Step 3: Parse and convert replacement pattern + // Match ${name} references + const namedRefRegex = /\$\{([^}]+)\}/g; + + const simplifiedReplace = replaceRegex.replace(namedRefRegex, (fullMatch, name) => { + const groupNumber = registry.getGroupNumber(name); + return `${options.backReferenceSymbol}${groupNumber}`; + }); + + return { + searchRegex: simplifiedSearch, + replaceRegex: simplifiedReplace, + }; +} diff --git a/regex_simplifier.test.js b/regex_simplifier.test.js new file mode 100644 index 0000000..a315fbd --- /dev/null +++ b/regex_simplifier.test.js @@ -0,0 +1,236 @@ +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; +import { simplifyNamedGroups } from './regex_simplifier.js'; + +describe('simplifyNamedGroups', () => { + describe('basic conversion', () => { + test('converts single named group', () => { + const result = simplifyNamedGroups('(?[a-z]+)', '${name}', { + backReferenceSymbol: '\\', + }); + + assert.equal(result.searchRegex, '([a-z]+)'); + assert.equal(result.replaceRegex, '\\1'); + }); + + test('converts multiple named groups', () => { + const result = simplifyNamedGroups( + '(?https?)://(?[^/]+)/(?.*)', + '${protocol}://localhost/${path}', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '(https?)://([^/]+)/(.*)'); + assert.equal(result.replaceRegex, '\\1://localhost/\\3'); + }); + + test('handles groups in different order in replacement', () => { + const result = simplifyNamedGroups( + '(?\\w+)-(?\\w+)-(?\\w+)', + '${third}/${second}/${first}', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '(\\w+)-(\\w+)-(\\w+)'); + assert.equal(result.replaceRegex, '\\3/\\2/\\1'); + }); + }); + + describe('backreference symbols', () => { + test('uses backslash by default', () => { + const result = simplifyNamedGroups('(?\\w+)', '${test}'); + + assert.equal(result.replaceRegex, '\\1'); + }); + + test('uses dollar sign when specified', () => { + const result = simplifyNamedGroups('(?\\w+)', '${test}', { + backReferenceSymbol: '$', + }); + + assert.equal(result.replaceRegex, '$1'); + }); + }); + + describe('nested and complex patterns', () => { + test('handles nested non-capturing groups', () => { + const result = simplifyNamedGroups( + '(?test(?:inner)?end)', + '${outer}', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '(test(?:inner)?end)'); + assert.equal(result.replaceRegex, '\\1'); + }); + + test('handles multiple nested parentheses', () => { + const result = simplifyNamedGroups( + '(?a(b(c)d)e)', + '${group}', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '(a(b(c)d)e)'); + assert.equal(result.replaceRegex, '\\1'); + }); + + test('handles escaped parentheses in groups', () => { + const result = simplifyNamedGroups( + '(?test\\(escaped\\))', + '${group}', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '(test\\(escaped\\))'); + assert.equal(result.replaceRegex, '\\1'); + }); + + test('handles complex real-world pattern', () => { + const result = simplifyNamedGroups( + '^(?https?)://(?[^/]+)/static/(?[^/]+)/(?[^.]+)\\.(?[a-f0-9]+)\\.js$', + '${protocol}://localhost/static/${segment}/${name}.js', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '^(https?)://([^/]+)/static/([^/]+)/([^.]+)\\.([a-f0-9]+)\\.js$'); + assert.equal(result.replaceRegex, '\\1://localhost/static/\\3/\\4.js'); + }); + }); + + describe('replacement without all groups', () => { + test('allows referencing only some groups', () => { + const result = simplifyNamedGroups( + '(?\\w+)-(?\\w+)-(?\\w+)', + '${a}-${c}', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '(\\w+)-(\\w+)-(\\w+)'); + assert.equal(result.replaceRegex, '\\1-\\3'); + }); + + test('allows replacement with no group references', () => { + const result = simplifyNamedGroups( + '(?\\w+)', + 'static-value', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '(\\w+)'); + assert.equal(result.replaceRegex, 'static-value'); + }); + }); + + describe('error cases', () => { + test('throws error for duplicate named group', () => { + assert.throws( + () => { + simplifyNamedGroups( + '(?\\w+)-(?\\d+)', + '${name}', + { backReferenceSymbol: '\\' } + ); + }, + { message: "Named capture group 'name' is already defined!" } + ); + }); + + test('throws error for undefined group reference', () => { + assert.throws( + () => { + simplifyNamedGroups( + '(?\\w+)', + '${name}-${undefined}', + { backReferenceSymbol: '\\' } + ); + }, + { message: "Named capture group 'undefined' is not defined in the search pattern!" } + ); + }); + + test('throws error for unmatched parentheses', () => { + assert.throws( + () => { + simplifyNamedGroups( + '(?\\w+', + '${name}', + { backReferenceSymbol: '\\' } + ); + }, + { message: "Unmatched parentheses for named group 'name'" } + ); + }); + }); + + describe('mixed content', () => { + test('handles literal text around groups', () => { + const result = simplifyNamedGroups( + 'prefix-(?\\w+)-suffix', + 'start-${middle}-end', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, 'prefix-(\\w+)-suffix'); + assert.equal(result.replaceRegex, 'start-\\1-end'); + }); + + test('handles special regex characters', () => { + const result = simplifyNamedGroups( + '^(?https?):\\/\\/(?[^/]+)\\/', + '${protocol}://new-${host}/', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '^(https?):\\/\\/([^/]+)\\/'); + assert.equal(result.replaceRegex, '\\1://new-\\2/'); + }); + }); + + describe('mixed numbered and named groups', () => { + test('handles named + numbered + named', () => { + const result = simplifyNamedGroups( + '(?\\w+)-([^-]+)-(?\\w+)', + '${first}/${third}', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '(\\w+)-([^-]+)-(\\w+)'); + assert.equal(result.replaceRegex, '\\1/\\3'); + }); + + test('handles numbered + named', () => { + const result = simplifyNamedGroups( + '(\\d+)-(?\\w+)', + '${name}', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '(\\d+)-(\\w+)'); + assert.equal(result.replaceRegex, '\\2'); + }); + + test('handles named + numbered (no reference to numbered)', () => { + const result = simplifyNamedGroups( + '(?https?)://([^/]+)/(?.*)', + '${protocol}://localhost/${path}', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '(https?)://([^/]+)/(.*)'); + assert.equal(result.replaceRegex, '\\1://localhost/\\3'); + }); + + test('handles multiple numbered groups interspersed', () => { + const result = simplifyNamedGroups( + '(?\\w+)-(\\d+)-(?\\w+)-(\\d+)-(?\\w+)', + '${c}/${b}/${a}', + { backReferenceSymbol: '\\' } + ); + + assert.equal(result.searchRegex, '(\\w+)-(\\d+)-(\\w+)-(\\d+)-(\\w+)'); + assert.equal(result.replaceRegex, '\\5/\\3/\\1'); + }); + }); +}); + diff --git a/rules.js b/rules.js new file mode 100644 index 0000000..fc0825b --- /dev/null +++ b/rules.js @@ -0,0 +1,132 @@ +import {simplifyNamedGroups} from './regex_simplifier.js'; + +/** + * Converts named capture groups to numbered capture groups because Chrome's declarativeNetRequest API only supports numbered capture groups. + * @param {Array} rules - The rules to simplify. + * @returns {Array} The simplified rules. + * @example + * const rules = [ + * { + * condition: { regexFilter: '(?[a-z]+) is (?[0-9]+) years old' }, + * action: { redirect: { regexSubstitution: '${name} is ${age} years old' } }, + * }, + * ]; + * const simplifiedRules = simplifyNamedGroupsInRules(rules); + * console.log(simplifiedRules); + * // [ + * // { + * // condition: { regexFilter: '([a-z]+) is ([0-9]+) years old' }, + * // action: { redirect: { regexSubstitution: '\\1 is \\2 years old' } }, + * // }, + * // ]; + */ +function simplifyNamedGroupsInRules(rules) { + const options = { + backReferenceSymbol: '\\', + }; + for (const rule of rules) { + const search = rule.condition.regexFilter; + const replace = rule.action?.redirect?.regexSubstitution || ''; + const { searchRegex, replaceRegex } = simplifyNamedGroups(search, replace, options); + rule.condition.regexFilter = searchRegex; + if (replace !== '') { + rule.action.redirect.regexSubstitution = replaceRegex; + } + } + return rules; +} + +export function createRules({ + redirectMongoCdnDevelopment, + redirectMongoCdnQa, + redirectMongoCdnProduction, + localAssetsUrl, +}) { + let ruleIdCounter = 1; + + const env = [ + ...(redirectMongoCdnDevelopment ? ['-dev'] : []), + ...(redirectMongoCdnQa ? ['-qa'] : []), + ...(redirectMongoCdnProduction ? [''] : []), + ]; + + const p = { + dash_env: `(?${env.join('|')})`, + mms_prefix: '(?/mms)?', + path: '(?.+)', + name: '(?[^/]+?)', + opt_dot_hash: '(?\\.[0-9a-f]{10,})?', + opt_dot_min: '(?\\.min)?', + asset_ext: '(?js|css|json)', + }; + + return simplifyNamedGroupsInRules([ + // redirect HMR updates + { + id: ruleIdCounter++, + condition: { + regexFilter: `^https:\/\/assets${p.dash_env}\\.mongodb-cdn\\.com${p.mms_prefix}\\/static\\/${p.path}\\/${p.name}\\.hot-update\\.${p.asset_ext}$`, + resourceTypes: ['script', 'stylesheet', 'other', 'xmlhttprequest'], + }, + action: { + type: 'redirect', + redirect: { + regexSubstitution: localAssetsUrl + '/static/${path}/${name}.hot-update.${asset_ext}', + }, + }, + }, + // redirect JS and CSS assets + { + id: ruleIdCounter++, + condition: { + regexFilter: `^https:\/\/assets${p.dash_env}\\.mongodb-cdn\\.com${p.mms_prefix}\\/static\\/${p.path}\\/${p.name}${p.opt_dot_hash}${p.opt_dot_min}\\.${p.asset_ext}$`, + resourceTypes: ['script', 'other', 'stylesheet'], + }, + action: { + type: 'redirect', + redirect: { + regexSubstitution: localAssetsUrl + '/static/${path}/${name}.${asset_ext}', + }, + }, + }, + // CORS for other resources + { + id: ruleIdCounter++, + condition: { + regexFilter: `^${localAssetsUrl}.*$`, + resourceTypes: [ + 'csp_report', + 'font', + 'image', + 'main_frame', + 'media', + 'object', + 'other', + 'ping', + 'script', + 'stylesheet', + 'sub_frame', + 'webbundle', + 'websocket', + 'webtransport', + 'xmlhttprequest', + ], + }, + action: { + type: 'modifyHeaders', + responseHeaders: [ + { + header: 'Access-Control-Allow-Origin', + operation: 'set', + value: '*', + }, + { + header: 'Access-Control-Allow-Headers', + operation: 'set', + value: '*', + }, + ], + }, + }, + ]); +} diff --git a/rules.test.js b/rules.test.js new file mode 100644 index 0000000..a77b076 --- /dev/null +++ b/rules.test.js @@ -0,0 +1,77 @@ +import {createRules} from './rules.js'; +import {describe, test} from 'node:test'; +import assert from 'node:assert'; + +/** + * Helper function to test if a URL matches and redirects correctly + * according to Chrome's declarativeNetRequest rules + */ +function testRedirect(rules, originalUrl) { + for (const rule of rules) { + if (rule.action.type !== 'redirect') continue; + + const regexFilter = rule.condition.regexFilter; + // Convert Chrome's regex format to JavaScript RegExp + // Chrome uses ^ and $ implicitly, and uses \1, \2 for back references + const regexSubstitution = rule.action.redirect.regexSubstitution.replaceAll(/\\(\d+)/g, '$$$1'); + + // Apply the substitution + const jsRegex = new RegExp(regexFilter); + if (jsRegex.test(originalUrl)) { + return originalUrl.replace(jsRegex, regexSubstitution); + } + } + + // No rule matched + return null; +} + +describe('DEV redirections', () => { + const rules = createRules({ + redirectMongoCdnDevelopment: true, + localAssetsUrl: 'http://localhost:8081', + }); + + test('CSS chunk with hash should redirect to minified version', () => { + const result = testRedirect( + rules, + 'https://assets-dev.mongodb-cdn.com/mms/static/dist/bem-components.f5bb320780.css' + ); + assert.strictEqual(result, 'http://localhost:8081/static/dist/bem-components.css'); + }); + + test('JS chunk with hash should redirect to minified version', () => { + const result = testRedirect( + rules, + 'https://assets-dev.mongodb-cdn.com/mms/static/dist/runtime.ebe475bf44114144d0de.js' + ); + assert.strictEqual(result, 'http://localhost:8081/static/dist/runtime.js'); + }); + + test('JS file with two segments should redirect', () => { + const result = testRedirect( + rules, + 'https://assets-dev.mongodb-cdn.com/mms/static/dist/main/ui-access-list-edit-page.js' + ); + assert.strictEqual(result, 'http://localhost:8081/static/dist/main/ui-access-list-edit-page.js'); + }); + + test('HMR hot-update JS file should redirect', () => { + const result = testRedirect( + rules, + 'https://assets-dev.mongodb-cdn.com/mms/static/dist/packages_ai-models_components_UsagePage_tsx.2b6a17246155ab151602.hot-update.js' + ); + assert.strictEqual( + result, + 'http://localhost:8081/static/dist/packages_ai-models_components_UsagePage_tsx.2b6a17246155ab151602.hot-update.js' + ); + }); + + test('HMR hot-update JSON file should redirect', () => { + const result = testRedirect( + rules, + 'https://assets-dev.mongodb-cdn.com/mms/static/dist/main.2b6a17246155ab151602.hot-update.json' + ); + assert.strictEqual(result, 'http://localhost:8081/static/dist/main.2b6a17246155ab151602.hot-update.json'); + }); +}); diff --git a/service_worker.js b/service_worker.js index 2445f9f..564b017 100644 --- a/service_worker.js +++ b/service_worker.js @@ -1,119 +1,22 @@ +import { createRules } from './rules.js'; + chrome.storage.sync .get({ - match: "https://my-source.com", - replace: "http://localhost:8080", - wds: true, - webServerPort: 8080, - compressed: false + redirectMongoCdnDevelopment: true, + redirectMongoCdnQa: false, + redirectMongoCdnProduction: false, + localAssetsUrl: 'http://localhost:8081', }) - .then(async ({ match, replace, wds, webServerPort, compressed }) => { - const replaceURL = new URL(replace); - const likelyWebServerURL = `${replaceURL.protocol}//${replaceURL.hostname}:${webServerPort}`; - - const isWDS = wds === "true"; - const isCompressed = compressed === "true"; - const matchForRegex = match.replaceAll(/\./g, '\\.'); - const likelyWebServerURLForRegex = likelyWebServerURL.replaceAll(/\./g, '\\.'); - - const genRegex = `^${matchForRegex}.*\/static\/([a-z]+)\/([^/]*)\\.[a-fA-F0-9]+\\..*$`; - const appAssetRegex = `^${matchForRegex}.*\/static\/([a-z]+)\/([a-z]+)\/(.*)\\.[a-fA-F0-9]+\\..*$` - const appAssetNoHashRegex = `^${matchForRegex}.*\/static\/([a-z]+)\/([a-z]+)\/(.*)\\..*$` + .then(async (config) => { + chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((info) => { + console.log(JSON.stringify(info)); + }); const oldRules = await chrome.declarativeNetRequest.getDynamicRules(); const oldRuleIds = oldRules.map((rule) => rule.id); - - // Uncomment for seeing matches (not a lot of info). - // chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((info) => { - // console.log(JSON.stringify(info)) - // }) - - const addMin = (type) => { - return isCompressed ? '.min' : ''; - } - const selectURLForRegexSub = (type) => { - return (isWDS && type === 'css') ? likelyWebServerURL : replace; - } - const makeGenAssetRegexSub = (type) => { - return `${selectURLForRegexSub(type)}/static/\\1/\\2${addMin(type)}.${type}` - } - const makeAppAssetRegexSub = (type) => { - return `${selectURLForRegexSub(type)}/static/\\1/\\2/\\3${addMin(type)}.${type}` - } - const makeAppAssetNoHashRegexSub = (type) => { - // min will show up in group 3, if needed. - return `${selectURLForRegexSub(type)}/static/\\1/\\2/\\3.${type}` - } - const cssAssetRegexSub = `${replace}/static/assets/css/\\1${addMin('css')}.css` - + const newRules = createRules(config); chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: oldRuleIds, - addRules: [ - // JS - { - id: 1, - action: { - type: "redirect", - "redirect": { regexSubstitution: makeGenAssetRegexSub('js'), } - }, - "condition": { - regexFilter: genRegex, - "resourceTypes": ["script"] - } - }, - // JS chunk redirected-to hashes - { - id: 2, - action: { - type: "redirect", - "redirect": { regexSubstitution: makeAppAssetNoHashRegexSub('js'), } - }, - "condition": { - regexFilter: appAssetNoHashRegex, - "resourceTypes": ["script"] - } - }, - - // CSS - { - id: 3, - action: { - type: "redirect", - redirect: { - regexSubstitution: makeGenAssetRegexSub('css'), - }, - }, - condition: { - regexFilter: genRegex, - resourceTypes: ["stylesheet"], - }, - }, - { - id: 4, - action: { - type: "redirect", - redirect: { - regexSubstitution: makeAppAssetRegexSub('css'), - }, - }, - condition: { - regexFilter: appAssetRegex, - resourceTypes: ["stylesheet"], - }, - }, - { - id: 5, - action: { - type: "modifyHeaders", - responseHeaders: [ - { header: "Access-Control-Allow-Origin", operation: "set", value: "*" }, - { header: "Access-Control-Allow-Headers", operation: "set", value: "*" }, - ], - }, - condition: { - urlFilter: isWDS ? likelyWebServerURL : replace, - resourceTypes: ["main_frame", "sub_frame", "stylesheet", "script", "image", "font", "object", "xmlhttprequest", "ping", "csp_report", "media", "websocket", "webtransport", "webbundle", "other"], - }, - }, - ], + addRules: newRules, }); });