packages/chrome-extension/src/index.ts (189 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { GitHubPageType } from "./app/github/GitHubPageType";
import { renderSingleEditorApp } from "./app/components/single/singleEditorEdit";
import { FileInfo, iframeContainer, renderSingleEditorReadonlyApp } from "./app/components/single/singleEditorView";
import { renderPrEditorsApp } from "./app/components/pr/prEditors";
import { mainContainer, runAfterUriChange } from "./app/utils";
import { Dependencies } from "./app/Dependencies";
import * as ReactDOM from "react-dom";
import { EditorEnvelopeLocator, KogitoEditorChannelApi } from "@kie-tools-core/editor/dist/api";
import "../resources/style.css";
import { Logger } from "./Logger";
import { Globals } from "./app/components/common/Main";
import { ExternalEditorManager } from "./ExternalEditorManager";
import { ResourceContentServiceFactory } from "./app/components/common/ChromeResourceContentService";
import { renderOpenRepoInExternalEditorApp } from "./app/components/openRepoInExternalEditor/openRepoInExternalEditorApp";
import { StateControl } from "@kie-tools-core/editor/dist/channel";
/**
* Starts a Kogito extension.
*
* @param args.name The extension name. Used to differentiate logs from other extensions.
* @param args.extensionIconUrl The relative path to search for an image that will be the icon used for your extension.
* @param args.githubAuthTokenCookieName The name of the cookie that will hold a GitHub PAT for your extension.
* @param args.editorEnvelopeLocator The file extension mapping to the provided Editors.
* @param args.externalEditorManager The implementation of ExternalEditorManager for your extension.
* @param args.customChannelApiImpl Optional channelApi implementation.
*/
export function startExtension(args: {
name: string;
extensionIconUrl: string;
githubAuthTokenCookieName: string;
editorEnvelopeLocator: EditorEnvelopeLocator;
externalEditorManager?: ExternalEditorManager;
getCustomChannelApiImpl?: (
pageType: GitHubPageType,
fileInfo: FileInfo,
stateControl: StateControl
) => KogitoEditorChannelApi | undefined;
}) {
const logger = new Logger(args.name);
const resourceContentServiceFactory = new ResourceContentServiceFactory();
const dependencies = new Dependencies();
const runInit = () => {
const pageType = discoverCurrentGitHubPageType();
const fileInfo = extractFileInfoFromUrl();
const stateControl = new StateControl();
init({
id: chrome.runtime.id,
logger: logger,
dependencies: dependencies,
githubAuthTokenCookieName: args.githubAuthTokenCookieName,
extensionIconUrl: args.extensionIconUrl,
editorEnvelopeLocator: args.editorEnvelopeLocator,
resourceContentServiceFactory: resourceContentServiceFactory,
externalEditorManager: args.externalEditorManager,
stateControl,
customChannelApiImpl: args.getCustomChannelApiImpl?.(pageType, fileInfo, stateControl),
});
};
runAfterUriChange(logger, () => setTimeout(runInit, 0));
setTimeout(runInit, 0);
}
function init(globals: Globals) {
globals.logger.log(`---`);
globals.logger.log(`Starting GitHub extension.`);
unmountPreviouslyRenderedFeatures(globals.id, globals.logger, globals.dependencies, globals.editorEnvelopeLocator);
const fileInfo = extractFileInfoFromUrl();
const pageType = discoverCurrentGitHubPageType();
if (!globals.dependencies.all.octiconMarkGitHub() || pageType === GitHubPageType.NOT_SUPPORTED) {
globals.logger.warn(
`This is not supported GitHub web page. '${window.location.origin}${window.location.pathname}'`
);
return;
}
if (pageType === GitHubPageType.EDIT) {
renderSingleEditorApp({ ...globals, fileInfo });
} else if (pageType === GitHubPageType.VIEW) {
if (!globals.dependencies.openRepoInExternalEditor.buttonContainerOnRepoFilesList()) {
globals.logger.warn(
"The extension stopped working for this asset view. Please be sure you explore the asset with the latest GitHub instance."
);
} else {
renderSingleEditorReadonlyApp({
...globals,
pageType,
className: "btn ml-2 d-none d-md-block",
container: () => globals.dependencies.openRepoInExternalEditor.buttonContainerOnRepoFilesList()!,
fileInfo,
});
}
} else if (
pageType === GitHubPageType.PR_HOME ||
pageType === GitHubPageType.PR_FILES ||
pageType === GitHubPageType.PR_COMMITS
) {
if (!globals.dependencies.openRepoInExternalEditor.buttonContainerOnPrs()) {
globals.logger.warn(
"The extension stopped working for this pull request view. Please be sure you explore the pull request on the latest GitHub instance."
);
} else {
renderPrEditorsApp({
...globals,
pageType,
className: "btn btn-sm",
container: () => globals.dependencies.openRepoInExternalEditor.buttonContainerOnPrs()!,
});
}
} else if (pageType === GitHubPageType.REPO_HOME) {
if (!globals.dependencies.openRepoInExternalEditor.buttonContainerOnRepoHome()) {
globals.logger.warn(
"The extension stopped working for this repository view. Please be sure you explore the repository on the latest GitHub instance."
);
} else {
renderOpenRepoInExternalEditorApp({
...globals,
pageType,
className: "btn btn-sm",
container: () => globals.dependencies.openRepoInExternalEditor.buttonContainerOnRepoHome()!,
});
}
} else if (pageType === GitHubPageType.CAN_NOT_BE_DETERMINED_FROM_URL) {
// if user uses [GITHUBINSTANCE_BASE_URL]/[ORG]/[REPO] we can not determine it from the url, but it may be still valid scenario for this chrome-extension
if (!globals.dependencies.openRepoInExternalEditor.buttonContainerOnRepoHome()) {
globals.logger.warn(
`The extension stopped working for this view '${window.location.origin}${window.location.pathname}'. Please be sure you explore repository, asset or pull request on the latest GitHub instance.`
);
} else {
// presence of the 'buttonContainerOnRepoHome' locator should mean, user is really on the repository home screen view
renderOpenRepoInExternalEditorApp({
...globals,
pageType,
className: "btn btn-sm",
container: () => globals.dependencies.openRepoInExternalEditor.buttonContainerOnRepoHome()!,
});
}
} else {
throw new Error(`Unknown GitHubPageType ${pageType}`);
}
}
export function extractFileInfoFromUrl() {
const split = window.location.pathname.split("/");
return {
gitRef: split[4],
repo: split[2],
org: split[1],
path: split.slice(5).join("/"),
};
}
function unmountPreviouslyRenderedFeatures(
id: string,
logger: Logger,
dependencies: Dependencies,
editorEnvelopeLocator: EditorEnvelopeLocator
) {
try {
if (mainContainer(id, dependencies.all.body())) {
ReactDOM.unmountComponentAtNode(mainContainer(id, dependencies.all.body())!);
logger.log("Unmounted previous features.");
}
switchHiddenCss(id, dependencies, editorEnvelopeLocator);
} catch (e) {
logger.log("Ignoring exception while unmounting features.");
}
}
function pathnameMatches(regex: string) {
return !!window.location.pathname.match(new RegExp(regex));
}
function switchHiddenCss(id: string, dependencies: Dependencies, editorEnvelopeLocator: EditorEnvelopeLocator) {
if (!editorEnvelopeLocator.getEnvelopeMapping(window.location.pathname)) {
dependencies.singleView.githubTextEditorToReplaceElement()?.classList.remove("hidden");
iframeContainer(id, dependencies)?.classList.add("hidden");
} else {
dependencies.singleView.githubTextEditorToReplaceElement()!.classList.add("hidden");
iframeContainer(id, dependencies)?.classList.remove("hidden");
}
}
export function discoverCurrentGitHubPageType() {
if (pathnameMatches(`.*/.*/edit/.*`)) {
return GitHubPageType.EDIT;
}
if (pathnameMatches(`.*/.*/blob/.*`)) {
return GitHubPageType.VIEW;
}
const isOrgSlashRepoSlashTreeSlashName =
window.location.pathname.split("/tree/").length === 2 && !window.location.pathname.split("/tree/")[1].includes("/");
if (isOrgSlashRepoSlashTreeSlashName) {
return GitHubPageType.REPO_HOME;
}
if (pathnameMatches(`.*/.*/pull/[0-9]+/files.*`)) {
return GitHubPageType.PR_FILES;
}
if (pathnameMatches(`.*/.*/pull/[0-9]+/commits.*`)) {
return GitHubPageType.PR_COMMITS;
}
if (pathnameMatches(`^.*/.*/pull/[0-9]+$`)) {
return GitHubPageType.PR_HOME;
}
if (["/tree/", "/pull/"].some((pathnamePart) => window.location.pathname.includes(pathnamePart))) {
// if pathanme containing one of these substrings and didn't match previous `if` statements, then it is not supperted by our extension
// .../tree/main/some/folders/only
// .../pull/1/checks
return GitHubPageType.NOT_SUPPORTED;
}
return GitHubPageType.CAN_NOT_BE_DETERMINED_FROM_URL;
}
export * from "./ExternalEditorManager";