From 32fa56e91ff3385c8e56aef072f8d1a262452615 Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Thu, 2 Jun 2022 15:18:07 +0800 Subject: [PATCH 01/13] fix: resolve PR page link via response --- src/platforms/GitHub/API.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/platforms/GitHub/API.ts b/src/platforms/GitHub/API.ts index ffa2911..322a89f 100644 --- a/src/platforms/GitHub/API.ts +++ b/src/platforms/GitHub/API.ts @@ -134,23 +134,23 @@ export async function getPullPageDocuments( if (!baseSHA || !headSHA) throw new Error(`Cannot fetch SHA for comparison`) // The SHA used to be retrieved from DOM of the pull page, but they can be unreliable if the PR has conflicts - const search = new URLSearchParams(window.location.search) + let search = new URLSearchParams(window.location.search) + search.set('lines', '0') search.set('sha1', baseSHA) search.set('sha2', headSHA) - let lines = 0 const diffsDOMs: Document[] = [] while (true) { - search.set('lines', lines.toString()) const diffsDOM = await getDOM( `https://${window.location.host}/${userName}/${repoName}/diffs?${search}`, ) diffsDOMs.push(diffsDOM) - if (diffsDOM.querySelector('.js-diff-progressive-container')) { - lines += 3000 - } else { - break - } + const src = diffsDOM + .querySelector('.js-diff-progressive-container include-fragment') + ?.getAttribute('src') + if (!src) break + + search = new URL(src, window.location.origin).searchParams } return diffsDOMs } From f56c8301a954bbab12c18aac2bf857aa92614541 Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Fri, 3 Jun 2022 12:36:46 +0800 Subject: [PATCH 02/13] 3.7.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 776108c..e5c58bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitako", - "version": "3.7.0", + "version": "3.7.1", "description": "File tree for GitHub, and more than that.", "repository": "https://github.com/EnixCoda/Gitako", "author": "EnixCoda", From a9217b5c4ce9764d22348241a4aa033c4126b1fb Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 21 Jun 2022 17:58:30 +0800 Subject: [PATCH 03/13] fix: add overscan to avoid blank flash --- src/components/FileExplorer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/FileExplorer.tsx b/src/components/FileExplorer.tsx index cd472d9..98f18ff 100644 --- a/src/components/FileExplorer.tsx +++ b/src/components/FileExplorer.tsx @@ -309,6 +309,7 @@ function ListView({ width, height, metaData, expandTo, renderNodeContext }: List itemSize={compactFileTree ? 24 : 37} height={height} width={width} + overscanCount={20} > {VirtualNode} From d02c32bb4b54eb8d37b7068e876c1d359c51c165 Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Sat, 25 Jun 2022 12:41:04 +0800 Subject: [PATCH 04/13] feat: add pjax mode setting --- src/components/SideBar.tsx | 3 +- src/components/SideBarBodyWrapper.tsx | 9 ++-- src/components/settings/SettingsBar.tsx | 21 +++++++- src/platforms/GitHub/index.ts | 17 +++++-- src/platforms/platform.d.ts | 2 +- src/styles/index.scss | 67 +++++++++++++------------ src/utils/config/helper.ts | 23 +++++++-- src/utils/hooks/usePJAX.ts | 22 ++++---- 8 files changed, 105 insertions(+), 59 deletions(-) diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index f2b82d5..8d23b6a 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -108,12 +108,11 @@ export function SideBar() { } }, [intelligentToggle, sidebarToggleMode]) + usePJAX() useOnPJAXDone(updateSideBarVisibility) platform.usePlatformHooks?.() - usePJAX() - // Hide sidebar when error due to auth but token is set #128 const hideSidebarOnInvalidToken: boolean = intelligentToggle === null && Boolean(state === 'error-due-to-auth' && accessToken) diff --git a/src/components/SideBarBodyWrapper.tsx b/src/components/SideBarBodyWrapper.tsx index e9a6a98..4698895 100644 --- a/src/components/SideBarBodyWrapper.tsx +++ b/src/components/SideBarBodyWrapper.tsx @@ -2,7 +2,7 @@ import { ResizeHandler } from 'components/ResizeHandler' import { useConfigs } from 'containers/ConfigsContext' import * as React from 'react' import { useDebounce, useWindowSize } from 'react-use' -import { defaultConfigs } from 'utils/config/helper' +import { getDefaultConfigs } from 'utils/config/helper' import { cx } from 'utils/cx' import { setCSSVariable } from 'utils/DOMHelper' import * as features from 'utils/features' @@ -103,6 +103,9 @@ export function SideBarBodyWrapper({ ) const dummySize: [number, number] = React.useMemo(() => [size, size], [size]) + + const defaultSideBarWidth = React.useMemo(() => getDefaultConfigs().sideBarWidth, []); + return (
{ - setSize(defaultConfigs.sideBarWidth) - apply(sizeVariableMountPoint, defaultConfigs.sideBarWidth) + setSize(defaultSideBarWidth) + apply(sizeVariableMountPoint, defaultSideBarWidth) }} onResizeStateChange={state => { blockLeaveRef.current = state === 'resizing' diff --git a/src/components/settings/SettingsBar.tsx b/src/components/settings/SettingsBar.tsx index 4e60491..ded58b3 100644 --- a/src/components/settings/SettingsBar.tsx +++ b/src/components/settings/SettingsBar.tsx @@ -1,9 +1,11 @@ import { Link } from '@primer/components' import { Icon } from 'components/Icon' +import { useConfigs } from 'containers/ConfigsContext' import { VERSION } from 'env' import { platform } from 'platforms' import { GitHub } from 'platforms/GitHub' import * as React from 'react' +import { useUpdateEffect } from 'react-use' import { useStateIO } from 'utils/hooks/useStateIO' import { SimpleField, SimpleToggleField } from '../SimpleToggleField' import { AccessTokenSettings } from './AccessTokenSettings' @@ -19,6 +21,7 @@ export const wikiLinks = { copyFileButton: `${WIKI_HOME_LINK}/Copy-file-and-snippet`, copySnippet: `${WIKI_HOME_LINK}/Copy-file-and-snippet`, createAccessToken: `${WIKI_HOME_LINK}/Access-token-for-Gitako`, + pjaxMode: `${WIKI_HOME_LINK}/Pjax-Mode`, } type Props = { @@ -30,7 +33,9 @@ function SettingsBarContent() { const useReloadHint = useStateIO('') const { value: reloadHint } = useReloadHint - const moreFields: SimpleField<'copyFileButton' | 'copySnippetButton'|'codeFolding'>[] = + const moreFields: SimpleField< + 'copyFileButton' | 'copySnippetButton' | 'codeFolding' | 'pjaxMode' + >[] = platform === GitHub ? [ { @@ -39,6 +44,16 @@ function SettingsBarContent() { wikiLink: wikiLinks.codeFolding, tooltip: `Read more in Gitako's Wiki`, }, + { + key: 'pjaxMode', + label: 'Native PJAX mode', + wikiLink: wikiLinks.pjaxMode, + tooltip: 'Please keep it enabled unless Gitako crashes after redirecting', + overwrite: { + value: pjaxMode => pjaxMode === 'native', + onChange: checked => (checked ? 'native' : 'pjax-api'), + }, + }, { key: 'copyFileButton', label: 'Copy file button', @@ -54,6 +69,10 @@ function SettingsBarContent() { ] : [] + useUpdateEffect(() => { + window.location.reload() + }, [useConfigs().value.pjaxMode]) + return ( <>

Settings

diff --git a/src/platforms/GitHub/index.ts b/src/platforms/GitHub/index.ts index e21855c..c9a8d15 100644 --- a/src/platforms/GitHub/index.ts +++ b/src/platforms/GitHub/index.ts @@ -1,6 +1,7 @@ import { useConfigs } from 'containers/ConfigsContext' import { GITHUB_OAUTH } from 'env' import { Base64 } from 'js-base64' +import { configRef } from 'utils/config/helper' import { run } from 'utils/general' import { resolveGitModules } from 'utils/gitSubmodule' import { sortFoldersToFront } from 'utils/treeParser' @@ -169,7 +170,10 @@ export const GitHub: Platform = { return await getRepositoryTreeData(metaData, path, recursive, accessToken) }, shouldShow() { - return Boolean(DOMHelper.isInCodePage() || (URLHelper.isInPullPage() && !DOMHelper.isNativePRFileTreeShown())) + return Boolean( + DOMHelper.isInCodePage() || + (URLHelper.isInPullPage() && !DOMHelper.isNativePRFileTreeShown()), + ) }, shouldExpandAll() { return Boolean(URLHelper.isInPullPage()) @@ -205,8 +209,8 @@ export const GitHub: Platform = { useGitHubCodeFold(codeFolding) useEnterpriseStatBarStyleFix() }, - delegatePJAXProps(options) { - if (!options?.node || options.node.type === 'blob') + delegatePJAXProps: options => { + if (configRef.pjaxMode === 'native' && (!options?.node || options.node.type === 'blob')) return { 'data-pjax': pjaxContainerSelector, onClick() { @@ -214,8 +218,11 @@ export const GitHub: Platform = { }, } }, - loadWithPJAX(url, element) { - element.click() + loadWithPJAX: (url, element) => { + if (configRef.pjaxMode === 'native') { + element.click() + return true + } }, } diff --git a/src/platforms/platform.d.ts b/src/platforms/platform.d.ts index e555d7a..8431249 100644 --- a/src/platforms/platform.d.ts +++ b/src/platforms/platform.d.ts @@ -26,6 +26,6 @@ type Platform = { delegatePJAXProps?(options?: { node?: TreeNode }): void | (React.DOMAttributes & Record) // support data-* attributes - loadWithPJAX?(url: string, element: HTMLElement): void + loadWithPJAX?(url: string, element: HTMLElement): boolean | void usePlatformHooks?(): void } diff --git a/src/styles/index.scss b/src/styles/index.scss index 60570e5..ddf9ad4 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -883,6 +883,40 @@ $minimal-z-index: max( overflow: auto; position: relative; + .select-wrapper { + position: relative; + + select { + width: 100%; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + // make it look like text inputs + height: 36px; + padding: 0 6px; + border-radius: 6px; + border: 1px solid var(--gitako-border-default); + background: var(--gitako-canvas-default); + color: var(--gitako-fg-default); + box-shadow: var(--gitako-primer-shadow-inset); + } + + .chevron { + position: absolute; + right: 6px; + top: 8px; + width: 10px; + height: 20px; + + &::before { + width: 10px; + height: 20px; + background-color: var(--gitako-fg-subtle); + @include pseudo-primer-icon('chevron-down'); + } + } + } + .shadow-shelter { position: absolute; width: 100%; @@ -914,39 +948,6 @@ $minimal-z-index: max( cursor: not-allowed; } } - .select-wrapper { - position: relative; - - select { - width: 100%; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - // make it look like text inputs - height: 36px; - padding: 0 6px; - border-radius: 6px; - border: 1px solid var(--gitako-border-default); - background: var(--gitako-canvas-default); - color: var(--gitako-fg-default); - box-shadow: var(--gitako-primer-shadow-inset); - } - - .chevron { - position: absolute; - right: 6px; - top: 8px; - width: 10px; - height: 20px; - - &::before { - width: 10px; - height: 20px; - background-color: var(--gitako-fg-subtle); - @include pseudo-primer-icon('chevron-down'); - } - } - } &.field-checkbox { padding-left: 20px; vertical-align: middle; diff --git a/src/utils/config/helper.ts b/src/utils/config/helper.ts index ea3c369..e541087 100644 --- a/src/utils/config/helper.ts +++ b/src/utils/config/helper.ts @@ -1,4 +1,5 @@ import { SearchMode } from 'components/searchModes' +import { platformName } from 'platforms' import { storageHelper } from 'utils/storageHelper' import { migrateConfig } from './migrations' @@ -21,6 +22,7 @@ export type Config = { compactFileTree: boolean restoreExpandedFolders: boolean showDiffInText: boolean + pjaxMode: 'native' | 'pjax-api' } enum configKeys { @@ -42,18 +44,20 @@ enum configKeys { compactFileTree = 'compactFileTree', restoreExpandedFolders = 'restoreExpandedFolders', showDiffInText = 'showDiffInText', + pjaxMode = 'pjaxMode', } -// do NOT use platform name +// NOT use platform name to distinguish GHE from github.com const platformStorageKey = `platform_` + window.location.host.toLowerCase() +const isInGitHub = platformStorageKey === 'platform_github.com' -export const defaultConfigs: Config = { +export const getDefaultConfigs: () => Config = () => ({ sideBarWidth: 260, shortcut: undefined, accessToken: '', compressSingletonFolder: true, - copyFileButton: platformStorageKey !== 'platform_github.com', // false when on github.com, - copySnippetButton: platformStorageKey !== 'platform_github.com', // false when on github.com + copyFileButton: !isInGitHub, // disable on github.com + copySnippetButton: !isInGitHub, // disable on github.com intelligentToggle: null, icons: 'rich', toggleButtonVerticalDistance: 124, // align with GitHub's navbar items @@ -66,11 +70,13 @@ export const defaultConfigs: Config = { compactFileTree: false, restoreExpandedFolders: true, showDiffInText: false, -} + pjaxMode: platformName === 'GitHub' ? 'native' : 'pjax-api', // use native on GitHub +}) const configKeyArray = Object.values(configKeys) function applyDefaultConfigs(configs: Partial) { + const defaultConfigs = getDefaultConfigs() return configKeyArray.reduce((applied, key) => { Object.assign(applied, { [key]: key in configs ? configs[key] : defaultConfigs[key] }) return applied @@ -79,9 +85,15 @@ function applyDefaultConfigs(configs: Partial) { export type VersionedConfig = Record & { configVersion: string } +export const configRef: Partial = {} +const updateConfigRef = async (config: Partial) => { + Object.assign(configRef, config) +} + const prepareConfig = new Promise(async resolve => { await migrateConfig() resolve() + updateConfigRef(await get()) }) async function get(): Promise { @@ -91,6 +103,7 @@ async function get(): Promise { } async function set(config: Config) { + updateConfigRef(config) return await storageHelper.set({ [platformStorageKey]: config }) } diff --git a/src/utils/hooks/usePJAX.ts b/src/utils/hooks/usePJAX.ts index 396dcaf..ba06191 100644 --- a/src/utils/hooks/usePJAX.ts +++ b/src/utils/hooks/usePJAX.ts @@ -1,4 +1,5 @@ -import { Config, Pjax } from 'pjax-api' +import { useConfigs } from 'containers/ConfigsContext' +import { Config } from 'pjax-api' import { platform } from 'platforms' import * as React from 'react' import { useEvent } from 'react-use' @@ -28,14 +29,18 @@ const config: Config = { } export function usePJAX() { + const { pjaxMode } = useConfigs().value // make history travel work React.useEffect(() => { - new Pjax({ - ...config, - filter() { - return false - }, - }) + if (pjaxMode === 'pjax-api') { + const { Pjax } = require('pjax-api') + new Pjax({ + ...config, + filter() { + return false + }, + }) + } }, []) // bindings for legacy support @@ -44,8 +49,7 @@ export function usePJAX() { } export const loadWithPJAX = (url: string, element: HTMLElement) => { - if (platform.loadWithPJAX) platform.loadWithPJAX(url, element) - else Pjax.assign(url, config) + platform.loadWithPJAX?.(url, element) || require('pjax-api').Pjax.assign(url, config) } export function useOnPJAXDone(callback: () => void) { From 1289ccdbd64f9e6fc07e6497aacf30bce1bf023d Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Sat, 25 Jun 2022 13:08:51 +0800 Subject: [PATCH 05/13] test: update project page test due to file list overscan --- __tests__/cases/non-parallel/project-page.gitako.ts | 6 +++--- __tests__/utils.ts | 11 ++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/__tests__/cases/non-parallel/project-page.gitako.ts b/__tests__/cases/non-parallel/project-page.gitako.ts index 6a831a9..8182fda 100644 --- a/__tests__/cases/non-parallel/project-page.gitako.ts +++ b/__tests__/cases/non-parallel/project-page.gitako.ts @@ -7,7 +7,7 @@ import { } from '../../utils' describe(`in Gitako project page`, () => { - beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako')) + beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako/tree/test/200-changed-files-200-lines-each')) it('should render Gitako', async () => { await expectToFind('.gitako-side-bar .gitako-side-bar-body-wrapper') @@ -22,11 +22,11 @@ describe(`in Gitako project page`, () => { const filesEle = await page.waitForSelector('.gitako-side-bar .files') // node of tsconfig.json should NOT be rendered before scroll down - await expectToNotFind(selectFileTreeItem('package.json')) + await expectToNotFind(selectFileTreeItem('tsconfig.json')) const box = await filesEle?.boundingBox() if (box) { await page.mouse.move(box.x + 40, box.y + 40) - await scroll({ totalDistance: 200, duration: 1000 }) + await scroll({ totalDistance: 7000, stepDistance: 100 }) // node of tsconfig.json should be rendered now await expectToFind(selectFileTreeItem('tsconfig.json')) diff --git a/__tests__/utils.ts b/__tests__/utils.ts index b4b3d61..88d5238 100644 --- a/__tests__/utils.ts +++ b/__tests__/utils.ts @@ -12,17 +12,14 @@ export function sleep(timeout: number) { export async function scroll({ totalDistance, - step = 1, - duration = 500, + stepDistance = 100, }: { totalDistance: number - step?: number - duration?: number + stepDistance?: number }) { let distance = 0 - while ((distance += step) < totalDistance) { - await (page.mouse as any).wheel({ deltaY: step }) - await sleep((duration * step) / totalDistance) + while ((distance += stepDistance) < totalDistance) { + await (page.mouse as any).wheel({ deltaY: stepDistance }) } } From 98ee885254521926996fdc692ff9eba1d3344e0a Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Sat, 25 Jun 2022 13:10:00 +0800 Subject: [PATCH 06/13] 3.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5c58bb..af1e257 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitako", - "version": "3.7.1", + "version": "3.7.2", "description": "File tree for GitHub, and more than that.", "repository": "https://github.com/EnixCoda/Gitako", "author": "EnixCoda", From 05507abbcbc976d47e275bbd4b59d227f4ff1165 Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Tue, 28 Jun 2022 16:59:20 +0800 Subject: [PATCH 07/13] fix: support GitHub turbo --- src/components/SideBar.tsx | 8 ++++- src/content.tsx | 10 ++++-- src/platforms/GitHub/index.ts | 2 ++ src/utils/DOMHelper.ts | 59 +++++++++++++++++++++++++++++++++-- src/utils/hooks/usePJAX.ts | 7 ++++- 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index 8d23b6a..ebee564 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -63,7 +63,7 @@ export function SideBar() { : intelligentToggle, ) const shouldShow = $shouldShow.value - React.useEffect(() => { + const toggleBodyIndent = React.useCallback(() => { if (sidebarToggleMode === 'persistent') { DOMHelper.setBodyIndent(shouldShow) } else { @@ -75,6 +75,12 @@ export function SideBar() { } }, [shouldShow, sidebarToggleMode]) + React.useEffect(() => { + toggleBodyIndent() + }, [toggleBodyIndent]) + + useOnPJAXDone(toggleBodyIndent) + // Save expand state on toggle if auto expand is off React.useEffect(() => { if (intelligentToggle !== null) { diff --git a/src/content.tsx b/src/content.tsx index cc11087..71cb78e 100644 --- a/src/content.tsx +++ b/src/content.tsx @@ -4,6 +4,11 @@ import { addMiddleware } from 'driver/connect' import { platform } from 'platforms' import * as React from 'react' import * as ReactDOM from 'react-dom' +import { + insertLogoMountPoint, + insertSideBarMountPoint, + persistGitakoElements +} from 'utils/DOMHelper' import './content.scss' if (platform.resolvePartialMetaData()) { @@ -18,8 +23,9 @@ if (platform.resolvePartialMetaData()) { async function init() { await injectStyles(browser.runtime.getURL('content.css')) - const SideBarElement = document.createElement('div') - document.body.appendChild(SideBarElement) + const SideBarElement = insertSideBarMountPoint() + const logoElement = insertLogoMountPoint() + persistGitakoElements(SideBarElement, logoElement) ReactDOM.render(, SideBarElement) } diff --git a/src/platforms/GitHub/index.ts b/src/platforms/GitHub/index.ts index c9a8d15..c7d5592 100644 --- a/src/platforms/GitHub/index.ts +++ b/src/platforms/GitHub/index.ts @@ -112,6 +112,7 @@ const pathSHAMap = new Map() const pjaxContainerSelector = ['#repo-content-pjax-container', '#js-repo-pjax-container'].find( selector => document.querySelector(selector), ) +const turboContainerId = 'repo-content-turbo-frame' export const GitHub: Platform = { isEnterprise, @@ -213,6 +214,7 @@ export const GitHub: Platform = { if (configRef.pjaxMode === 'native' && (!options?.node || options.node.type === 'blob')) return { 'data-pjax': pjaxContainerSelector, + 'data-turbo-frame': turboContainerId, onClick() { /* Overwriting default onClick */ }, diff --git a/src/utils/DOMHelper.ts b/src/utils/DOMHelper.ts index b369b46..3b7eff6 100644 --- a/src/utils/DOMHelper.ts +++ b/src/utils/DOMHelper.ts @@ -36,17 +36,17 @@ export function setBodyIndent(shouldShowGitako: boolean) { } export function $(selector: string): HTMLElement | null -export function $(selector: string, existCallback: (element: HTMLElement) => T1): T1 +export function $(selector: string, existCallback: (element: HTMLElement) => T1): T1 | null export function $( selector: string, existCallback: (element: HTMLElement) => T1, otherwise: () => T2, -): T1 | T2 +): T1 | T2 | null export function $( selector: string, existCallback: undefined | null, otherwise: () => T2, -): HTMLElement | null | T2 +): HTMLElement | T2 export function $(selector: string, existCallback?: any, otherwise?: any) { const element = document.querySelector(selector) if (element) { @@ -55,6 +55,15 @@ export function $(selector: string, existCallback?: any, otherwise?: any) { return otherwise ? otherwise() : null } +export function insertSideBarMountPoint() { + const mountPointID = 'gitako-mount-point-wrapper' + const sideBarElement = document.createElement('div') + sideBarElement.setAttribute('data-turbo-permanent', '') + sideBarElement.setAttribute('id', mountPointID) + document.body.appendChild(sideBarElement) + return sideBarElement +} + /** * add the logo element into DOM */ @@ -64,6 +73,7 @@ export function insertLogoMountPoint() { return $(logoSelector, undefined, function createLogoMountPoint() { const logoMountElement = document.createElement('div') logoMountElement.setAttribute('id', logoID) + logoMountElement.setAttribute('data-turbo-permanent', '') document.body.appendChild(logoMountElement) return logoMountElement }) @@ -142,3 +152,46 @@ export function setCSSVariable(name: string, value: string | undefined, element: if (value === undefined) element.style.removeProperty(name) else element.style.setProperty(name, value) } + +/** + * Unlike the good-old-PJAX-time, now GitHub replaces whole body element after redirecting using turbo. + * If move Gitako mount point from `body` to `html`, Gitako style would break because it inherits style from GitHub body. + * The temporary solution is recovery Gitako elements once the body is removed. + */ +export function persistGitakoElements(SideBarElement: HTMLElement, logoElement: HTMLElement) { + const observer = new MutationObserver(mutations => { + for (const { addedNodes, removedNodes } of mutations) { + const [addedBody, removedBody] = [addedNodes, removedNodes].map(findBodyElement) + if (addedBody && removedBody) { + // hard-coded list due to limited time + // TODO: refactor in a better practice + + // migrate gitako attributes, e.g. class + const propertiesNeedToMigrate = ['--gitako-width'] + for (const property of propertiesNeedToMigrate) { + const oldValue = removedBody.style.getPropertyValue(property) + if (oldValue) addedBody.style.setProperty(property, oldValue) + } + const cssClassesNeedToMigrate = ['with-gitako-spacing'] + for (const cssClass of cssClassesNeedToMigrate) { + if (removedBody.classList.contains(cssClass)) addedBody.classList.add(cssClass) + } + + // move gitako elements + if (!addedBody.contains(SideBarElement)) addedBody.appendChild(SideBarElement) + if (removedBody.contains(SideBarElement)) removedBody.removeChild(SideBarElement) + if (!addedBody.contains(logoElement)) addedBody.appendChild(logoElement) + if (removedBody.contains(logoElement)) removedBody.removeChild(logoElement) + } + } + + function findBodyElement(addedNodes: NodeList) { + return Array.from(addedNodes).find(addedNode => addedNode instanceof HTMLBodyElement) as + | HTMLBodyElement + | undefined + } + }) + observer.observe(document.documentElement, { + childList: true, + }) +} diff --git a/src/utils/hooks/usePJAX.ts b/src/utils/hooks/usePJAX.ts index ba06191..80e37bc 100644 --- a/src/utils/hooks/usePJAX.ts +++ b/src/utils/hooks/usePJAX.ts @@ -4,6 +4,8 @@ import { platform } from 'platforms' import * as React from 'react' import { useEvent } from 'react-use' +// TODO: rename PJAX + const config: Config = { areas: [ // github @@ -53,7 +55,10 @@ export const loadWithPJAX = (url: string, element: HTMLElement) => { } export function useOnPJAXDone(callback: () => void) { - useEvent('pjax:end', callback, document) + useEvent('pjax:end', callback, document) // legacy support + // 'turbo:render' should be the best timing but GitHub has attached a mutation observer on body to block that + // TODO: fire at turbo:render + useEvent('turbo:load', callback, document) } export function useRedirectedEvents( From c1f8456e7f43e2e95b340066b670893479b98eff Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Tue, 28 Jun 2022 17:03:20 +0800 Subject: [PATCH 08/13] fix: handle type errors --- src/platforms/GitHub/DOMHelper.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platforms/GitHub/DOMHelper.ts b/src/platforms/GitHub/DOMHelper.ts index 32478b7..5cc4adb 100644 --- a/src/platforms/GitHub/DOMHelper.ts +++ b/src/platforms/GitHub/DOMHelper.ts @@ -7,8 +7,8 @@ import { CopyFileButton, copyFileButtonClassName } from './CopyFileButton' export function resolveMeta(): Partial { const metaData = { - userName: $('[itemprop="author"] > a[rel="author"]', e => e.textContent?.trim()), - repoName: $('[itemprop="name"] > a[href]', e => e.textContent?.trim()), + userName: $('[itemprop="author"] > a[rel="author"]', e => e.textContent?.trim()) || undefined, + repoName: $('[itemprop="name"] > a[href]', e => e.textContent?.trim()) || undefined, branchName: getCurrentBranch(true), } if (!metaData.userName || !metaData.repoName) { @@ -56,7 +56,7 @@ export function getCurrentBranch(passive = false) { const findFileButtonSelector = '#js-repo-pjax-container .repository-content .file-navigation a[data-hotkey="t"]' - const urlFromFindFileButton: string | undefined = $( + const urlFromFindFileButton: string | null = $( findFileButtonSelector, element => (element as HTMLAnchorElement).href, ) From b112de229efe3edecbe20bb9752e4faddd98c90b Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Tue, 28 Jun 2022 23:12:45 +0800 Subject: [PATCH 09/13] 3.7.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af1e257..dee84ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitako", - "version": "3.7.2", + "version": "3.7.3", "description": "File tree for GitHub, and more than that.", "repository": "https://github.com/EnixCoda/Gitako", "author": "EnixCoda", From bcc34949abd71b83752853cd9d0bb9d14da028d6 Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Sat, 2 Jul 2022 23:23:51 +0800 Subject: [PATCH 10/13] feat: atomic async function --- src/utils/general.test.ts | 36 +++++++++++++++++++++++++++++++++++- src/utils/general.ts | 8 ++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/utils/general.test.ts b/src/utils/general.test.ts index 205acc8..acb3949 100644 --- a/src/utils/general.test.ts +++ b/src/utils/general.test.ts @@ -1,4 +1,4 @@ -import { resolveDiffGraphMeta } from './general' +import { atomicAsyncFunction, resolveDiffGraphMeta } from './general' it(`should resolve diff stat graph meta properly`, () => { const example = ` @@ -27,3 +27,37 @@ it(`should resolve diff stat graph meta properly`, () => { expect([meta.g, meta.r]).toEqual([g, r]) }) }) + +it(`should schedule atomic promises properly`, async () => { + const sleep = (duration: number) => new Promise(resolve => setTimeout(resolve, duration)) + + const recorder: string[] = [] + const sleepWithNoise = async (duration: number, noise: string) => { + await sleep(duration) + recorder.push(noise) + return noise + } + + const atomicSleep = atomicAsyncFunction(sleepWithNoise) + + recorder.length = 0 + const atomicReturns = await Promise.all([atomicSleep(200, 'a'), atomicSleep(100, 'b')]) + // Expected time sheet + // 0 100 200 300 + // [a ] + // [b ] + // Recorder: [a, b] + // + expect(recorder).toEqual(['a', 'b']) + expect(atomicReturns).toEqual(['a', 'b']) + + recorder.length = 0 + const normalReturns = await Promise.all([sleepWithNoise(200, 'a'), sleepWithNoise(100, 'b')]) + // Time sheet if not atomic + // 0 100 200 + // [a ] + // [b ] + // Recorder: [b, a] + expect(recorder).toEqual(['b', 'a']) + expect(normalReturns).toEqual(['a', 'b']) +}) diff --git a/src/utils/general.ts b/src/utils/general.ts index 619b889..a71dcf3 100644 --- a/src/utils/general.ts +++ b/src/utils/general.ts @@ -219,3 +219,11 @@ export function resolveDiffGraphMeta(additions: number, deletions: number, chang w = 5 - g - r return { g, r, w } } + +export function atomicAsyncFunction(fn: (...args: Args) => Promise) { + let last: Promise | undefined + return async (...args: Args) => { + last = last ? last.then(() => fn(...args)) : fn(...args) + return last + } +} From 919ce07b20e252731bdd23cabd5a851dfb9b293f Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Sat, 2 Jul 2022 23:09:49 +0800 Subject: [PATCH 11/13] feat: cache report error --- src/analytics.ts | 35 ++++++++++++++++++- src/utils/config/helper.ts | 4 +-- src/utils/config/migrations/1.3.4.ts | 3 +- .../config/migrations/clearRaiseErrorCache.ts | 13 +++++++ src/utils/config/migrations/index.ts | 22 ++++++++---- src/utils/storageHelper.ts | 13 +++++-- 6 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 src/utils/config/migrations/clearRaiseErrorCache.ts diff --git a/src/analytics.ts b/src/analytics.ts index 2d9a6cb..456838c 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -2,6 +2,8 @@ import * as Sentry from '@sentry/browser' import { Middleware } from 'driver/connect.js' import { IN_PRODUCTION_MODE, VERSION } from 'env' import { platform } from 'platforms' +import { atomicAsyncFunction } from 'utils/general' +import { storageHelper, storageKeys } from 'utils/storageHelper' const PUBLIC_KEY = 'd22ec5c9cc874539a51c78388c12e3b0' const PROJECT_ID = '1406497' @@ -69,12 +71,43 @@ export const withErrorLog: Middleware = function withErrorLog(method, args) { ] } -export function raiseError( +// 1. Only cache errors for current version, so that future errors can still be exposed +// - Run migration to clean on every update + +// 2. Only cache the top 2 levels of stack, e.g. +// ``` +// Error: cannot get current branch +// at Module.getCurrentBranch (chrome-extension://______id______/index.js:1:1)" +// ``` +// So that different initial callees would not result in multiple records +const MAX_STACK_LEVEL = 2 +const hasTheErrorBeenReported = atomicAsyncFunction(async function hasTheErrorBeenReported( + error: Error, +) { + const message = error.stack?.split('\n').slice(0, MAX_STACK_LEVEL).join('\n') + if (!message) return true // ignore errors that has no stack + + type ErrorCache = string + const cache: ErrorCache[] = + (await storageHelper.get(storageKeys.raiseErrorCache))?.[storageKeys.raiseErrorCache] || [] + const has = cache.includes(message) + + if (!has) { + cache.push(message) + await storageHelper.set({ [storageKeys.raiseErrorCache]: cache }) + } + + return has +}) + +export async function raiseError( error: Error, extra?: { [key: string]: any }, ) { + if (await hasTheErrorBeenReported(error)) return + if (!IN_PRODUCTION_MODE || platform.isEnterprise()) { // ignore errors from enterprise to get less noise on Sentry console.error(error) diff --git a/src/utils/config/helper.ts b/src/utils/config/helper.ts index e541087..1dc332e 100644 --- a/src/utils/config/helper.ts +++ b/src/utils/config/helper.ts @@ -1,6 +1,6 @@ import { SearchMode } from 'components/searchModes' import { platformName } from 'platforms' -import { storageHelper } from 'utils/storageHelper' +import { Storage, storageHelper } from 'utils/storageHelper' import { migrateConfig } from './migrations' export type Config = { @@ -83,7 +83,7 @@ function applyDefaultConfigs(configs: Partial) { }, {} as Config) } -export type VersionedConfig = Record & { configVersion: string } +export type VersionedConfig = Record & Storage export const configRef: Partial = {} const updateConfigRef = async (config: Partial) => { diff --git a/src/utils/config/migrations/1.3.4.ts b/src/utils/config/migrations/1.3.4.ts index 9fd8cd9..7131416 100644 --- a/src/utils/config/migrations/1.3.4.ts +++ b/src/utils/config/migrations/1.3.4.ts @@ -1,12 +1,11 @@ import { storageHelper } from 'utils/storageHelper' import { Migration } from '.' -import { Storage } from '../../storageHelper' import { Config, VersionedConfig } from '../helper' export const migration: Migration = { version: '1.3.4', async migrate(version) { - const config: any | void = await storageHelper.get & Storage>([ + const config: any | void = await storageHelper.get>([ 'configVersion', 'platform_undefined', 'platform_GitHub', diff --git a/src/utils/config/migrations/clearRaiseErrorCache.ts b/src/utils/config/migrations/clearRaiseErrorCache.ts new file mode 100644 index 0000000..a467457 --- /dev/null +++ b/src/utils/config/migrations/clearRaiseErrorCache.ts @@ -0,0 +1,13 @@ +import { storageHelper } from 'utils/storageHelper' +import { Migration, onConfigOutdated } from '.' +import { version } from '../../../../package.json' + +// Run every time a new version is released. +export const migration: Migration = { + version, + async migrate(version) { + await onConfigOutdated(version, async () => { + await storageHelper.set({ raiseErrorCache: [] }) + }) + }, +} diff --git a/src/utils/config/migrations/index.ts b/src/utils/config/migrations/index.ts index 276c197..545c7b8 100644 --- a/src/utils/config/migrations/index.ts +++ b/src/utils/config/migrations/index.ts @@ -1,10 +1,12 @@ -import { storageHelper } from 'utils/storageHelper' +import { storageHelper, storageKeys } from 'utils/storageHelper' +import { version } from '../../../../package.json' import { Storage } from '../../storageHelper' import { migration as v1v0v1 } from './1.0.1' import { migration as v1v3v4 } from './1.3.4' import { migration as v2v6v0 } from './2.6.0' import { migration as v3v0v0 } from './3.0.0' import { migration as v3v5v0 } from './3.5.0' +import { migration as clearRaiseErrorCache } from './clearRaiseErrorCache' export type Migration = { version: string @@ -13,21 +15,29 @@ export type Migration = { export async function migrateConfig() { const migrations: Migration[] = [v1v0v1, v1v3v4, v2v6v0, v3v0v0, v3v5v0] + migrations.push(clearRaiseErrorCache) // Make sure this is run after other version-specific migrations for (const { version, migrate } of migrations) { await migrate(version) } + + await storageHelper.set({ [storageKeys.configVersion]: version }) } export async function onConfigOutdated( - configVersion: string, + migrationConfigVersion: string, runIfOutdated: (config: T) => Async, ) { const config = await storageHelper.get() - if (config && config.configVersion < configVersion) { - const { configVersion: $configVersion, ...restConfig } = config - await runIfOutdated(restConfig as T) - await storageHelper.set({ configVersion }) + if (config) { + const { + [storageKeys.configVersion]: savedConfigVersion, + [storageKeys.raiseErrorCache]: __, + ...restConfig + } = config + if (savedConfigVersion < migrationConfigVersion) { + await runIfOutdated(restConfig as T) + } } } diff --git a/src/utils/storageHelper.ts b/src/utils/storageHelper.ts index e3dc40a..a8fe9bd 100644 --- a/src/utils/storageHelper.ts +++ b/src/utils/storageHelper.ts @@ -1,8 +1,15 @@ const localStorage = browser.storage.local +const keys = { + configVersion: 'configVersion', + raiseErrorCache: 'raiseErrorCache', +} as const + +export const storageKeys = keys + export type Storage = { - // save root level `configVersion` for easier future migrating - [key in EnumString<'configVersion'>]: string + // save root level keys for easier future migrating + [key in EnumString]: string // separate different platform configs to simplify interactions with browser storage API // e.g. @@ -12,7 +19,7 @@ export type Storage = { async function get< T extends { [key: string]: any - } + }, >(mapping: string | string[] | null = null): Promise { try { return (await localStorage.get(mapping || undefined)) as T From 3ed0a8e8f7c242df5e337e7d3d4a89c4c836d025 Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Sun, 3 Jul 2022 00:10:35 +0800 Subject: [PATCH 12/13] fix: update GitHub page content selector --- .../cases/non-parallel/pjax.commits-page.ts | 2 +- src/platforms/GitHub/DOMHelper.ts | 24 +++++++++---------- src/platforms/GitHub/index.ts | 5 +--- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/__tests__/cases/non-parallel/pjax.commits-page.ts b/__tests__/cases/non-parallel/pjax.commits-page.ts index cdd87bc..a719005 100644 --- a/__tests__/cases/non-parallel/pjax.commits-page.ts +++ b/__tests__/cases/non-parallel/pjax.commits-page.ts @@ -6,7 +6,7 @@ describe(`in Gitako project page`, () => { it('should not break go back in history', async () => { for (let i = 0; i < 3; i++) { const commitLinks = await page.$$( - `#js-repo-pjax-container .TimelineItem-body ol li > div:nth-child(1) a[href*="/commit/"]`, + `main .TimelineItem-body ol li > div:nth-child(1) a[href*="/commit/"]`, ) if (commitLinks.length < 2) throw new Error(`No enough commits`) commitLinks[i].click() diff --git a/src/platforms/GitHub/DOMHelper.ts b/src/platforms/GitHub/DOMHelper.ts index 5cc4adb..a1dccf9 100644 --- a/src/platforms/GitHub/DOMHelper.ts +++ b/src/platforms/GitHub/DOMHelper.ts @@ -38,8 +38,8 @@ export function getIssueTitle() { export function getCurrentBranch(passive = false) { const selectedBranchButtonSelector = [ - '.repository-content #branch-select-menu summary', - '.repository-content .branch-select-menu summary', + 'main #branch-select-menu summary', + 'main .branch-select-menu summary', ].join() const branchButtonElement = $(selectedBranchButtonSelector) if (branchButtonElement) { @@ -55,7 +55,7 @@ export function getCurrentBranch(passive = false) { } const findFileButtonSelector = - '#js-repo-pjax-container .repository-content .file-navigation a[data-hotkey="t"]' + 'main .file-navigation a[data-hotkey="t"]' const urlFromFindFileButton: string | null = $( findFileButtonSelector, element => (element as HTMLAnchorElement).href, @@ -96,8 +96,8 @@ const PAGE_TYPES = { */ function getCurrentPageType() { const blobPathSelector = '#blob-path' // path next to branch switcher - const blobWrapperSelector = '.repository-content .blob-wrapper table' - const readmeSelector = '.repository-content .readme' + const blobWrapperSelector = 'main .blob-wrapper table' + const readmeSelector = 'main .readme' const searchResultSelector = '.codesearch-results' return ( $(searchResultSelector, () => PAGE_TYPES.SEARCH) || @@ -110,7 +110,7 @@ function getCurrentPageType() { const REPO_TYPE_PRIVATE = 'private' as const const REPO_TYPE_PUBLIC = 'public' as const export function getRepoPageType() { - const headerSelector = `#js-repo-pjax-container .pagehead.repohead h1` + const headerSelector = `main .pagehead.repohead h1` return $(headerSelector, header => { const repoPageTypes = [REPO_TYPE_PRIVATE, REPO_TYPE_PUBLIC] for (const repoPageType of repoPageTypes) { @@ -127,7 +127,7 @@ export function getRepoPageType() { */ export function getCodeElement() { if (getCurrentPageType() === PAGE_TYPES.RAW_TEXT) { - const codeContentSelector = '.repository-content .data table' + const codeContentSelector = 'main .data table' const codeContentElement = $(codeContentSelector) if (!codeContentElement) { raiseError(new Error('cannot find code content element')) @@ -158,7 +158,7 @@ export function attachCopyFileBtn() { } if (!buttonGroup) { - const buttonGroupSelector = '.repository-content .Box-header .BtnGroup' + const buttonGroupSelector = 'main .Box-header .BtnGroup' const buttonGroups = document.querySelectorAll(buttonGroupSelector) const $buttonGroup = buttonGroups[buttonGroups.length - 1] if ($buttonGroup) buttonGroup = $buttonGroup as HTMLElement @@ -180,9 +180,9 @@ export function attachCopyFileBtn() { } export function attachCopySnippet() { - const readmeSelector = '.repository-content div#readme' + const readmeSelector = 'main div#readme' return $(readmeSelector, () => { - const readmeArticleSelector = '.repository-content div#readme article' + const readmeArticleSelector = 'main div#readme article' return $( readmeArticleSelector, readmeElement => { @@ -227,10 +227,10 @@ export function attachCopySnippet() { }, () => { // in URL like `/{user}/{repo}/delete/{branch}/path/to/file - const deleteReadmeSelector = '.repository-content div#readme del' + const deleteReadmeSelector = 'main div#readme del' if (!$(deleteReadmeSelector)) { // in pages where readme is not markdown, e.g. txt - const plainReadmeSelector = '.repository-content div#readme .plain' + const plainReadmeSelector = 'main div#readme .plain' if (!$(plainReadmeSelector)) { raiseError( new Error('cannot find mount point for copy snippet button while readme exists'), diff --git a/src/platforms/GitHub/index.ts b/src/platforms/GitHub/index.ts index c7d5592..fda71db 100644 --- a/src/platforms/GitHub/index.ts +++ b/src/platforms/GitHub/index.ts @@ -108,10 +108,7 @@ function resolvePageScope(defaultBranchName?: string) { const pathSHAMap = new Map() -// Try lookup PJAX containers, #js-repo-pjax-container could exist while #repo-content-pjax-container does not. -const pjaxContainerSelector = ['#repo-content-pjax-container', '#js-repo-pjax-container'].find( - selector => document.querySelector(selector), -) +const pjaxContainerSelector = 'main' const turboContainerId = 'repo-content-turbo-frame' export const GitHub: Platform = { From d32b13e65ecd2171378a2e10c2ac45af1d2d6794 Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Sun, 3 Jul 2022 00:25:44 +0800 Subject: [PATCH 13/13] 3.7.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dee84ac..879dae2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitako", - "version": "3.7.3", + "version": "3.7.4", "description": "File tree for GitHub, and more than that.", "repository": "https://github.com/EnixCoda/Gitako", "author": "EnixCoda",