Merge branch 'develop' into manifest-v3

This commit is contained in:
EnixCoda 2024-03-29 23:45:58 +08:00
commit dba3c15e72
56 changed files with 746 additions and 511 deletions

View file

@ -1,2 +0,0 @@
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
PUPPETEER_EXEC_PATH=/opt/homebrew/bin/chromium

View file

@ -11,10 +11,9 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Cache deps
uses: actions/cache@v1
- uses: actions/cache@v3
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@ -26,7 +25,7 @@ jobs:
env:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
run: |
yarn
yarn --ignore-platform --ignore-engines --frozen-lockfile --prefer-offline
- name: Retrieve vscode icons
uses: actions/checkout@v3
@ -36,7 +35,7 @@ jobs:
- name: Build
run: |
make build
NODE_OPTIONS=--openssl-legacy-provider make build
- name: Archive production artifacts
uses: actions/upload-artifact@v3

View file

@ -13,7 +13,7 @@ on:
- 'server/**'
# Runs everyday to detect GitHub update in time
schedule:
- cron: '0 0 * * *'
- cron: '30 16 * * *'
jobs:
build:
@ -50,7 +50,7 @@ jobs:
env:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
run: |
yarn
yarn --ignore-platform --ignore-engines --frozen-lockfile --prefer-offline
- name: E2E Test
uses: mujo-code/puppeteer-headful@master
@ -89,7 +89,7 @@ jobs:
env:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
run: |
yarn
yarn --ignore-platform --ignore-engines --frozen-lockfile --prefer-offline
- name: Unit Test
run: |

View file

@ -36,6 +36,8 @@ Gitako is **FREE**. If you like it, please
[Feature discussions](https://github.com/EnixCoda/Gitako/discussions) and [bug reports](https://github.com/EnixCoda/Gitako/issues/) are also welcome!
Check out [contributing.md](./contributing.md) if you want to contribute to Gitako directly.
### About
#### Source of the name and logo?

View file

@ -579,7 +579,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 3.10.0;
MARKETING_VERSION = 3.12.0;
PRODUCT_BUNDLE_IDENTIFIER = enixcoda.Gitako.Extension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -604,7 +604,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 3.10.0;
MARKETING_VERSION = 3.12.0;
PRODUCT_BUNDLE_IDENTIFIER = enixcoda.Gitako.Extension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -631,7 +631,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 3.10.0;
MARKETING_VERSION = 3.12.0;
PRODUCT_BUNDLE_IDENTIFIER = enixcoda.Gitako;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Developer Sign for Distribution";
@ -657,7 +657,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 3.10.0;
MARKETING_VERSION = 3.12.0;
PRODUCT_BUNDLE_IDENTIFIER = enixcoda.Gitako;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Developer Sign for Distribution";

View file

@ -0,0 +1,15 @@
import { selectors } from '../../selectors'
import { testURL } from '../../testURL'
import { getTextContent, sleep } from '../../utils'
describe(`in Gitako project page`, () => {
beforeAll(() => page.goto(testURL`https://github.com/GitakoExtension/test-empty`))
it('should render error message', async () => {
await sleep(5000)
expect(await getTextContent(selectors.gitako.errorMessage)).toBe(
'This project seems to be empty.',
)
})
})

View file

@ -1,25 +1,25 @@
import { selectors } from '../../selectors'
import { testURL } from '../../testURL'
import { expectToFind, expectToNotFind, sleep, waitForRedirect } from '../../utils'
jest.retryTimes(3)
describe(`in Gitako project page`, () => {
beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako/commits/develop'))
beforeAll(() => page.goto(testURL`https://github.com/EnixCoda/Gitako/commits/develop`))
it('should not break go back in history', async () => {
for (let i = 0; i < 3; i++) {
const commitLinks = await page.$$(
`main .TimelineItem-body ol li > div:nth-child(1) a[href*="/commit/"]`,
)
const commitLinks = await page.$$(selectors.github.commitLinks)
if (commitLinks.length < 2) throw new Error(`No enough commits`)
commitLinks[i].click()
await waitForRedirect()
await expectToFind('div.commit')
await expectToFind(selectors.github.commitSummary)
await sleep(1000)
page.goBack()
await sleep(1000)
// The selector for file content
await expectToNotFind('div.commit')
await expectToNotFind(selectors.github.commitSummary)
}
})
})

View file

@ -1,26 +1,28 @@
import { selectors } from '../../selectors'
import { testURL } from '../../testURL'
import { expectToFind, expectToNotFind, sleep, waitForRedirect } from '../../utils'
jest.retryTimes(3)
describe(`in Gitako project page`, () => {
beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako/tree/develop/src'))
beforeAll(() => page.goto(testURL`https://github.com/EnixCoda/Gitako/tree/develop/src`))
it('should not break go back in history', async () => {
for (let i = 0; i < 3; i++) {
const commitLinks = await page.$$(
`.js-details-container div[role="row"] div[role="rowheader"] a[title*="."]`,
)
if (commitLinks.length < 2) throw new Error(`No enough files`)
const fileItems = await page.$$(selectors.github.fileListItemFileLinks)
if (fileItems.length < 2) throw new Error(`No enough files`)
await waitForRedirect(async () => {
await commitLinks[i].click()
await fileItems[i].click()
})
await expectToFind('table.js-file-line-container')
await expectToFind(selectors.github.fileContent)
await sleep(1000)
page.goBack()
await sleep(1000)
// The selector for file content
await expectToNotFind('table.js-file-line-container')
await expectToNotFind(selectors.github.fileContent)
}
})
})

View file

@ -1,8 +1,10 @@
import { selectors } from '../../selectors'
import { testURL } from '../../testURL'
import {
collapseFloatModeSidebar,
expandFloatModeSidebar,
getTextContent,
patientClick,
selectFileTreeItem,
sleep,
waitForRedirect,
} from '../../utils'
@ -10,20 +12,20 @@ import {
jest.retryTimes(3)
describe(`in Gitako project page`, () => {
beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako/tree/develop/src'))
beforeAll(() => page.goto(testURL`https://github.com/EnixCoda/Gitako/tree/develop/src`))
it('should work with PJAX', async () => {
await sleep(3000)
await expandFloatModeSidebar()
await patientClick(selectFileTreeItem('src/analytics.ts'))
await patientClick(selectors.gitako.fileItemOf('src/analytics.ts'))
await waitForRedirect()
await collapseFloatModeSidebar()
await page.click('a[data-selected-links^="repo_issues "]')
await page.click(selectors.github.navBarItemIssues)
await waitForRedirect()
await page.click('a[data-selected-links^="repo_pulls "]')
await page.click(selectors.github.navBarItemPulls)
await waitForRedirect()
page.goBack()
@ -32,10 +34,6 @@ describe(`in Gitako project page`, () => {
page.goBack()
await sleep(1000)
expect(
await page.evaluate(
() => document.querySelector('.final-path')?.textContent === 'analytics.ts',
),
).toBe(true)
expect(await getTextContent(selectors.github.breadcrumbFileName)).toBe('/analytics.ts')
})
})

View file

@ -1,9 +1,10 @@
import { selectors } from '../../selectors'
import { testURL } from '../../testURL'
import {
expandFloatModeSidebar,
expectToFind,
expectToNotFind,
patientClick,
selectFileTreeItem,
sleep,
waitForRedirect,
} from '../../utils'
@ -11,17 +12,16 @@ import {
jest.retryTimes(3)
describe(`in Gitako project page`, () => {
beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako/tree/test/multiple-changes'))
beforeAll(() => page.goto(testURL`https://github.com/EnixCoda/Gitako/tree/test/multiple-changes`))
it('should work with PJAX', async () => {
await sleep(3000)
await expandFloatModeSidebar()
await patientClick(selectFileTreeItem('.babelrc'))
await patientClick(selectors.gitako.fileItemOf('.babelrc'))
await waitForRedirect()
// The selector for file content
await expectToFind('table.js-file-line-container')
await expectToFind(selectors.github.fileContent)
await waitForRedirect(async () => {
await sleep(1000) // This prevents failing in some cases due to some mystery scheduling issue of puppeteer or jest
@ -29,6 +29,6 @@ describe(`in Gitako project page`, () => {
})
// The selector for file content
await expectToNotFind('table.js-file-line-container')
await expectToNotFind(selectors.github.fileContent)
})
})

View file

@ -1,39 +1,37 @@
import {
expandFloatModeSidebar,
expectToFind,
expectToNotFind,
scroll,
selectFileTreeItem,
} from '../../utils'
import { selectors } from '../../selectors'
import { testURL } from '../../testURL'
import { expandFloatModeSidebar, expectToFind, expectToNotFind, scroll } from '../../utils'
jest.retryTimes(3)
describe(`in Gitako project page`, () => {
beforeAll(() =>
page.goto('https://github.com/EnixCoda/Gitako/tree/test/200-changed-files-200-lines-each'),
page.goto(
testURL`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')
await expectToFind(selectors.gitako.bodyWrapper)
})
it('should render file list', async () => {
await expectToFind('.gitako-side-bar .files .node-item')
await expectToFind(selectors.gitako.fileItem)
})
it('should render while scroll', async () => {
await expandFloatModeSidebar()
const filesEle = await page.waitForSelector('.gitako-side-bar .files')
const filesEle = await page.waitForSelector(selectors.gitako.files)
// node of tsconfig.json should NOT be rendered before scroll down
await expectToNotFind(selectFileTreeItem('tsconfig.json'))
await expectToNotFind(selectors.gitako.fileItemOf('tsconfig.json'))
const box = await filesEle?.boundingBox()
if (box) {
await page.mouse.move(box.x + 40, box.y + 40)
await scroll({ totalDistance: 10000, stepDistance: 100 })
// node of tsconfig.json should be rendered now
await expectToFind(selectFileTreeItem('tsconfig.json'))
await expectToFind(selectors.gitako.fileItemOf('tsconfig.json'))
}
})
})

View file

@ -2,10 +2,11 @@
* Confirm basic behaviors of puppeteer assertions
*/
import { testURL } from '../../testURL'
import { expectToFind, expectToNotFind } from '../../utils'
describe(`in random page`, () => {
beforeAll(() => page.goto('https://google.com'))
beforeAll(() => page.goto(testURL`https://google.com`))
it('wait for hidden non-exist element should resolve null', async () => {
expect(

View file

@ -1,20 +1,20 @@
import { expectToFind, selectFileTreeItem, sleep, waitForRedirect } from '../../utils'
import { selectors } from '../../selectors'
import { testURL } from '../../testURL'
import { expectToFind, sleep, waitForRedirect } from '../../utils'
describe(`in Gitako project page`, () => {
beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako/tree/develop/src'))
beforeAll(() => page.goto(testURL`https://github.com/EnixCoda/Gitako/tree/develop/src`))
it('expand to target on load and after redirect', async () => {
await sleep(3000)
// Expect Gitako sidebar to have expanded src to see contents
await expectToFind(selectFileTreeItem('src/components'))
await expectToFind(selectors.gitako.fileItemOf('src/components'))
await page.click(
`.js-details-container div[role="row"] div[role="rowheader"] [title="components"]`,
)
await page.click(selectors.github.fileListItemLinkOf('components'))
await waitForRedirect()
// Expect Gitako sidebar to have expanded components and see contents
await expectToFind(selectFileTreeItem('src/components/Gitako.tsx'))
await expectToFind(selectors.gitako.fileItemOf('src/components/Gitako.tsx'))
})
})

View file

@ -1,7 +1,8 @@
import { testURL } from '../../testURL'
import { expectToNotFind } from '../../utils'
describe(`in GitHub homepage`, () => {
beforeAll(() => page.goto('https://github.com'))
beforeAll(() => page.goto(testURL`https://github.com`))
it('should not render Gitako', async () => {
await expectToNotFind('.gitako-side-bar .gitako-side-bar-body-wrapper')

View file

@ -1,13 +1,15 @@
import { selectors } from '../../selectors'
import { testURL } from '../../testURL'
import { expectToFind } from '../../utils'
describe(`in Gitako project page`, () => {
beforeAll(() => page.goto('https://github.com/EnixCoda/Gitako/pull/71'))
beforeAll(() => page.goto(testURL`https://github.com/EnixCoda/Gitako/pull/71`))
it('should render Gitako', async () => {
await expectToFind('.gitako-side-bar .gitako-side-bar-body-wrapper')
await expectToFind(selectors.gitako.bodyWrapper)
})
it('should render file list', async () => {
await expectToFind('.gitako-side-bar .files .node-item')
await expectToFind(selectors.gitako.fileItem)
})
})

View file

@ -1,7 +1,11 @@
const baseConfig = require('./jest.config')
/**
* @type {import('@jest/types').Config.InitialOptions}
*/
module.exports = {
...baseConfig,
maxWorkers: 1,
testMatch: [...baseConfig.testMatch, '**/__tests__/cases/non-parallel/*.ts?(x)'],
setupFilesAfterEnv: ['<rootDir>/setup.ts'],
}

21
__tests__/selectors.ts Normal file
View file

@ -0,0 +1,21 @@
export const selectors = {
github: {
breadcrumbFileName: `[data-testid="breadcrumbs-filename"]`,
fileContent: 'textarea[aria-label="file content"]',
commitLinks: `li[data-testid="commit-row-item"] [data-testid="listview-item-title-container"] a[href*="/commit/"]`,
// assume title contains `.` is file item
fileListItemFileLinks: `table[aria-labelledby="folders-and-files"] tr.react-directory-row td.react-directory-row-name-cell-large-screen .react-directory-filename-column .react-directory-truncate a[aria-label$="(File)"]`,
fileListItemLinkOf: (name: string) =>
`table[aria-labelledby="folders-and-files"] tr.react-directory-row td.react-directory-row-name-cell-large-screen .react-directory-filename-column .react-directory-truncate a[title="${name}"]`,
commitSummary: 'div.commit',
navBarItemIssues: 'a[data-selected-links^="repo_issues "]',
navBarItemPulls: 'a[data-selected-links^="repo_pulls "]',
},
gitako: {
fileItem: '.gitako-side-bar .files .node-item',
fileItemOf: (path: string) => `.gitako-side-bar .files .node-item[title="${path}"]`,
errorMessage: '#gitako-logo-mount-point .error-message',
files: '.gitako-side-bar .files',
bodyWrapper: '.gitako-side-bar .gitako-side-bar-body-wrapper',
},
}

1
__tests__/setup.ts Normal file
View file

@ -0,0 +1 @@
jest.retryTimes(3)

11
__tests__/testURL.ts Normal file
View file

@ -0,0 +1,11 @@
// string template function, take input URL string and add a search param
// example: url`http://g.com` => `http://g.com?k1="json-value"`
export function testURL(strings: TemplateStringsArray, ...values: unknown[]) {
const raw = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '')
const GITAKO_ACCESS_TOKEN = process.env.GITAKO_ACCESS_TOKEN
if (!GITAKO_ACCESS_TOKEN) return raw
const url = new URL(raw, 'http://localhost')
url.searchParams.set('gitako-config-accessToken', JSON.stringify(GITAKO_ACCESS_TOKEN))
return url.href
}

View file

@ -87,11 +87,11 @@ export async function waitForRedirect(action?: () => void | Promise<void>) {
fired = true
return action()
})
return Promise.race([waitForLegacyPJAXRedirect($action), waitForTurboRedirect($action)])
}
export function selectFileTreeItem(path: string): string {
return `.gitako-side-bar .files a[title="${path}"]`
return Promise.race([
waitForLegacyPJAXRedirect($action),
waitForTurboRedirect($action),
sleep(3 * 1000),
])
}
export async function patientClick(selector: string) {
@ -119,3 +119,7 @@ export async function collapseFloatModeSidebar() {
})
await sleep(500)
}
export function getTextContent(query: string) {
return page.evaluate(query => document.querySelector(query)?.textContent, query)
}

44
contributing.md Normal file
View file

@ -0,0 +1,44 @@
# Contributing
Thank you if you are trying to contribute!
Note: if you were using Windows, you may need to find alternatives for `make` commands. Or use WSL. I've not tested development on Windows and do not guarantee if it would work.
## Set up development env
1. Clone the repo
1. Run `make pull-icons` to install dependencies
1. Run `yarn` to install dependencies
1. Run `yarn dev` to start the development server, you'll see a `dist` folder appear in the root of this project
1. Open the extensions page in Chrome, enable developer mode, and load the extension from the `dist` folder
1. Navigate to repository of your choice and you should see the extension appear
When you modify source code, you need to do either of below to apply your changes:
- (recommended) use [the Extension Reloader extension](https://chrome.google.com/webstore/detail/fimgfedafeadlieiabdeeaodndnlbhid). It could reload all extensions then refresh the page (you need to enable it in its settings).
- manually reload the extension in the `chrome://extensions` and then refresh your repository page
## Develop with more browsers
Gitako supports more browsers, in order to develop for them, please do the followings.
### Edge:
- Similar to above steps for Chrome
### Firefox:
- run `yarn dev-firefox`
- a new instance of Firefox will open with Gitako automatically installed
- navigate to a GitHub repo and you should see the extension appear
- when you modify source, refresh the tab
### Safari (macOS only):
- run `yarn dev-safari`
- Open `Safari/Gitako/Gitako.xcodeproj` in Xcode
- Click the "Run" button
- Enable developer mode in Safari's preferences
- Enable Gitako in Safari's preferences
- Open a Safari tab and visit a GitHub repo, then activate Gitako via Gitako icon next to the address bar
- when you modify source, click the "Run" button in Xcode and refresh the tab

View file

@ -1,8 +1,6 @@
const path = require('path')
if (process.arch === 'arm64' && process.platform === 'darwin') {
require('dotenv').config({
path: '.env.arm.mac',
})
require('dotenv').config()
}
const CRX_PATH = path.resolve(__dirname, 'dist')

View file

@ -1,6 +1,6 @@
{
"name": "gitako",
"version": "3.10.0",
"version": "3.12.0",
"description": "File tree for GitHub, and more than that.",
"repository": "https://github.com/EnixCoda/Gitako",
"author": "EnixCoda",
@ -9,13 +9,13 @@
"homepage": "https://github.com/EnixCoda/Gitako",
"scripts": {
"prepare": "husky install",
"dev": "VERSION=dev-v$(node scripts/get-version.js) webpack --watch",
"dev": "NODE_OPTIONS=--openssl-legacy-provider VERSION=dev-v$(node scripts/get-version.js) webpack --watch",
"dev-safari": "TARGET=safari yarn run dev",
"debug-firefox": "web-ext run --source-dir=dist --firefox-profile=firefox-profile --profile-create-if-missing --keep-profile-changes --start-url https://github.com/EnixCoda/Gitako",
"analyze-bundle": "ANALYZE= NODE_ENV=production webpack",
"dev-firefox": "web-ext run --source-dir=dist --firefox-profile=firefox-profile --profile-create-if-missing --keep-profile-changes --start-url https://github.com/EnixCoda/Gitako",
"analyze-bundle": "NODE_OPTIONS=--openssl-legacy-provider ANALYZE= NODE_ENV=production webpack",
"postinstall": "node scripts/fix-deps",
"postversion": "sh scripts/post-version.sh",
"build": "VERSION=v$(node scripts/get-version.js) NODE_ENV=production webpack",
"build": "NODE_OPTIONS=--openssl-legacy-provider VERSION=v$(node scripts/get-version.js) NODE_ENV=production webpack",
"roll": "make release",
"test": "yarn run test:parallel && yarn run test:non-parallel",
"test:unit": "NODE_ENV=test jest --config jest.config.js",
@ -46,6 +46,7 @@
"react-use": "^17.3.2",
"react-window": "^1.8.7",
"styled-components": "^5.3.5",
"superstruct": "^1.0.3",
"webext-domain-permission-toggle": "^3.0.0",
"webext-dynamic-content-scripts": "^9.0.0",
"webextension-polyfill": "^0.10.0"
@ -84,7 +85,7 @@
"json-loader": "^0.5.7",
"lint-staged": "^13.0.3",
"mini-css-extract-plugin": "^0.9.0",
"prettier": "^2.7.1",
"prettier": "^2.8.3",
"puppeteer": "^10.1.0",
"raw-loader": "^4.0.0",
"sass": "^1.26.2",
@ -114,6 +115,7 @@
]
},
"resolutions": {
"fsevents": "^2.3.3",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.3"
}

View file

@ -266,7 +266,7 @@ export function useRenderGoToButton(searched: boolean, goTo: (path: string[]) =>
? function renderGoToButton(node: TreeNode): React.ReactNode {
return (
<button
title={'Reveal in file tree'}
title={'Reveal in file tree (⏎)'}
className={'go-to-button'}
onClick={() => goTo(node.path.split('/'))}
>

View file

@ -5,7 +5,7 @@ import { SearchBar } from 'components/SearchBar'
import { useConfigs } from 'containers/ConfigsContext'
import { PortalContext } from 'containers/PortalContext'
import { RepoContext } from 'containers/RepoContext'
import { platform } from 'platforms'
import { useInspector } from 'containers/Inspector'
import * as React from 'react'
import { usePrevious, useUpdateEffect } from 'react-use'
import { cx } from 'utils/cx'
@ -171,13 +171,17 @@ function LoadedFileExplorer({
const renderLabelText = useRenderLabelText(searchKey)
const goToCurrentItem = React.useCallback(() => {
const targetPath = platform.getCurrentPath(metaData.branchName)
const targetPath = getCurrentPath()
if (targetPath) expandTo(targetPath)
}, [metaData.branchName, expandTo])
}, [getCurrentPath, expandTo])
useOnLocationChange(goToCurrentItem)
useAfterRedirect(goToCurrentItem)
const [currentPath, setCurrentPath] = React.useState(() => getCurrentPath())
useAfterRedirect(React.useCallback(() => setCurrentPath(getCurrentPath()), [getCurrentPath]))
useInspector('CurrentPath', currentPath)
const ref = React.useRef<HTMLDivElement | null>(null)
useFocusOnPendingTarget(
'files',

View file

@ -1,5 +1,6 @@
import { GearIcon } from '@primer/octicons-react'
import { GearIcon, SyncIcon } from '@primer/octicons-react'
import { Link } from '@primer/react'
import { ReloadContext } from 'containers/ReloadContext'
import { VERSION } from 'env'
import * as React from 'react'
import { RoundIconButton } from './RoundIconButton'
@ -11,23 +12,42 @@ type Props = {
export function Footer(props: Props) {
const { toggleShowSettings } = props
const reload = React.useContext(ReloadContext)
return (
<div className={'gitako-footer'}>
<Link
className={'version'}
href={wikiLinks.changeLog}
title={'Check out new features!'}
target="_blank"
rel="noopener noreferrer"
>
{VERSION}
</Link>
<RoundIconButton
aria-label={'settings'}
icon={GearIcon}
iconColor="fg.muted"
onClick={toggleShowSettings}
/>
<div className="gitako-footer-section">
<Link
className={'version'}
href={wikiLinks.changeLog}
title={'Gitako changelog'}
target="_blank"
rel="noopener noreferrer"
>
{VERSION}
</Link>
<Link
title="About to say good bye."
href={wikiLinks.bye}
target="_blank"
rel="noopener noreferrer"
>
👋
</Link>
</div>
<div>
<RoundIconButton
aria-label={'Reload'}
icon={SyncIcon}
iconColor="fg.muted"
onClick={() => reload()}
/>
<RoundIconButton
aria-label={'Settings'}
icon={GearIcon}
iconColor="fg.muted"
onClick={toggleShowSettings}
/>
</div>
</div>
)
}

View file

@ -1,7 +1,7 @@
import { SideBar } from 'components/SideBar'
import { ConfigsContextWrapper } from 'containers/ConfigsContext'
import { InspectorContextWrapper } from 'containers/Inspector'
import { ReloadContextWrapper } from 'containers/ReloadContext'
import { InspectorContextWrapper } from 'containers/StateInspector'
import * as React from 'react'
import { StyleSheetManager } from 'styled-components'
import { insertMountPoint } from 'utils/DOMHelper'
@ -12,25 +12,26 @@ import { RepoContextWrapper } from '../containers/RepoContext'
import { StateBarStateContextWrapper } from '../containers/SideBarState'
export function Gitako() {
const mountPoint = React.useMemo(() => insertMountPoint(), [])
return (
<InspectorContextWrapper>
<StyleSheetManager target={insertMountPoint()}>
<ReloadContextWrapper>
<ErrorBoundary>
<ConfigsContextWrapper>
<StateBarStateContextWrapper>
<StateBarErrorContextWrapper>
<StyleSheetManager target={mountPoint}>
<ReloadContextWrapper>
<ErrorBoundary>
<ConfigsContextWrapper>
<InspectorContextWrapper>
<StateBarErrorContextWrapper>
<StateBarStateContextWrapper>
<OAuthWrapper>
<RepoContextWrapper>
<SideBar />
</RepoContextWrapper>
</OAuthWrapper>
</StateBarErrorContextWrapper>
</StateBarStateContextWrapper>
</ConfigsContextWrapper>
</ErrorBoundary>
</ReloadContextWrapper>
</StyleSheetManager>
</InspectorContextWrapper>
</StateBarStateContextWrapper>
</StateBarErrorContextWrapper>
</InspectorContextWrapper>
</ConfigsContextWrapper>
</ErrorBoundary>
</ReloadContextWrapper>
</StyleSheetManager>
)
}

View file

@ -42,7 +42,7 @@ export function SearchBar({ onSearch, onFocus, value }: Props) {
block
sx={{ borderRadius: 0 }}
className={'search-input'}
aria-label="search files"
aria-label="Search files"
placeholder={formatWithShortcut(`Search files`, focusSearchInputShortcut)}
onChange={({ target: { value } }) => onSearch(value, searchMode)}
value={value}

View file

@ -65,24 +65,12 @@ export function SideBar() {
return (
<Theme>
<ToggleShowButtonWrapper
shouldExpand={shouldExpand}
setShouldExpand={setShouldExpand}
toggleShowSideBar={toggleShowSideBar}
/>
<SidebarContext.Provider value={sidebarContextValue}>
<IIFC>
{() => {
const logoContainerElement = useLogoContainerElement()
return (
<Portal into={logoContainerElement}>
<ToggleShowButton
error={error}
className={cx({
hidden: shouldExpand,
})}
onHover={sidebarToggleMode === 'float' ? () => setShouldExpand(true) : undefined}
onClick={toggleShowSideBar}
/>
</Portal>
)
}}
</IIFC>
<div className={'gitako-side-bar'}>
<div
className={cx('gitako-side-bar-body-wrapper', `toggle-mode-${sidebarToggleMode}`, {
@ -174,6 +162,30 @@ export function SideBar() {
)
}
function ToggleShowButtonWrapper({
shouldExpand,
setShouldExpand,
toggleShowSideBar,
}: {
shouldExpand: boolean
setShouldExpand: React.Dispatch<React.SetStateAction<boolean>>
toggleShowSideBar: () => void
}) {
const logoContainerElement = useLogoContainerElement()
const { sidebarToggleMode } = useConfigs().value
return (
<Portal into={logoContainerElement}>
<ToggleShowButton
className={cx({
hidden: shouldExpand,
})}
onHover={sidebarToggleMode === 'float' ? () => setShouldExpand(true) : undefined}
onClick={toggleShowSideBar}
/>
</Portal>
)
}
function useFocusSidebarOnExpand(shouldExpand: boolean) {
React.useEffect(() => {
// prevent keeping focus within Gitako
@ -278,18 +290,21 @@ function useCollapseOnNoPermissionWhenTokenHasBeenSet(
function useShouldExpand() {
const getDerivedExpansion = useGetDerivedExpansion()
const error = useLoadedContext(SideBarErrorContext).value
const [shouldExpand, setShouldExpand] = React.useState(getDerivedExpansion)
const toggleShowSideBar = React.useCallback(
() => setShouldExpand(show => !show),
[setShouldExpand],
)
useSaveExpandStateOnToggle(shouldExpand)
useUpdateBodyIndentOnStateUpdate(shouldExpand)
const $shouldExpand = error ? false : shouldExpand
useSaveExpandStateOnToggle($shouldExpand)
useUpdateBodyIndentOnStateUpdate($shouldExpand)
useUpdateBodyIndentAfterRedirect(setShouldExpand)
useCollapseOnNoPermissionWhenTokenHasBeenSet(setShouldExpand)
return [shouldExpand, setShouldExpand, toggleShowSideBar] as const
return [$shouldExpand, setShouldExpand, toggleShowSideBar] as const
}
function useShowSidebarKeyboard(

View file

@ -1,15 +1,16 @@
import { SyncIcon } from '@primer/octicons-react'
import iconURL from 'assets/icons/Gitako.png'
import { useConfigs } from 'containers/ConfigsContext'
import { SideBarErrorContext } from 'containers/ErrorContext'
import { ReloadContext } from 'containers/ReloadContext'
import * as React from 'react'
import { useDebounce, useWindowSize } from 'react-use'
import { cx } from 'utils/cx'
import { useLoadedContext } from 'utils/hooks/useLoadedContext'
import { useResizeHandler } from 'utils/hooks/useResizeHandler'
import { RoundIconButton } from './RoundIconButton'
type Props = {
error?: string | null
className?: React.HTMLAttributes<HTMLButtonElement>['className']
onHover?: React.HTMLAttributes<HTMLButtonElement>['onMouseEnter']
onClick?: (e: PointerEvent) => void
@ -21,8 +22,10 @@ function getSafeDistance(y: number, height: number) {
return Math.max(0, Math.min(y, height - buttonHeight))
}
export function ToggleShowButton({ error, className, onClick, onHover }: Props) {
export function ToggleShowButton({ className, onClick, onHover }: Props) {
const reload = React.useContext(ReloadContext)
const error = useLoadedContext(SideBarErrorContext).value
const ref = React.useRef<HTMLDivElement>(null)
const config = useConfigs()
const [distance, setDistance] = React.useState(config.value.toggleButtonVerticalDistance)

View file

@ -20,15 +20,13 @@ export const wikiLinks = {
compressSingletonFolder: `${WIKI_HOME_LINK}/Compress-Singleton-Folder`,
changeLog: `${WIKI_HOME_LINK}/Change-Log`,
codeFolding: `${WIKI_HOME_LINK}/Code-folding`,
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`,
bye: `${WIKI_HOME_LINK}/About-to-say-good-bye`,
}
const moreFields: SimpleConfigField<
'copyFileButton' | 'copySnippetButton' | 'codeFolding' | 'pjaxMode'
>[] =
const moreFields: SimpleConfigField<'copySnippetButton' | 'codeFolding' | 'pjaxMode'>[] =
platform === GitHub
? [
{
@ -47,12 +45,6 @@ const moreFields: SimpleConfigField<
onChange: checked => (checked ? 'native' : 'pjax-api'),
},
},
{
key: 'copyFileButton',
label: 'Copy file button',
wikiLink: wikiLinks.copyFileButton,
tooltip: `Read more in Gitako's Wiki`,
},
{
key: 'copySnippetButton',
label: 'Copy snippet button',

View file

@ -1,5 +1,5 @@
import { PropsWithChildren } from 'common'
import { useInspector } from 'containers/StateInspector'
import { useInspector } from 'containers/Inspector'
import * as React from 'react'
import { useStateIO } from 'utils/hooks/useStateIO'

View file

@ -1,7 +1,10 @@
import { PropsWithChildren, ReactIO } from 'common'
import { IN_PRODUCTION_MODE } from 'env'
import * as React from 'react'
import { noop } from 'utils/general'
import { useStateIO } from 'utils/hooks/useStateIO'
import { useConfigs } from './ConfigsContext'
import { Config } from 'utils/config/helper'
export type InspectorContextShape = ReactIO<JSONObject>
@ -11,7 +14,10 @@ export const InspectorContextWrapper = IN_PRODUCTION_MODE
? React.Fragment
: function InspectorContextWrapper({ children }: PropsWithChildren) {
const $ = useStateIO<JSONObject>({})
const [show, setShow] = React.useState(true)
const configs = useConfigs()
const { __showInspector: show } = configs.value
const setShow = (__showInspector: Config['__showInspector']) =>
configs.onChange({ __showInspector })
return (
<InspectorContext.Provider value={$}>
@ -57,9 +63,11 @@ export const InspectorContextWrapper = IN_PRODUCTION_MODE
)
}
export function useInspector(key: string, value: JSONValue) {
const $ = React.useContext(InspectorContext)
React.useEffect(() => {
$?.onChange(prev => ({ ...prev, [key]: value }))
}, [key, value]) // eslint-disable-line react-hooks/exhaustive-deps
}
export const useInspector = IN_PRODUCTION_MODE
? noop
: function useInspector(key: string, value: JSONValue) {
const $ = React.useContext(InspectorContext)
React.useEffect(() => {
$?.onChange(prev => ({ ...prev, [key]: value }))
}, [key, value]) // eslint-disable-line react-hooks/exhaustive-deps
}

View file

@ -9,7 +9,7 @@ import { useHandleNetworkError } from 'utils/hooks/useHandleNetworkError'
import { useLoadedContext } from 'utils/hooks/useLoadedContext'
import { useStateIO } from 'utils/hooks/useStateIO'
import { SideBarStateContext } from './SideBarState'
import { useInspector } from './StateInspector'
import { useInspector } from './Inspector'
export const RepoContext = React.createContext<MetaData | null>(null)

View file

@ -1,7 +1,9 @@
import { PropsWithChildren } from 'common'
import * as React from 'react'
import { useLoadedContext } from 'utils/hooks/useLoadedContext'
import { useStateIO } from 'utils/hooks/useStateIO'
import { useInspector } from './StateInspector'
import { SideBarErrorContext } from './ErrorContext'
import { useInspector } from './Inspector'
export type SideBarState =
| 'disabled'
@ -13,7 +15,8 @@ export type SideBarState =
| 'tree-rendering'
| 'tree-rendered'
| 'idle'
| 'error-due-to-auth'
| 'error' // when error occurs, sidebar should never expand
| 'error-due-to-auth' // this is a special error, user can expand sidebar and set token to fix the error
export type SideBarStateContextShape = IO<SideBarState>
@ -22,10 +25,17 @@ export const SideBarStateContext = React.createContext<SideBarStateContextShape
export function StateBarStateContextWrapper({ children }: PropsWithChildren) {
const $state = useStateIO<SideBarState>('disabled')
useInspector('SideBarStateContext', $state.value)
return (
<SideBarStateContext.Provider value={$state}>
{$state.value !== null && children}
</SideBarStateContext.Provider>
const error = useLoadedContext(SideBarErrorContext).value
const $$state: IO<SideBarState> = React.useMemo(
() =>
error && $state.value !== 'error'
? {
...$state,
value: 'error',
}
: $state,
[$state, error],
)
return <SideBarStateContext.Provider value={$$state}>{children}</SideBarStateContext.Provider>
}

View file

@ -1,7 +1,8 @@
import { Gitako } from 'components/Gitako'
import * as React from 'react'
import { createRoot } from 'react-dom/client'
import { insertSideBarMountPoint } from 'utils/DOMHelper'
import { insertMountPoint, insertSideBarMountPoint } from 'utils/DOMHelper'
import { useAfterRedirect } from 'utils/hooks/useFastRedirect'
import './content.scss'
if (document.readyState === 'loading') {
@ -12,7 +13,18 @@ if (document.readyState === 'loading') {
async function init() {
await injectStyles(browser.runtime.getURL('content.css'))
createRoot(insertSideBarMountPoint()).render(<Gitako />)
const mountPoint = insertSideBarMountPoint()
const MountPointWatcher = () => {
useAfterRedirect(React.useCallback(() => insertMountPoint(() => mountPoint), []))
return null
}
createRoot(mountPoint).render(
<>
<MountPointWatcher />
<Gitako />
</>,
)
}
// injects a copy of stylesheets so that other extensions(e.g. dark reader) could read

View file

@ -2,7 +2,7 @@ import { errors } from 'platforms'
import { isEnterprise } from '.'
import { is } from '../../utils/is'
import { gitakoServiceHost } from '../../utils/networkService'
import { continuousLoadPages, getDOM, resolveHeaderLink } from './utils'
import { continuousLoadFragmentedPages, getDOM, resolveHeaderLink } from './utils'
function isAPIRateLimitExceeded(content: JSONValue) {
return (
@ -147,7 +147,7 @@ export async function getPullPageDocuments(
document?: Document,
): Promise<Document[]> {
// Response of this API contains view of few files but is not complete.
return continuousLoadPages(
return continuousLoadFragmentedPages(
document ||
(await getDOM(`${window.location.origin}/${userName}/${repoName}/pull/${pullId}/files`)),
)
@ -158,7 +158,7 @@ export async function getCommitPageDocuments(): Promise<Document[]> {
repoName: string,
commitId: string, */
// arguments are not used because info are collected from DOM directly
return continuousLoadPages(document)
return continuousLoadFragmentedPages(document)
}
export async function getBlobData(

View file

@ -1,15 +1,103 @@
import { raiseError } from 'analytics'
import { Clippy, ClippyClassName } from 'components/Clippy'
import * as React from 'react'
import * as s from 'superstruct'
import { $ } from 'utils/$'
import { formatClass, parseIntFromElement } from 'utils/DOMHelper'
import { renderReact, run } from 'utils/general'
import { CopyFileButton, copyFileButtonClassName } from './CopyFileButton'
import { renderReact } from 'utils/general'
import { embeddedDataStruct } from './embeddedDataStructures'
const selectors = {
normal: {
reactApp: `react-app[app-name="react-code-view"] [data-target="react-app.reactRoot"]`,
codeTab: '#code-tab',
branchSwitcher: [`summary[title="Switch branches or tags"]`, `#branch-select-menu`].join(),
fileNavigation: `.file-navigation`,
breadcrumbs: `[data-testid="breadcrumbs"]`,
breadcrumbsFilename: `[data-testid="breadcrumbs-filename"]`,
},
globalNavigation: {
navbar: {
repositoryOwner: [
'.AppHeader-context-item[data-hovercard-type="user"]',
'.AppHeader-context-item[data-hovercard-type="organization"]',
].join(),
// its meant to be the element visually next to the `repositoryOwner` element
repositoryName:
'nav[role="navigation"] ul[role="list"] li:nth-child(2) .AppHeader-context-item',
},
branchSelector: 'button[id^="branch-picker-"]',
pathContext: '[data-testid="breadcrumbs"]',
pathContextFileName: '[data-testid="breadcrumbs-filename"]',
pathContextScreenReaderHeading: '[data-testid="screen-reader-heading"]',
embeddedData: {
app: 'script[type="application/json"][data-target="react-app.embeddedData"]',
reposOverview:
'[partial-name="repos-overview"] script[type="application/json"][data-target="react-partial.embeddedData"]',
},
},
}
const getDOMJSON = (selector: string) =>
$(selector, e => {
try {
return JSON.parse(e.textContent || '')
} catch (error) {
return null
}
})
function getMetaFromPayload(payload: s.Infer<typeof embeddedDataStruct.repoPayload>) {
const { repo, refInfo } = payload
const { defaultBranch, name: repoName, ownerLogin: userName } = repo
const { name: branchName } = refInfo
return {
defaultBranch,
metaData: {
userName,
repoName,
branchName,
},
}
}
// in code page, there is a JSON script tag in DOM with meta data
function resolveEmbeddedAppData() {
const data = getDOMJSON(selectors.globalNavigation.embeddedData.app)
if (s.is(data, embeddedDataStruct.app)) return getMetaFromPayload(data.payload)
}
function resolveEmbeddedReposOverviewData() {
const data = getDOMJSON(selectors.globalNavigation.embeddedData.reposOverview)
if (s.is(data, embeddedDataStruct.reposOverview))
return getMetaFromPayload(data.props.initialPayload)
}
export function resolveEmbeddedData(): {
defaultBranch: string
metaData: MetaData
} | void {
return resolveEmbeddedAppData() || resolveEmbeddedReposOverviewData()
}
export function resolveMeta(): Partial<MetaData> {
const dataFromJSON = resolveEmbeddedData()
if (dataFromJSON) return dataFromJSON.metaData
const metaData = {
userName: $('[itemprop="author"] > a[rel="author"]', e => e.textContent?.trim()) || undefined,
repoName: $('[itemprop="name"] > a[href]', e => e.textContent?.trim()) || undefined,
userName:
$(
'[itemprop="author"] > a[rel="author"]',
e => e.textContent?.trim(),
() => $(selectors.globalNavigation.navbar.repositoryOwner, e => e.textContent?.trim()),
) || undefined,
repoName:
$(
'[itemprop="name"] > a[href]',
e => e.textContent?.trim(),
() => $(selectors.globalNavigation.navbar.repositoryName, e => e.textContent?.trim()),
) || undefined,
branchName: getCurrentBranch(true),
}
if (!metaData.userName || !metaData.repoName) {
@ -19,17 +107,26 @@ export function resolveMeta(): Partial<MetaData> {
}
export function isInRepoPage() {
const repoHeadSelector = '.repohead' // legacy
const repoHeadSelector = '.repohead'
const authorNameSelector = '.author[itemprop="author"]'
return Boolean(
document.querySelector(repoHeadSelector) || document.querySelector(authorNameSelector),
document.querySelector(
[
repoHeadSelector,
authorNameSelector,
selectors.globalNavigation.navbar.repositoryOwner,
].join(),
),
)
}
export function isInCodePage() {
const branchListSelector = ['#branch-select-menu', '.branch-select-menu'].join()
const branchListSelector = [
selectors.normal.breadcrumbsFilename,
selectors.normal.branchSwitcher,
].join()
// The element may still exist in DOM for PR pages, but not visible
return Boolean($(branchListSelector, e => e.offsetWidth > 0 && e.offsetHeight > 0))
return Boolean($(branchListSelector))
}
export function isInPullFilesPage() {
@ -50,18 +147,21 @@ export function getCurrentBranch(passive = false) {
const selectedBranchButtonSelector = [
'main #branch-select-menu summary',
'main .branch-select-menu summary',
selectors.globalNavigation.branchSelector,
].join()
const branchButtonElement = $(selectedBranchButtonSelector)
if (branchButtonElement) {
const branchNameSpanElement = branchButtonElement.querySelector('span')
const branchNameSpanElement = branchButtonElement.querySelector(
['.ref-selector-button-text-container', 'span'].join(),
)
if (branchNameSpanElement) {
const partialBranchNameFromInnerText = branchNameSpanElement.textContent || ''
const partialBranchNameFromInnerText = branchNameSpanElement.textContent?.trim() || ''
if (partialBranchNameFromInnerText && !partialBranchNameFromInnerText.includes('…'))
return partialBranchNameFromInnerText
}
const defaultTitle = 'Switch branches or tags'
const title = branchButtonElement.title.trim()
if (title !== defaultTitle && !title.includes(' ')) return title
if (title && title !== defaultTitle && !title.includes(' ')) return title
}
const findFileButtonSelector = 'main .file-navigation a[data-hotkey="t"]'
@ -78,6 +178,17 @@ export function getCurrentBranch(passive = false) {
}
}
const branchNameFromCodeTab = $(selectors.normal.codeTab, e => {
if (e instanceof HTMLAnchorElement) {
const chunks = e.href.split('/')
const indexOfTree = chunks.indexOf('tree')
if (indexOfTree === -1) return
const branchName = chunks.slice(indexOfTree + 1).join('/')
return branchName
}
})
if (branchNameFromCodeTab) return branchNameFromCodeTab
if (!passive) raiseError(new Error('cannot get current branch'))
}
@ -104,33 +215,17 @@ const PAGE_TYPES = {
* TODO: distinguish type 'preview'
*/
function getCurrentPageType() {
const blobPathSelector = '#blob-path' // path next to branch switcher
const blobWrapperSelector = 'main .blob-wrapper table'
const readmeSelector = 'main .readme'
const searchResultSelector = '.codesearch-results'
const searchResultSelector = '.search-sub-header'
const blobPathSelector = '[aria-label="file content"]'
const readmeSelector = 'main #readme'
return (
$(searchResultSelector, () => PAGE_TYPES.SEARCH) ||
$(blobWrapperSelector, () => $(blobPathSelector, () => PAGE_TYPES.RAW_TEXT)) ||
$(blobPathSelector, () => PAGE_TYPES.RAW_TEXT) ||
$(readmeSelector, () => PAGE_TYPES.RENDERED) ||
PAGE_TYPES.OTHERS
)
}
const REPO_TYPE_PRIVATE = 'private' as const
const REPO_TYPE_PUBLIC = 'public' as const
export function getRepoPageType() {
const headerSelector = `main .pagehead.repohead h1`
return $(headerSelector, header => {
const repoPageTypes = [REPO_TYPE_PRIVATE, REPO_TYPE_PUBLIC]
for (const repoPageType of repoPageTypes) {
if (header.classList.contains(repoPageType)) {
return repoPageType
}
}
raiseError(new Error('cannot get repo page type'))
})
}
/**
* get text content of raw text content
*/
@ -145,49 +240,6 @@ export function getCodeElement() {
}
}
/**
* add copy file content buttons to button groups
* click these buttons will copy file content to clipboard
*/
export function attachCopyFileBtn() {
const removeButtons = () => {
const buttons = document.querySelectorAll(formatClass(copyFileButtonClassName))
buttons.forEach(button => {
button.parentElement?.removeChild(button)
})
}
if (getCurrentPageType() === PAGE_TYPES.RAW_TEXT) {
let buttonGroup: HTMLElement | null = null
if (!buttonGroup) {
const rawUrlButtonSelector = '#raw-url'
const $buttonGroup = document.querySelector(rawUrlButtonSelector)?.parentElement
if ($buttonGroup) buttonGroup = $buttonGroup
}
if (!buttonGroup) {
const buttonGroupSelector = 'main .Box-header .BtnGroup'
const buttonGroups = document.querySelectorAll(buttonGroupSelector)
const $buttonGroup = buttonGroups[buttonGroups.length - 1]
if ($buttonGroup) buttonGroup = $buttonGroup as HTMLElement
}
run(async () => {
if (!buttonGroup) raiseError(new Error(`No button groups found`))
else if (!buttonGroup.lastElementChild) return
else {
removeButtons() // prevent duplicated buttons
const button = await renderReact(React.createElement(CopyFileButton))
if (button instanceof HTMLElement) buttonGroup.appendChild(button)
}
})
}
// return callback so that disabling after redirecting from file page to non-page works properly
return removeButtons
}
export function attachCopySnippet() {
const readmeSelector = 'main div#readme'
return $(readmeSelector, () => {
@ -257,17 +309,35 @@ export function getPath() {
const pathElement =
document.querySelector(blobPathElementSelector) ||
document.querySelector(folderPathElementSelector)?.nextElementSibling
if (!pathElement?.querySelector('.js-repo-root')) {
return []
if (pathElement?.querySelector('.js-repo-root')) {
const path = (pathElement.textContent || '')
.replace(/\n/g, '')
.replace(/\/\s+Jump to.*/m, '')
.trim()
.split('/')
.filter(Boolean)
.slice(1) // the first is the repo's name
return path
}
const path = ((pathElement as HTMLDivElement).textContent || '')
.replace(/\n/g, '')
.replace(/\/\s+Jump to.*/m, '')
.trim()
.split('/')
.filter(Boolean)
.slice(1) // the first is the repo's name
return path
const pathContextElement = document.querySelector(
selectors.globalNavigation.pathContext,
)?.parentElement
let path = pathContextElement?.textContent?.trim()
if (path) {
// [Breadcrumbs]:repoName/:path
const screenReader = pathContextElement?.querySelector(
selectors.globalNavigation.pathContextScreenReaderHeading,
)
if (screenReader) path = path.replace(screenReader.textContent || '', '')
return path.split('/').slice(1)
}
return []
}
export function isNativeFileTreeShown() {
return Boolean($('#repos-file-tree'))
}
export function isNativePRFileTreeShown() {

View file

@ -0,0 +1,81 @@
import * as s from 'superstruct'
const repo = s.object({
id: s.number(),
defaultBranch: s.string(),
name: s.string(),
ownerLogin: s.string(),
currentUserCanPush: s.boolean(),
isFork: s.boolean(),
isEmpty: s.boolean(),
createdAt: s.string(),
ownerAvatar: s.string(),
public: s.boolean(),
private: s.boolean(),
isOrgOwned: s.boolean(),
})
const user = s.object({
id: s.number(),
login: s.string(),
userEmail: s.string(),
})
const rel = s.object({
name: s.string(),
listCacheKey: s.string(),
canEdit: s.boolean(),
refType: s.string(),
currentOid: s.string(),
})
const treeItem = s.object({
name: s.string(),
path: s.string(),
contentType: s.string(),
})
const tree = s.object({
items: s.array(treeItem),
templateDirectorySuggestionUrl: s.nullable(s.never()),
readme: s.nullable(s.never()),
totalCount: s.number(),
showBranchInfobar: s.boolean(),
})
const repoPayload = s.object({
allShortcutsEnabled: s.boolean(),
path: s.string(),
repo: repo,
currentUser: user,
refInfo: rel,
tree: tree,
fileTree: s.nullable(s.never()),
fileTreeProcessingTime: s.nullable(s.never()),
foldersToFetch: s.array(s.unknown()),
treeExpanded: s.boolean(),
symbolsExpanded: s.boolean(),
isOverview: s.boolean(),
overview: s.unknown(),
})
const reposOverview = s.object({
props: s.object({
initialPayload: repoPayload,
appPayload: s.unknown(),
}),
})
const app = s.object({
payload: repoPayload,
})
export const embeddedDataStruct = {
repo,
user,
rel,
treeItem,
tree,
repoPayload,
reposOverview,
app,
}

View file

@ -1,16 +0,0 @@
import { platform } from 'platforms'
import * as React from 'react'
import { useAfterRedirect } from 'utils/hooks/useFastRedirect'
import * as DOMHelper from '../DOMHelper'
import { GitHub } from '../index'
export function useGitHubAttachCopyFileButton(copyFileButton: boolean) {
const attachCopyFileButton = React.useCallback(
function attachCopyFileButton() {
if (platform === GitHub && copyFileButton) DOMHelper.attachCopyFileBtn()
},
[copyFileButton],
)
React.useEffect(attachCopyFileButton, [attachCopyFileButton])
useAfterRedirect(attachCopyFileButton)
}

View file

@ -7,13 +7,12 @@ import { resolveGitModules } from 'utils/gitSubmodule'
import { sortFoldersToFront } from 'utils/treeParser'
import * as API from './API'
import * as DOMHelper from './DOMHelper'
import * as URLHelper from './URLHelper'
import { getCommitTreeData } from './getCommitTreeData'
import { getPullRequestTreeData } from './getPullRequestTreeData'
import { useEnterpriseStatBarStyleFix } from './hooks/useEnterpriseStatBarStyleFix'
import { useGitHubAttachCopyFileButton } from './hooks/useGitHubAttachCopyFileButton'
import { useGitHubAttachCopySnippetButton } from './hooks/useGitHubAttachCopySnippetButton'
import { useGitHubCodeFold } from './hooks/useGitHubCodeFold'
import * as URLHelper from './URLHelper'
export function processTree(tree: TreeNode[]): TreeNode {
// nodes are created from items and put onto tree
@ -67,17 +66,27 @@ export function processTree(tree: TreeNode[]): TreeNode {
}
export function isEnterprise() {
if (window.location.host === 'github.com') return false
return (
(window.location.host !== 'github.com' &&
/**
* <a class="Header-link " href="https://host.com/" data-hotkey="g d" aria-label="Homepage Enterprise">
* <span>Enterprise</span>
* </a>
*/
$(
/**
* <a class="AppHeader-logo" href="https://host.com/" data-hotkey="g d" aria-label="Homepage Enterprise">
* <svg></svg>
* </a>
*/
$('a.AppHeader-logo[aria-label="Homepage Enterprise"]') !== null ||
/**
* <a class="Header-link " href="https://host.com/" data-hotkey="g d" aria-label="Homepage Enterprise">
* <span>Enterprise</span>
* </a>
*/
$(
[
'a.Header-link[aria-label="Homepage Enterprise"]',
e => e.textContent?.trim() === 'Enterprise',
)) ||
'a.Header-link[aria-label="Homepage"]', // legacy support
].join(),
e => e.textContent?.trim() === 'Enterprise',
) ||
false
)
}
@ -107,7 +116,7 @@ export const GitHub: Platform = {
}
const { type } = metaFromURL
let branchName
let branchName = metaFromDOM.branchName
if (URLHelper.isInPullPage()) {
branchName = DOMHelper.getIssueTitle()
} else if (URLHelper.isInCommitPage()) {
@ -128,6 +137,9 @@ export const GitHub: Platform = {
return metaData
},
async getDefaultBranchName({ userName, repoName }, accessToken) {
const dataFromJSON = DOMHelper.resolveEmbeddedData()
if (dataFromJSON?.defaultBranch) return dataFromJSON.defaultBranch
return (await API.getRepoMeta(userName, repoName, accessToken)).default_branch
},
resolveUrlFromMetaData({ userName, repoName, branchName }) {
@ -157,7 +169,7 @@ export const GitHub: Platform = {
},
shouldExpandSideBar() {
return Boolean(
DOMHelper.isInCodePage() ||
(DOMHelper.isInCodePage() && !DOMHelper.isNativePRFileTreeShown()) ||
URLHelper.isInCommitPage() ||
(URLHelper.isInPullPage() && !DOMHelper.isNativePRFileTreeShown()),
)
@ -190,8 +202,7 @@ export const GitHub: Platform = {
return `https://github.com/login/oauth/authorize?${params}`
},
usePlatformHooks() {
const { copyFileButton, copySnippetButton, codeFolding } = useConfigs().value
useGitHubAttachCopyFileButton(copyFileButton)
const { copySnippetButton, codeFolding } = useConfigs().value
useGitHubAttachCopySnippetButton(copySnippetButton)
useGitHubCodeFold(codeFolding)
useEnterpriseStatBarStyleFix()

View file

@ -78,7 +78,7 @@ export async function getDOM(url: string) {
return new DOMParser().parseFromString(await (await fetch(url)).text(), 'text/html')
}
export async function continuousLoadPages(doc: Document, onReceivePage?: (doc: Document) => void) {
export async function continuousLoadFragmentedPages(doc: Document) {
/**
* <include-fragment
* src="..."
@ -91,17 +91,21 @@ export async function continuousLoadPages(doc: Document, onReceivePage?: (doc: D
'include-fragment[data-targets="diff-file-filter.progressiveLoaders"]',
'.js-diff-progressive-container include-fragment[src]', // legacy support
]
const documents: Document[] = [doc]
// eslint-disable-next-line no-constant-condition
while (true) {
const fragment = doc.querySelector(fragmentSelectors.join()) as HTMLElement
if (!fragment) break
const src = fragment.getAttribute('src')
if (!src) break
// Using `src` directly below would fail in Firefox if the src is an absolute path
doc = await getDOM(new URL(src, window.location.origin).href)
documents.push(doc)
onReceivePage?.(doc)
const selector = fragmentSelectors.find(selector => doc.querySelector(selector))
if (selector) {
// eslint-disable-next-line no-constant-condition
while (true) {
const fragment = doc.querySelector(selector)
if (!(fragment instanceof HTMLElement)) break
const src = fragment.getAttribute('src')
if (!src) break
// Using `src` without origin below would fail in Firefox if the src is an absolute path
doc = await getDOM(new URL(src, window.location.origin).href)
documents.push(doc)
}
}
return documents
}

View file

@ -117,7 +117,7 @@ export const Gitea: Platform = {
},
async getTreeData(metaData, path, recursive, accessToken) {
const { userName, repoName, branchName } = metaData
const treeData = await API.getTreeData(userName, repoName, branchName, recursive)
const treeData = await API.getTreeData(userName, repoName, branchName, recursive, accessToken)
const root = processTree(
treeData.tree.map(item => ({

View file

@ -26,17 +26,6 @@ export function getCurrentBranch() {
raiseError(new Error('cannot get current branch'))
}
const REPO_TYPE_PRIVATE = 'private' as const
const REPO_TYPE_PUBLIC = 'public' as const
export function getRepoPageType() {
const headerSelector = `.git-project-title .icon-lock`
return $(
headerSelector,
() => REPO_TYPE_PRIVATE,
() => REPO_TYPE_PUBLIC,
)
}
export function attachCopySnippet() {
const readmeSelector = '.file_content.markdown-body'
return $(readmeSelector, () => {

View file

@ -1,6 +1,6 @@
import { GITEE_OAUTH } from 'env'
import { Base64 } from 'js-base64'
import { platform } from 'platforms'
import { errors, platform } from 'platforms'
import * as React from 'react'
import { resolveGitModules } from 'utils/gitSubmodule'
import { useAfterRedirect } from 'utils/hooks/useFastRedirect'
@ -180,6 +180,10 @@ export const Gitee: Platform = {
usePlatformHooks() {
useProgressBar()
},
mapErrorMessage: (error: Error) =>
({
['Only signed in user is allowed to call APIs.']: errors.BAD_CREDENTIALS,
}[error.message]),
}
export function useGiteeAttachCopySnippetButton(copySnippetButton: boolean) {

View file

@ -29,4 +29,5 @@ type Platform = {
| void
loadWithFastRedirect?(url: string, element: HTMLElement): boolean | void
usePlatformHooks?(): void
mapErrorMessage?: (error: Error) => string | void
}

View file

@ -41,7 +41,9 @@ $minimal-z-index: max(
}
html[data-with-gitako-spacing='true'] {
body {
body,
.AppHeader deferred-side-panel div[data-modal-dialog-overlay] // side panel opened via clicking github top-left icon in global navigation mode
{
@media screen {
margin-left: var(--gitako-width);
}
@ -585,6 +587,11 @@ html[data-with-gitako-spacing='true'] {
padding: 2px 6px 2px 10px;
border-top: 1px solid var(--color-border-muted);
&-section {
display: inline-flex;
gap: 8px;
}
.version {
color: var(--color-fg-muted);
}

View file

@ -1,15 +1,15 @@
export function $(selector: string): HTMLElement | null
export function $<T1>(selector: string, existCallback: (element: HTMLElement) => T1): T1 | null
export function $<T1, T2>(
export function $<E extends HTMLElement>(selector: string): E | null
export function $<R1>(selector: string, existCallback: (element: HTMLElement) => R1): R1 | null
export function $<R1, R2>(
selector: string,
existCallback: (element: HTMLElement) => T1,
otherwise: () => T2,
): T1 | T2
export function $<T2>(
existCallback: (element: HTMLElement) => R1,
otherwise: () => R2,
): R1 | R2
export function $<E extends HTMLElement, R2>(
selector: string,
existCallback: undefined | null,
otherwise: () => T2,
): HTMLElement | T2
otherwise: () => R2,
): E | R2
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function $(selector: string, existCallback?: any, otherwise?: any) {
const element = document.querySelector(selector)

View file

@ -103,34 +103,37 @@ export function setBodyIndent(shouldShowGitako: boolean) {
* </html>
*/
const mountPointContainer = document.documentElement
export function insertMountPoint() {
return $(formatID(rootElementID), undefined, () => {
const mountPointContainer = document.body
export function insertMountPoint(
create = () => {
const element = document.createElement('div')
element.setAttribute('id', rootElementID)
mountPointContainer.appendChild(element)
return element
})
},
) {
return $(formatID(rootElementID), undefined, () => mountPointContainer.appendChild(create()))
}
export function insertSideBarMountPoint() {
const sidebarMountPointID = 'gitako-sidebar-mount-point'
return $(formatID(sidebarMountPointID), undefined, () => {
const sideBarElement = document.createElement('div')
sideBarElement.setAttribute('id', sidebarMountPointID)
insertMountPoint().appendChild(sideBarElement)
return sideBarElement
})
const id = 'gitako-sidebar-mount-point'
const create = () => {
const element = document.createElement('div')
element.setAttribute('id', id)
return element
}
return $<HTMLDivElement, HTMLDivElement>(formatID(id), undefined, () =>
insertMountPoint().appendChild(create()),
)
}
export function insertLogoMountPoint() {
const logoMountPointID = 'gitako-logo-mount-point'
return $(formatID(logoMountPointID), undefined, () => {
const logoMountElement = document.createElement('div')
logoMountElement.setAttribute('id', logoMountPointID)
insertMountPoint().appendChild(logoMountElement)
return logoMountElement
})
const id = 'gitako-logo-mount-point'
const create = () => {
const element = document.createElement('div')
element.setAttribute('id', id)
return element
}
return $(formatID(id), undefined, () => insertMountPoint().appendChild(create()))
}
/**

View file

@ -9,7 +9,6 @@ export type Config = {
focusSearchInputShortcut: string | undefined // shortcut for focusing search input
accessToken: string | undefined
compressSingletonFolder: boolean
copyFileButton: boolean
copySnippetButton: boolean
intelligentToggle: boolean | null // `null` stands for intelligent, boolean for sidebar open state
icons: 'rich' | 'dim' | 'native'
@ -23,6 +22,7 @@ export type Config = {
restoreExpandedFolders: boolean
pjaxMode: 'native' | 'pjax-api'
showDiffInText: boolean
__showInspector?: boolean
}
export type ConfigKeys = keyof Config
@ -33,7 +33,6 @@ enum configKeys {
focusSearchInputShortcut = 'focusSearchInputShortcut',
accessToken = 'accessToken',
compressSingletonFolder = 'compressSingletonFolder',
copyFileButton = 'copyFileButton',
copySnippetButton = 'copySnippetButton',
intelligentToggle = 'intelligentToggle',
icons = 'icons',
@ -47,6 +46,7 @@ enum configKeys {
restoreExpandedFolders = 'restoreExpandedFolders',
pjaxMode = 'pjaxMode',
showDiffInText = 'showDiffInText',
__showInspector = '__showInspector',
}
// NOT use platform name to distinguish GHE from github.com
@ -59,7 +59,6 @@ export const getDefaultConfigs: () => Config = () => ({
focusSearchInputShortcut: undefined,
accessToken: '',
compressSingletonFolder: true,
copyFileButton: !isInGitHub, // disable on github.com
copySnippetButton: !isInGitHub, // disable on github.com
intelligentToggle: null,
icons: 'rich',
@ -77,7 +76,7 @@ export const getDefaultConfigs: () => Config = () => ({
const configKeyArray = Object.values(configKeys)
function applyDefaultConfigs(configs: Partial<Config>) {
function applyDefaultConfigs(configs: Partial<Config> = {}) {
const defaultConfigs = getDefaultConfigs()
return configKeyArray.reduce((applied, key) => {
Object.assign(applied, { [key]: key in configs ? configs[key] : defaultConfigs[key] })
@ -95,15 +94,45 @@ const updateConfigRef = async (config: Partial<Config>) => {
const configMigration = migrateConfig()
configMigration.then(async () => updateConfigRef(await get()))
let loadedConfigFromURL = false
function getConfigFromURL() {
const config: Partial<Config> = {}
// config params pattern:
// ?gitako-config-<key>=<json-value>
new URLSearchParams(window.location.search).forEach((value, key) => {
if (key.match(/^gitako-config-/)) {
const configKey = key.replace(/^gitako-config-/, '')
if (configKey in configKeys) {
try {
config[configKeys[configKey as configKeys]] = JSON.parse(value)
loadedConfigFromURL = true
} catch (error) {
throw new Error(`Failed to parse config "${configKey}" from URL: ${value}`, {
cause: error,
})
}
} else {
console.warn(`Unknown config "${configKey}" from URL: ${value}`)
}
}
})
return config
}
async function get(): Promise<Config> {
await configMigration
const config = await storageHelper.get<Record<string, Config>>([platformStorageKey])
return applyDefaultConfigs(config?.[platformStorageKey] || {})
const savedConfig = (await storageHelper.get<Record<string, Config>>([platformStorageKey]))?.[
platformStorageKey
]
const configFromURL = getConfigFromURL()
return applyDefaultConfigs({ ...savedConfig, ...configFromURL })
}
async function set(config: Config) {
updateConfigRef(config)
return await storageHelper.set({ [platformStorageKey]: config })
if (!loadedConfigFromURL) await storageHelper.set({ [platformStorageKey]: config })
}
export const configHelper = { get, set }

View file

@ -1,5 +1,7 @@
import { getSafeWidth, MINIMAL_CONTENT_VIEWPORT_WIDTH, MINIMAL_WIDTH } from './getSafeWidth'
jest.retryTimes(3) // Math.random may result in failure due to floating point precision
it(`should shrink when window is being resized smaller`, () => {
const randomGrow = 100 * Math.random()
expect(

View file

@ -1,7 +1,7 @@
import { useConfigs } from 'containers/ConfigsContext'
import { platform } from 'platforms'
import * as React from 'react'
import { useEvent } from 'react-use'
import { useEvent, useInterval } from 'react-use'
const config: import('pjax-api').Config = {
areas: [
@ -54,8 +54,17 @@ export const loadWithFastRedirect = (url: string, element: HTMLElement) => {
}
export function useAfterRedirect(callback: () => void) {
useEvent('pjax:end', callback, document) // legacy support
useEvent('turbo:render', callback, document) // prevent page content shift after first redirect to new page via turbo when sidebar is pinned
const latestHref = React.useRef(location.href)
const raceCallback = React.useCallback(() => {
const { href } = location
if (latestHref.current !== href) {
latestHref.current = href
callback()
}
}, [callback])
useInterval(raceCallback, 500)
useEvent('pjax:end', raceCallback, document) // legacy support
useEvent('turbo:render', raceCallback, document) // prevent page content shift after first redirect to new page via turbo when sidebar is pinned
}
export function useRedirectedEvents(

View file

@ -1,5 +1,5 @@
import { useConfigs } from 'containers/ConfigsContext'
import { errors, platformName } from 'platforms'
import { errors, platform, platformName } from 'platforms'
import { useCallback } from 'react'
import { useLoadedContext } from 'utils/hooks/useLoadedContext'
import { SideBarErrorContext } from '../../containers/ErrorContext'
@ -12,33 +12,34 @@ export function useHandleNetworkError() {
return useCallback(
function handleNetworkError(err: Error) {
if (err.message === errors.EMPTY_PROJECT) {
const message = platform.mapErrorMessage?.(err) || err.message
if (message === errors.EMPTY_PROJECT) {
changeErrorContext('This project seems to be empty.')
return
}
if (err.message === errors.BLOCKED_PROJECT) {
if (message === errors.BLOCKED_PROJECT) {
changeErrorContext('Access to the project is blocked.')
return
}
if (
err.message === errors.NOT_FOUND ||
err.message === errors.BAD_CREDENTIALS ||
err.message === errors.API_RATE_LIMIT
message === errors.NOT_FOUND ||
message === errors.BAD_CREDENTIALS ||
message === errors.API_RATE_LIMIT
) {
changeStateContext('error-due-to-auth')
return
}
if (err.message === errors.CONNECTION_BLOCKED) {
if (message === errors.CONNECTION_BLOCKED) {
if (accessToken) changeErrorContext(`Cannot connect to ${platformName}.`)
else changeStateContext('error-due-to-auth')
return
}
if (err.message === errors.SERVER_FAULT) {
if (message === errors.SERVER_FAULT) {
changeErrorContext(`${platformName} server went down.`)
return
}

View file

@ -5,7 +5,7 @@
"target": "ESNext",
"jsx": "react",
"strict": true,
"lib": ["dom", "es2017.object", "es2016", "ES2019.Array", "ES2020.String"],
"lib": ["dom", "es2017.object", "es2016", "ES2019.Array", "ES2020.String", "ES2022.Error"],
"baseUrl": "src",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,

View file

@ -117,11 +117,11 @@ module.exports = {
sideEffects: false,
},
{
test: /\.js$/,
test: /\.m?js$/,
loader: 'babel-loader',
// Transpile as least files under node_modules
include:
/node_modules\/(webext-content-scripts|webext-detect-page|webext-dynamic-content-scripts)\/.*\.js$/,
/node_modules\/(webext-content-scripts|webext-detect-page|webext-dynamic-content-scripts|superstruct)\/.*\.m?js$/,
options: {
cacheDirectory: true,
},

174
yarn.lock
View file

@ -2812,11 +2812,6 @@ abab@^2.0.6:
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
@ -4039,7 +4034,7 @@ chokidar@^3.4.0, chokidar@^3.4.2:
optionalDependencies:
fsevents "~2.3.2"
chownr@^1.1.1, chownr@^1.1.4:
chownr@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
@ -4722,13 +4717,6 @@ debug@4.3.1:
dependencies:
ms "2.1.2"
debug@^3.2.6:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
dependencies:
ms "^2.1.1"
debug@^4.3.2, debug@^4.3.4, debug@~4.3.1:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
@ -4877,11 +4865,6 @@ detect-file@^1.0.0:
resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
detect-libc@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
detect-newline@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
@ -6092,13 +6075,6 @@ fs-extra@~9.0.1:
jsonfile "^6.0.1"
universalify "^1.0.0"
fs-minipass@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
dependencies:
minipass "^2.6.0"
fs-monkey@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3"
@ -6124,23 +6100,10 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
fsevents@^1.2.7:
version "1.2.9"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f"
integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==
dependencies:
nan "^2.12.1"
node-pre-gyp "^0.12.0"
fsevents@^2.3.2, fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
fsevents@~2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805"
integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==
fsevents@^1.2.7, fsevents@^2.3.2, fsevents@^2.3.3, fsevents@~2.1.2, fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.1:
version "1.1.1"
@ -6786,7 +6749,7 @@ hyphenate-style-name@^1.0.2:
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==
iconv-lite@0.4.24, iconv-lite@^0.4.4:
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@ -6827,13 +6790,6 @@ iferr@^0.1.5:
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
ignore-walk@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.2.tgz#99d83a246c196ea5c93ef9315ad7b0819c35069b"
integrity sha512-EXyErtpHbn75ZTsOADsfx6J/FPo6/5cjev46PXrcTpd8z3BoRkXgYu9/JVqrI7tusjmwCZutGeRJeU0Wo1e4Cw==
dependencies:
minimatch "^3.0.4"
ignore@^3.3.5:
version "3.3.10"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
@ -8697,21 +8653,6 @@ minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
minipass@^2.6.0, minipass@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
dependencies:
safe-buffer "^5.1.2"
yallist "^3.0.0"
minizlib@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
dependencies:
minipass "^2.9.0"
mississippi@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
@ -8816,7 +8757,7 @@ mz@2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
nan@^2.12.1, nan@^2.14.0:
nan@^2.14.0:
version "2.14.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
@ -8867,15 +8808,6 @@ ncp@~2.0.0:
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
needle@^2.2.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c"
integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==
dependencies:
debug "^3.2.6"
iconv-lite "^0.4.4"
sax "^1.2.4"
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@ -8947,22 +8879,6 @@ node-notifier@10.0.1:
uuid "^8.3.2"
which "^2.0.2"
node-pre-gyp@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149"
integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==
dependencies:
detect-libc "^1.0.2"
mkdirp "^0.5.1"
needle "^2.2.1"
nopt "^4.0.1"
npm-packlist "^1.1.6"
npmlog "^4.0.2"
rc "^1.2.7"
rimraf "^2.6.1"
semver "^5.3.0"
tar "^4"
node-releases@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96"
@ -8973,14 +8889,6 @@ node-releases@^2.0.6:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
nopt@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=
dependencies:
abbrev "1"
osenv "^0.1.4"
normalize-path@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
@ -9008,19 +8916,6 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
npm-bundled@^1.0.1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd"
integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==
npm-packlist@^1.1.6:
version "1.4.4"
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44"
integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw==
dependencies:
ignore-walk "^3.0.1"
npm-bundled "^1.0.1"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@ -9042,7 +8937,7 @@ npm-run-path@^5.1.0:
dependencies:
path-key "^4.0.0"
npmlog@^4.0.2, npmlog@^4.1.2:
npmlog@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@ -9260,7 +9155,7 @@ os-browserify@^0.3.0:
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
os-homedir@^1.0.0, os-homedir@^1.0.1:
os-homedir@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
@ -9288,19 +9183,6 @@ os-shim@^0.1.2:
resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917"
integrity sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=
os-tmpdir@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
osenv@^0.1.4:
version "0.1.5"
resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
dependencies:
os-homedir "^1.0.0"
os-tmpdir "^1.0.0"
p-cancelable@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050"
@ -9726,10 +9608,10 @@ prepend-http@^1.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
prettier@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
prettier@^2.8.3:
version "2.8.3"
resolved "https://registry.npmmirror.com/prettier/-/prettier-2.8.3.tgz#ab697b1d3dd46fb4626fbe2f543afe0cc98d8632"
integrity sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==
pretty-format@^27.0.2, pretty-format@^27.5.1:
version "27.5.1"
@ -10014,7 +9896,7 @@ raw-loader@^4.0.0:
loader-utils "^1.2.3"
schema-utils "^2.5.0"
rc@1.2.8, rc@^1.2.7:
rc@1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
@ -10494,7 +10376,7 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3:
rimraf@^2.5.4, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@ -10556,7 +10438,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@ -10601,7 +10483,7 @@ sass@^1.26.2:
dependencies:
chokidar ">=2.0.0 <4.0.0"
sax@>=0.6.0, sax@^1.2.4:
sax@>=0.6.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@ -10687,7 +10569,7 @@ semver@7.3.7, semver@^7.3.5, semver@^7.3.7:
dependencies:
lru-cache "^6.0.0"
semver@^5.3.0, semver@^5.5.0, semver@^5.6.0:
semver@^5.5.0, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -11443,6 +11325,11 @@ stylis@^4.0.6:
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.1.tgz#e46c6a9bbf7c58db1e65bb730be157311ae1fe12"
integrity sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==
superstruct@^1.0.3:
version "1.0.3"
resolved "https://registry.npmmirror.com/superstruct/-/superstruct-1.0.3.tgz#de626a5b49c6641ff4d37da3c7598e7a87697046"
integrity sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg==
supports-color@6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
@ -11512,19 +11399,6 @@ tar-stream@^2.0.0:
inherits "^2.0.3"
readable-stream "^3.1.1"
tar@^4:
version "4.4.19"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3"
integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==
dependencies:
chownr "^1.1.4"
fs-minipass "^1.2.7"
minipass "^2.9.0"
minizlib "^1.3.3"
mkdirp "^0.5.5"
safe-buffer "^5.2.1"
yallist "^3.1.1"
terser-webpack-plugin@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4"
@ -12597,7 +12471,7 @@ y18n@^5.0.5:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
yallist@^3.0.0, yallist@^3.0.2, yallist@^3.1.1:
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==