From d2e8cd066aec2a4f9220226caa81a70c97eedd63 Mon Sep 17 00:00:00 2001 From: Sean Dawson Date: Tue, 1 Oct 2019 17:12:31 +1000 Subject: [PATCH] refactor: create generic interface for SCMs This prepares the project for adding other SCMs. The changes to Babel were due to an issue with shimming Promises and Firefox (https://github.com/mozilla/webextension-polyfill/issues/105#issuecomment-477653576) --- .babelrc | 23 +++++++- package.json | 3 +- src/app/background.ts | 3 +- src/app/constants.ts | 6 -- src/app/main.ts | 36 +++++------- src/app/messenging.ts | 12 ++-- src/app/modes/diff.tsx | 26 ++------- src/app/modes/preview.tsx | 37 +++++------- .../{bitbucket => bitbucket-server}/fetch.ts | 22 ++++++- src/app/scms/bitbucket-server/index.ts | 50 ++++++++++++++++ .../{ => scms/bitbucket-server}/scraper.ts | 11 ++-- .../{bitbucket => bitbucket-server}/types.ts | 21 ++++++- .../{bitbucket => bitbucket-server}/ui.ts | 9 ++- src/app/types/messenging.ts | 8 ++- src/app/types/scms.ts | 58 +++++++++++++++++++ src/app/types/scraper.ts | 19 ------ webpack.shared.config.js | 2 +- 17 files changed, 227 insertions(+), 119 deletions(-) delete mode 100644 src/app/constants.ts rename src/app/scms/{bitbucket => bitbucket-server}/fetch.ts (81%) create mode 100644 src/app/scms/bitbucket-server/index.ts rename src/app/{ => scms/bitbucket-server}/scraper.ts (93%) rename src/app/scms/{bitbucket => bitbucket-server}/types.ts (83%) rename src/app/scms/{bitbucket => bitbucket-server}/ui.ts (85%) create mode 100644 src/app/types/scms.ts delete mode 100644 src/app/types/scraper.ts diff --git a/.babelrc b/.babelrc index 66891bd0..ae20c0c7 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,25 @@ { "presets": [ - ["@babel/preset-env", { "useBuiltIns": "usage", "corejs": 3 }], + [ + "@babel/preset-env", + { + "corejs": 3, + "debug": false, + "exclude": [ + "es.promise", + "es.promise.finally" + ], + "loose": true, + "spec": true, + "targets": { + "browsers": [ + "last 2 Chrome versions", + "last 2 Firefox versions" + ] + }, + "useBuiltIns": "usage" + } + ], "@babel/preset-react" ], "plugins": [ @@ -31,4 +50,4 @@ "@babel/plugin-proposal-do-expressions", "@babel/plugin-proposal-function-bind" ] -} +} \ No newline at end of file diff --git a/package.json b/package.json index 67966968..c53c0dbe 100644 --- a/package.json +++ b/package.json @@ -125,13 +125,12 @@ "engines": { "node": ">= 10.0.0" }, - "browserslist": "defaults", "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } }, "typeCoverage": { - "atLeast": 99.79 + "atLeast": 100 } } diff --git a/src/app/background.ts b/src/app/background.ts index 2df20c56..6a51adce 100644 --- a/src/app/background.ts +++ b/src/app/background.ts @@ -1,5 +1,4 @@ import { browser, Runtime } from "webextension-polyfill-ts"; -import { messages } from "~app/constants"; import { resetTab, toggleDiffOnTab } from "~app/messenging"; import { Message } from "~app/types/messenging"; @@ -65,7 +64,7 @@ browser.runtime.onMessage.addListener( console.log( `Received message from [${senderTabId}]: [${JSON.stringify(message)}]` ); - if (message.type === messages.Supported && message.value === true) { + if (message.type === "Supported" && message.value === true) { console.log("Adding tab to enabled set!"); tabEnabledSet.add(senderTabId); console.log(tabEnabledSet); diff --git a/src/app/constants.ts b/src/app/constants.ts deleted file mode 100644 index 3e36bb5b..00000000 --- a/src/app/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const messages: Readonly> = { - ToggleDiff: "ToggleDiff", - Reset: "Reset", - Supported: "Supported", - BitbucketDataScraped: "BitbucketDataScraped" -}; diff --git a/src/app/main.ts b/src/app/main.ts index e82927ee..d325efa9 100644 --- a/src/app/main.ts +++ b/src/app/main.ts @@ -1,13 +1,8 @@ import { browser, Runtime } from "webextension-polyfill-ts"; -import { messages } from "~app/constants"; import { setTabSupportsMulePreview } from "~app/messenging"; -import { isDiffMode, stopDiff, toggleDiff } from "~app/modes/diff"; -import { isPreviewMode, togglePreview } from "~app/modes/preview"; -import { - getBitbucketDiffElement, - getBitbucketFilePreviewElement, - isRunningInBitbucket -} from "~app/scms/bitbucket/ui"; +import { stopDiff, toggleDiff } from "~app/modes/diff"; +import { togglePreview } from "~app/modes/preview"; +import { bitbucketServerScmModule } from "~app/scms/bitbucket-server"; import { Message } from "~app/types/messenging"; import "../scss/extension.scss"; @@ -28,14 +23,15 @@ browser.runtime.onMessage.addListener( message )}]` ); - if (message.type === messages.ToggleDiff) { - if (getBitbucketDiffElement() !== null) { - toggleDiff(); + const mode = await bitbucketServerScmModule.determineScmMode(); + if (message.type === "ToggleDiff") { + if (mode === "Diff") { + toggleDiff(bitbucketServerScmModule); } - if (getBitbucketFilePreviewElement() !== null) { - togglePreview(); + if (mode === "Preview") { + togglePreview(bitbucketServerScmModule); } - } else if (message.type === messages.Reset) { + } else if (message.type === "Reset") { reset(); } return true; // Enable async @@ -49,10 +45,7 @@ const onReady = () => { const startReadyPolling = () => { const readyPoller = setInterval(() => { - if ( - getBitbucketDiffElement() !== null || - getBitbucketFilePreviewElement() !== null - ) { + if (bitbucketServerScmModule.isReady()) { console.log("[Mule Preview] Ready!"); clearInterval(readyPoller); onReady(); @@ -64,12 +57,13 @@ const startReadyPolling = () => { }, bitbucketPollPeriod); }; -const reset = () => { +const reset = async () => { stopDiff(); // Reset button - setTabSupportsMulePreview(false); + await setTabSupportsMulePreview(false); - if (isRunningInBitbucket() && (isDiffMode() || isPreviewMode())) { + const supported = await bitbucketServerScmModule.isSupported(); + if (supported) { console.log( "[Mule Preview] I'm pretty sure this is the right place but I have to wait for the element to be ready." ); diff --git a/src/app/messenging.ts b/src/app/messenging.ts index 9cbc9d3e..4e079a74 100644 --- a/src/app/messenging.ts +++ b/src/app/messenging.ts @@ -1,5 +1,4 @@ import { browser } from "webextension-polyfill-ts"; -import { messages } from "~app/constants"; import { Message } from "~app/types/messenging"; const sendMessageRobust = async (currentTabId: number, message: Message) => @@ -7,19 +6,18 @@ const sendMessageRobust = async (currentTabId: number, message: Message) => console.warn(`Could not send message: [${error}]. Ignoring...`); }); -export const setTabSupportsMulePreview = async (supported: boolean) => { - await browser.runtime.sendMessage({ - type: messages.Supported, +export const setTabSupportsMulePreview = async (supported: boolean) => + browser.runtime.sendMessage({ + type: "Supported", value: supported }); -}; export const resetTab = async (tabId: number) => sendMessageRobust(tabId, { - type: messages.Reset + type: "Reset" }); export const toggleDiffOnTab = async (tabId: number) => sendMessageRobust(tabId, { - type: messages.ToggleDiff + type: "ToggleDiff" }); diff --git a/src/app/modes/diff.tsx b/src/app/modes/diff.tsx index 4f48d803..f9dbaef8 100644 --- a/src/app/modes/diff.tsx +++ b/src/app/modes/diff.tsx @@ -2,19 +2,9 @@ import { MulePreviewDiffContent } from "@agiledigital/mule-preview"; import * as React from "react"; import * as ReactDOM from "react-dom"; import { browser } from "webextension-polyfill-ts"; -import { getFileContentFromDiff } from "~app/scms/bitbucket/fetch"; -import { DiffContent } from "~app/scms/bitbucket/types"; -import { getBitbucketData } from "~app/scms/bitbucket/ui"; -import { ScraperResponse } from "~app/types/scraper"; +import { DiffContent, ScmModule } from "~app/types/scms"; import { createContainerElement, getMulePreviewElement } from "~app/ui"; -const handleBitbucketData = (bitbucketData: ScraperResponse) => { - if (!bitbucketData.valid) { - throw new Error("Could not fetch Bitbucket data"); - } - return getFileContentFromDiff(bitbucketData); -}; - const handleFileContent = ({ fileA, fileB }: DiffContent) => { const element = document.querySelector("body"); @@ -33,7 +23,7 @@ const handleFileContent = ({ fileA, fileB }: DiffContent) => { ); }; -const startDiff = () => { +const startDiff = async (scmModule: ScmModule) => { if (getMulePreviewElement() !== null) { console.log("[Mule Preview] Already loaded. Will not load again."); return; @@ -42,13 +32,7 @@ const startDiff = () => { console.log( "[Mule Preview] Bitbucket detected. Will attempt to load overlay." ); - getBitbucketData() - .then(handleBitbucketData) - .then(content => { - if (content !== undefined) { - handleFileContent(content); - } - }); + return scmModule.getDiffContent().then(handleFileContent); }; export const stopDiff = () => { @@ -58,11 +42,11 @@ export const stopDiff = () => { } }; -export const toggleDiff = () => { +export const toggleDiff = (scmModule: ScmModule) => { const element = getMulePreviewElement(); if (element === null) { console.log("[Mule Preview] No existing element. Starting diff"); - startDiff(); + startDiff(scmModule); } else { console.log("[Mule Preview] Existing element found. Stopping diff"); stopDiff(); diff --git a/src/app/modes/preview.tsx b/src/app/modes/preview.tsx index af10ef0d..e513815d 100644 --- a/src/app/modes/preview.tsx +++ b/src/app/modes/preview.tsx @@ -1,12 +1,11 @@ import { MulePreviewContent } from "@agiledigital/mule-preview"; -import fetch from "cross-fetch"; import * as React from "react"; import * as ReactDOM from "react-dom"; import { browser } from "webextension-polyfill-ts"; -import { getFileRawUrlFromContentView } from "~app/scms/bitbucket/ui"; +import { ScmModule } from "~app/types/scms"; import { createContainerElement, getMulePreviewElement } from "~app/ui"; -const startPreview = () => { +const startPreview = async (scmModule: ScmModule) => { if (getMulePreviewElement() !== null) { console.log("[Mule Preview] Already loaded. Will not load again."); return; @@ -21,23 +20,17 @@ const startPreview = () => { throw new Error("Could not find body element"); } - const url = getFileRawUrlFromContentView(); - return fetch(url) - .then(response => response.text()) - .then(content => { - const mulePreviewElement = createContainerElement(); - element.appendChild(mulePreviewElement); - ReactDOM.render( - , - mulePreviewElement - ); - }) - .catch(err => { - console.error(err); - }); + return scmModule.getPreviewContent().then(content => { + const mulePreviewElement = createContainerElement(); + element.appendChild(mulePreviewElement); + ReactDOM.render( + , + mulePreviewElement + ); + }); }; export const stopPreview = () => { @@ -47,11 +40,11 @@ export const stopPreview = () => { } }; -export const togglePreview = () => { +export const togglePreview = (scmModule: ScmModule) => { const element = getMulePreviewElement(); if (element === null) { console.log("[Mule Preview] No existing element. Starting preview"); - startPreview(); + startPreview(scmModule); } else { console.log("[Mule Preview] Existing element found. Stopping preview"); stopPreview(); diff --git a/src/app/scms/bitbucket/fetch.ts b/src/app/scms/bitbucket-server/fetch.ts similarity index 81% rename from src/app/scms/bitbucket/fetch.ts rename to src/app/scms/bitbucket-server/fetch.ts index d1021d5e..dfeacb03 100644 --- a/src/app/scms/bitbucket/fetch.ts +++ b/src/app/scms/bitbucket-server/fetch.ts @@ -1,6 +1,11 @@ import fetch from "cross-fetch"; -import { Change, ChangesResponse, DiffContent } from "~app/scms/bitbucket/types"; -import { ValidScraperResponse } from "~app/types/scraper"; +import { + Change, + ChangesResponse, + ScraperResponse, + ValidScraperResponse +} from "~app/scms/bitbucket-server/types"; +import { DiffContent } from "~app/types/scms"; /** * Functions to fetch files from Bitbucket to preview and diff @@ -88,3 +93,16 @@ export const getFileContentFromDiff = async ({ } }); }; + +export const handleBitbucketData = async ( + bitbucketData: ScraperResponse +): Promise => { + if (!bitbucketData.valid) { + throw new Error("Could not fetch Bitbucket data"); + } + const diffContent = await getFileContentFromDiff(bitbucketData); + if (diffContent === undefined) { + throw new Error("Could not fetch Bitbucket diff"); + } + return diffContent; +}; diff --git a/src/app/scms/bitbucket-server/index.ts b/src/app/scms/bitbucket-server/index.ts new file mode 100644 index 00000000..61a90778 --- /dev/null +++ b/src/app/scms/bitbucket-server/index.ts @@ -0,0 +1,50 @@ +import { + DiffContent, + PreviewContent, + ScmMode, + ScmModule +} from "~app/types/scms"; +import { handleBitbucketData } from "./fetch"; +import { + getBitbucketData, + getBitbucketDiffElement, + getBitbucketFilePreviewElement, + getFileRawUrlFromContentView +} from "./ui"; + +/** + * Supports the self hosted version of Atlassian Bitbucket (Bitbucket Server) + * + * Does not support Bitbucket Cloud, this works very differently under the hood + * and will need to be a separate module. + * + * Tested on Atlassian Bitbucket v6.1.1 + * It will probably work on any version of v6 and maybe even earlier versions + * but has not be tested on those versions. + */ +export const bitbucketServerScmModule: ScmModule = { + isSupported: async (): Promise => { + const metaTag = document.querySelector("meta[name=application-name]"); + return metaTag === null + ? false + : metaTag.getAttribute("content") === "Bitbucket"; + }, + isReady: (): boolean => + getBitbucketDiffElement() !== null || + getBitbucketFilePreviewElement() !== null, + determineScmMode: async (): Promise => { + if (new URL(document.URL).pathname.endsWith("diff")) { + return "Diff"; + } + if (new URL(document.URL).pathname.endsWith(".xml")) { + return "Preview"; + } + return "None"; + }, + getDiffContent: async (): Promise => + getBitbucketData().then(handleBitbucketData), + getPreviewContent: async (): Promise => { + const url = getFileRawUrlFromContentView(); + return fetch(url).then(response => response.text()); + } +}; diff --git a/src/app/scraper.ts b/src/app/scms/bitbucket-server/scraper.ts similarity index 93% rename from src/app/scraper.ts rename to src/app/scms/bitbucket-server/scraper.ts index 159271b8..3e28c771 100644 --- a/src/app/scraper.ts +++ b/src/app/scms/bitbucket-server/scraper.ts @@ -1,10 +1,11 @@ -import { messages } from "~app/constants"; import { CommonPageState, + DiffDetails, DiffPageState, - PullRequestPageState -} from "~app/scms/bitbucket/types"; -import { DiffDetails, ScraperResponse } from "~app/types/scraper"; + PullRequestPageState, + ScraperResponse +} from "~app/scms/bitbucket-server/types"; +import { MessageType } from "~app/types/messenging"; // Note: Code in this file has to be injected into the target // browser so it should be free of dependencies and small as possible @@ -96,7 +97,7 @@ import { DiffDetails, ScraperResponse } from "~app/types/scraper"; const payload = preparePayload(bitbucketPageState); document.dispatchEvent( - new CustomEvent(messages.BitbucketDataScraped, { + new CustomEvent("BitbucketDataScraped" as MessageType, { detail: payload }) ); diff --git a/src/app/scms/bitbucket/types.ts b/src/app/scms/bitbucket-server/types.ts similarity index 83% rename from src/app/scms/bitbucket/types.ts rename to src/app/scms/bitbucket-server/types.ts index 5e4a65af..c1395b82 100644 --- a/src/app/scms/bitbucket/types.ts +++ b/src/app/scms/bitbucket-server/types.ts @@ -105,7 +105,22 @@ export type DiffPaths = { readonly toFilePath: string; }; -export type DiffContent = { - readonly fileA?: string; - readonly fileB?: string; +export type ScraperResponse = ValidScraperResponse | InvalidScraperResponse; + +export type DiffDetails = { + readonly sourceRepoId: string; + readonly targetRepoId: string; + readonly targetCommit: string; + readonly sourceCommit: string; +}; + +export type ValidScraperResponse = DiffDetails & { + readonly valid: true; + readonly repoName: string; + readonly path: string; + readonly projectCode: string; +}; + +export type InvalidScraperResponse = { + readonly valid: false; }; diff --git a/src/app/scms/bitbucket/ui.ts b/src/app/scms/bitbucket-server/ui.ts similarity index 85% rename from src/app/scms/bitbucket/ui.ts rename to src/app/scms/bitbucket-server/ui.ts index 3ebd8473..d9930c0d 100644 --- a/src/app/scms/bitbucket/ui.ts +++ b/src/app/scms/bitbucket-server/ui.ts @@ -1,7 +1,6 @@ import { browser } from "webextension-polyfill-ts"; -import { messages } from "~app/constants"; import { injectScript } from "~app/inject"; -import { ScraperResponse } from "~app/types/scraper"; +import { ScraperResponse } from "./types"; /** * Functions to get the state of the Bitbucket UI @@ -40,17 +39,17 @@ export const getCurrentFile = () => { export const getBitbucketData = async (): Promise => { return new Promise((resolve, reject) => { - document.addEventListener(messages.BitbucketDataScraped, (( + document.addEventListener("BitbucketDataScraped", (( event: CustomEvent ) => { - console.log(`Recieved [${messages.BitbucketDataScraped}] event!`); + console.log(`Recieved ["BitbucketDataScraped"] event!`); resolve(event.detail); }) as EventListener); setTimeout( () => reject(new Error("Took too long to scrape Bitbucket data")), 1000 ); - injectScript(browser.extension.getURL("dist/scraper.js"), "body"); + injectScript(browser.extension.getURL("dist/bitbucket-scraper.js"), "body"); }); }; diff --git a/src/app/types/messenging.ts b/src/app/types/messenging.ts index 3638a5ec..38b3344e 100644 --- a/src/app/types/messenging.ts +++ b/src/app/types/messenging.ts @@ -1,4 +1,10 @@ +export type MessageType = + | "ToggleDiff" + | "Reset" + | "Supported" + | "BitbucketDataScraped"; + export type Message = { - readonly type: string; + readonly type: MessageType; readonly value?: boolean; }; diff --git a/src/app/types/scms.ts b/src/app/types/scms.ts new file mode 100644 index 00000000..aad547af --- /dev/null +++ b/src/app/types/scms.ts @@ -0,0 +1,58 @@ +/** + * Whether the current SCM page is viewing a single file (Preview) + * or a diff between two files (Diff). + * Will be None if the the SCM is supported, but the current page doesn't + * give enough context to use Mule Preview. + */ +export type ScmMode = "None" | "Diff" | "Preview"; + +/** + * Represents the content of a file that is being previewed. + */ +export type PreviewContent = string; + +/** + * Represents the content of two files that are being diffed. + */ +export type DiffContent = { + readonly fileA?: string; + readonly fileB?: string; +}; + +/** + * Defines methods to support a specific SCM (e.g. Bitbucket, Github etc.) + */ +export type ScmModule = { + /** + * Returns whether this module is supported on the current page. + * This should be a short running task that does not do anything too + * intensive as it will be called every time a page is loaded. + */ + readonly isSupported: () => Promise; + + /** + * Polled by the host extension to make sure that the page has completely + * finished loaded. Even if the DOM is ready, the SCM might have some + * Javascript that needs to finish executing. + */ + readonly isReady: () => boolean; + + /** + * Determines what mode the current SCM is in. + * This call can be longer running than the `isSupported` method because + * it will only be called if this SCM is detected. + */ + readonly determineScmMode: () => Promise; + + /** + * If the `determineScmMode` returns "Diff", this method will + * return the content of the two files being diffed. + */ + readonly getDiffContent: () => Promise; + + /** + * If the `determineScmMode` returns "Preview", this method will + * return the content of the file being previewed. + */ + readonly getPreviewContent: () => Promise; +}; diff --git a/src/app/types/scraper.ts b/src/app/types/scraper.ts deleted file mode 100644 index 7faa782d..00000000 --- a/src/app/types/scraper.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type ScraperResponse = ValidScraperResponse | InvalidScraperResponse; - -export type DiffDetails = { - readonly sourceRepoId: string; - readonly targetRepoId: string; - readonly targetCommit: string; - readonly sourceCommit: string; -}; - -export type ValidScraperResponse = DiffDetails & { - readonly valid: true; - readonly repoName: string; - readonly path: string; - readonly projectCode: string; -}; - -export type InvalidScraperResponse = { - readonly valid: false; -}; diff --git a/webpack.shared.config.js b/webpack.shared.config.js index 6a3b9f6b..42927408 100644 --- a/webpack.shared.config.js +++ b/webpack.shared.config.js @@ -6,7 +6,7 @@ module.exports = { entry: { main: path.resolve(__dirname, "src/app/main.ts"), background: path.resolve(__dirname, "src/app/background.ts"), - scraper: path.resolve(__dirname, "src/app/scraper.ts") + "bitbucket-scraper": path.resolve(__dirname, "src/app/scms/bitbucket-server/scraper.ts") }, output: {