svelte: Towards a better data fetching and GraphQL authoring experience (#59383)

This PR refactors almost all of the prototypes GraphQL queries to take advantage of GraphQLs compossibility via fragments. The goal is to provide a more structured approach to authoring and executing GraphQL queries, with the following advantages:
- Data dependency co-location makes it easier to maintain/extend individual components.
- Data fetching happens in specific, predictable places (layout and page loaders).

On a high level it works like this:

- Components declare their data dependencies in `<Component>.gql` files next to them. Thanks to GraphQL code generation they can import the corresponding TypeScript types via `import type { SomeFragment} from './<Component>.gql'`.
- Higher level components compose the fragments of their children.
- At the page/layout level `page.gql`/`layout.gql` files define the queries, composed from the data dependencies of the page/layout.
- The page/layout data loaders can import queries directly from the corresponding `.gql` file.

Authoring the `.gql` files should be relatively easily if the graphql language server is setup. The changes in the `.graphlrc` file make all fragments globally available which means that every fragment needs to be unique and we don't need to use unofficial `#import` directives inside `.gql` files.

There are a couple of things to consider though:

- Caching: If different pages/layouts fetch the same data with different queries, we won't leverage caching without additional setup. That's something I still need to look into. That also means that sometimes we might want to use a shared queries instead of composition/co-location, if caching is more important.
- Shared layout data: Some data fetched in layouts is accessed by sub-layouts/sub-pages, but the layout doesn't know which sub-layout/sub-page is loaded, making query composition more difficult. So it far it seems that data shared this way is rather limited/constrained. So my current approach is to have components/pages define fragments following a specific naming convention, and have the loaders that provide this data compose them. Examples for this is `SearchInput_AuthorizedUser` and `RepoPage_ResolvedRevision`.
  This is not ideal because it has to be remembered to embed this fragment in the right place, but it's not worse than the current situation (where we often don't know where the query providing some data is defined).
- On demand data fetching: Not all data is necessary for rendering a page, some data is only fetched when in response to some user interaction. The layout/page loader should still be the place that executes the query, but instead of doing it on page load it passes a function to the page to fetch the data on demand. This way we can maintain data co-location and fetching in loaders. For an example see the `fetchCommitHistory` function.

NOTE: I expect there to be changes to this approach as we uncover more data loading requirements.
This commit is contained in:
Felix Kling 2024-01-09 10:33:30 +01:00 committed by GitHub
parent a5984c7d6e
commit 031bb871fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 1038 additions and 1232 deletions

View File

@ -18,6 +18,7 @@ yarn.lock
# Generated files
*.gql.d.ts
*.gql.ts
src/lib/graphql-operations.ts
src/lib/graphql-types.ts
static/mockServiceWorker.js

View File

@ -10,5 +10,6 @@ vite.config.ts.timestamp-*
# Generated TypeScript type definitions and GraphQL documents
*.gql.d.ts
*.gql.ts
src/lib/graphql-types.ts
src/lib/graphql-operations.ts

View File

@ -0,0 +1,4 @@
{
"schema": "../../cmd/frontend/graphqlbackend/*.graphql",
"documents": "src/**/*.gql"
}

View File

@ -14,6 +14,7 @@ package-lock.json
yarn.lock
/static/mockServiceWorker.js
*.gql.d.ts
*.gql.ts
graphql-operations.ts
graphql-types.ts

View File

@ -43,10 +43,10 @@ BUILD_DEPS = [
":node_modules/@graphql-codegen/typescript",
":node_modules/@graphql-codegen/typescript-operations",
":node_modules/@graphql-codegen/near-operation-file-preset",
":node_modules/@graphql-codegen/typed-document-node",
":node_modules/@graphql-tools/utils",
":node_modules/@melt-ui/svelte",
":node_modules/@popperjs/core",
":node_modules/@rollup/plugin-graphql",
":node_modules/@sourcegraph/branded",
":node_modules/@sourcegraph/common",
":node_modules/@sourcegraph/http-client",

View File

@ -10,8 +10,6 @@ implementation of the Sourcegraph app.
```bash
# Install dependencies
pnpm install
# Generate GraphQL types
pnpm run -w generate
# Run dev server
pnpm run dev
```
@ -31,10 +29,9 @@ packages:
- Since we use the [barrel](https://basarat.gitbook.io/typescript/main-1/barrel)
style of organizing our modules, many (unused) dependencies are imported into
the app. This isn't really available, and at best will only increase the
initial loading time. But some modules, especially those that access browser
specific features during module initialization, can even cause the dev build
to fail.
the app. This isn't ideal and at best will only increase the initial loading
time. Some modules, especially those that access browser specific features
during module initialization, can even cause the dev build to fail.
- Reusing code is great, but also potentially exposes someone who modifies the
reused code to this package and therefore Svelte (if the reused code changes
in an incompatible way, this package needs to be updated too). To limit the
@ -69,6 +66,39 @@ because it also validates imported modules from other packages, and we are not
explicitly marking type-only imports with `type` in other parts of the code
base (which is required by this package).
### Data loading with GraphQL
This project makes use of query composition, i.e. components define their own
data dependencies via fragments, which get composed by their callers and are
eventually being used in a query in a loader.
This goal of this approach is to make data dependencies co-located and easier
to change, as well to make the flow of data clearer. Data fetching should only
happen in data loaders, not components.
There are a couple of issues to consider with this approach and sometimes we'll
have to make exceptions:
- Caching: If every loader composes its own query it's possible that two
queries fetch the same data, in which case we miss out on caching. If caching
the data is more important than data co-location it might be preferable to
define a reusable query function. Example: File list for currently opened
folder (sidebar + folder page)
- Shared data from layout loaders: While it's very convenient that pages have
access to any data from the ancestor layout loaders, that doesn't work well
with data dependency co-location. The layout loaders don't know which
sub-layout or sub-page is loaded and what data it needs.
Fortunately we don't have a lot of data (yet) that is used this way. The
prime example for this right now is information about the authenticated user.
The current approach is to name data-dependencies on the current user as
`<ComponentName>_AuthenticatedUser` and use that fragment in the
`AuthenticatedUser` fragment in `src/routes/layout.gql`.
This approach might change as we uncover more use cases.
- On demand data loading: Not all data is fetched/needed immediately for
rendering page. Data for e.g. typeaheads is fetched on demand. Ideally the
related queries are still composed by the data loader, which passes a
function for fetching the data to the page.
## Production build
A production version of this app can be built with

View File

@ -1,49 +0,0 @@
// @ts-check
const { visit } = require('graphql')
/**
* Custom version of the `typed-document-node` plugin that only generates the type definitions
* for the documents without generating a parsed version. This is all we need because the
* `@rollup/plugin-graphql` plugin already parses the documents for us.
*
* @param {import('graphql').GraphQLSchema} _schema
* @param {import('@graphql-codegen/plugin-helpers').Types.DocumentFile[]} documents
* @param {{operationResultSuffix?: string}} config
*/
const plugin = (_schema, documents, config) => {
const { operationResultSuffix = '' } = config
/** @type {{name: string}[]} */
const allOperations = []
for (const item of documents) {
if (item.document) {
visit(item.document, {
OperationDefinition: {
enter: node => {
if (node.name && node.name.value) {
allOperations.push({
name: node.name.value,
})
}
},
},
})
}
}
const documentNodes = allOperations.map(
({ name }) =>
`export declare const ${name}: TypedDocumentNode<${name}${operationResultSuffix}, ${name}Variables>`
)
if (documentNodes.length === 0) {
return ''
}
return {
prepend: ["import type { TypedDocumentNode } from '@graphql-typed-document-node/core'\n"],
content: documentNodes.join('\n'),
}
}
module.exports = { plugin }

View File

@ -20,12 +20,12 @@
"@faker-js/faker": "^8.0.2",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/near-operation-file-preset": "^3.0.0",
"@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-tools/utils": "^10.0.11",
"@graphql-typed-document-node/core": "^3.2.0",
"@playwright/test": "1.25.0",
"@rollup/plugin-graphql": "^2.0.4",
"@storybook/addon-essentials": "^7.2.0",
"@storybook/addon-interactions": "^7.2.0",
"@storybook/addon-links": "^7.2.0",

View File

@ -0,0 +1,16 @@
fragment Avatar_User on User {
avatarURL
displayName
username
}
fragment Avatar_Person on Person {
displayName
avatarURL
name
}
fragment Avatar_Team on Team {
avatarURL
displayName
}

View File

@ -1,10 +1,10 @@
<script lang="ts" context="module">
import { Story } from '@storybook/addon-svelte-csf'
import UserAvatar from './UserAvatar.svelte'
import Avatar from './Avatar.svelte'
import { faker } from '@faker-js/faker'
export const meta = {
component: UserAvatar,
component: Avatar,
}
</script>
@ -17,9 +17,9 @@
<Story name="Default">
<h2>With <code>avatarURL</code></h2>
<UserAvatar user={{ avatarURL }} />
<Avatar avatar={{ avatarURL, displayName: null }} />
<h2>With <code>username</code> "{username}"</h2>
<UserAvatar user={{ username }} />
<Avatar avatar={{ username, avatarURL: null, displayName: null }} />
<h2>With <code>displayName</code> "{displayName}"</h2>
<UserAvatar user={{ displayName }} />
<Avatar avatar={{ displayName, avatarURL: null, username }} />
</Story>

View File

@ -1,11 +1,9 @@
<script lang="ts">
interface User {
avatarURL?: string | null
displayName?: string | null
username?: string | null
}
import type { Avatar_User, Avatar_Team, Avatar_Person } from './Avatar.gql'
export let user: User
type Avatar = Avatar_User | Avatar_Team | Avatar_Person
export let avatar: Avatar
function getInitials(name: string): string {
const names = name.split(' ')
@ -16,11 +14,25 @@
return initials[0]
}
$: name = user.displayName || user.username || ''
function getName(avatar: Avatar): string {
switch (avatar.__typename) {
case 'User':
return avatar.displayName || avatar.username || ''
case 'Person':
return avatar.displayName || avatar.name || ''
case 'Team':
return avatar.displayName || ''
default:
return ''
}
}
$: name = getName(avatar)
$: avatarURL = avatar.avatarURL
</script>
{#if user.avatarURL}
<img src={user.avatarURL} role="presentation" aria-hidden="true" alt="Avatar of {name}" />
{#if avatarURL}
<img src={avatarURL} role="presentation" aria-hidden="true" alt="Avatar of {name}" />
{:else}
<div>
<span>{getInitials(name)}</span>

View File

@ -1,12 +1,31 @@
<script lang="ts" context="module">
import type { BlobFileFields } from '$lib/repo/api/blob'
import { HovercardView } from '$lib/repo/HovercardView'
export interface BlobInfo extends BlobFileFields {
commitID: string
filePath: string
export interface BlobInfo {
/**
* Name of the repository this file belongs to.
*/
repoName: string
/**
* The commit OID of the currently viewed commit.
*/
commitID: string
/**
* Human readable version of the current commit (e.g. branch name).
*/
revision: string
/**
* The path of the file relative to the repository root.
*/
filePath: string
/**
* The content of the file.
*/
content: string
/**
* The language of the file.
*/
languages: string[]
}
const extensionsCompartment = new Compartment()

View File

@ -8,18 +8,15 @@ fragment Commit on GitCommit {
date
person {
name
displayName
email
avatarURL
...Avatar_Person
}
}
committer {
date
person {
name
displayName
email
avatarURL
...Avatar_Person
}
}
}

View File

@ -3,7 +3,7 @@
import type { Commit } from './Commit.gql'
import Icon from '$lib/Icon.svelte'
import UserAvatar from '$lib/UserAvatar.svelte'
import Avatar from '$lib/Avatar.svelte'
import Timestamp from '$lib/Timestamp.svelte'
import Tooltip from '$lib/Tooltip.svelte'
@ -31,13 +31,13 @@
<div class="root">
<div class="avatar">
<Tooltip tooltip={authorAvatarTooltip}>
<UserAvatar user={author} />
<Avatar avatar={author} />
</Tooltip>
</div>
{#if committer && committer.name !== author.name}
<div class="avatar">
<Tooltip tooltip="{committer.name} (committer)">
<UserAvatar user={committer} />
<Avatar avatar={committer} />
</Tooltip>
</div>
{/if}
@ -50,7 +50,7 @@
</button>
{/if}
</span>
<span>committed by <strong>{commit.author.person.name}</strong> <Timestamp date={commitDate} /></span>
<span>committed by <strong>{author.name}</strong> <Timestamp date={commitDate} /></span>
{#if expanded}
<pre>{commit.body}</pre>
{/if}

View File

@ -16,6 +16,7 @@ export { highlightNodeMultiline, highlightNode } from '@sourcegraph/common/src/u
export { logger } from '@sourcegraph/common/src/util/logger'
export { isSafari } from '@sourcegraph/common/src/util/browserDetection'
export { isExternalLink, type LineOrPositionOrRange } from '@sourcegraph/common/src/util/url'
export { parseJSONCOrError } from '@sourcegraph/common/src/util/jsonc'
let highlightingLoaded = false

View File

@ -1,23 +0,0 @@
import { gql, query } from '$lib/graphql'
import type { EvaluatedFeatureFlagsResult } from '$lib/graphql-operations'
export interface FeatureFlag {
name: string
value: boolean
}
const FEATUREFLAGS_QUERY = gql`
query EvaluatedFeatureFlags {
evaluatedFeatureFlags {
name
value
}
}
`
export async function fetchEvaluatedFeatureFlags(): Promise<FeatureFlag[]> {
return (
await query<EvaluatedFeatureFlagsResult>(FEATUREFLAGS_QUERY, undefined, {
fetchPolicy: 'no-cache',
})
).evaluatedFeatureFlags
}

View File

@ -1,2 +1 @@
export * from './stores'
export * from './api'

View File

@ -3,7 +3,7 @@ import { derived, readable, type Readable } from 'svelte/store'
import { getStores } from '$lib/stores'
import type { FeatureFlagName } from '$lib/web'
import type { FeatureFlag } from './api'
import type { FeatureFlag } from '../../routes/layout.gql'
const MINUTE = 60000
const FEATURE_FLAG_CACHE_TTL = MINUTE * 10

View File

@ -1 +0,0 @@
export * from '@sourcegraph/shared/src/graphql-operations'

View File

@ -0,0 +1,3 @@
fragment BottomPanel_HistoryConnection on GitCommitConnection {
...HistoryPanel_HistoryConnection
}

View File

@ -11,14 +11,14 @@
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import type { HistoryResult } from '$lib/graphql-operations'
import { fetchRepoCommits } from '$lib/repo/api/commits'
import TabPanel from '$lib/TabPanel.svelte'
import Tabs from '$lib/Tabs.svelte'
import HistoryPanel, { type Capture as HistoryPanelCapture } from './HistoryPanel.svelte'
import type { BottomPanel_HistoryConnection } from './BottomPanel.gql'
export let history: Promise<HistoryResult>
export let history: Promise<BottomPanel_HistoryConnection | null>
export let fetchCommitHistory: (afterCursor: string | null) => Promise<BottomPanel_HistoryConnection | null>
export function capture(): Capture {
return {
@ -51,18 +51,6 @@
selectedTab = event.detail
}
async function fetchMoreHistory(pageInfo: HistoryResult['pageInfo']) {
if (!$page.data.resolvedRevision) {
throw new Error('Unable to resolve repo revision')
}
return fetchRepoCommits({
repoID: $page.data.resolvedRevision.repo.id,
revision: $page.data.resolvedRevision.commitID,
filePath: $page.params.path,
pageInfo,
})
}
let selectedTab: number | null = null
let historyPanel: HistoryPanel
@ -76,7 +64,7 @@
<Tabs selected={selectedTab} toggable on:select={selectTab}>
<TabPanel title="History">
{#key $page.params.path}
<HistoryPanel bind:this={historyPanel} {history} fetchMoreHandler={fetchMoreHistory} />
<HistoryPanel bind:this={historyPanel} {history} fetchMoreHandler={fetchCommitHistory} />
{/key}
</TabPanel>
</Tabs>

View File

@ -1,5 +1,3 @@
#import './FileDiffHunks.gql'
fragment FileDiff_Diff on FileDiff {
newPath
oldPath

View File

@ -4,7 +4,7 @@
import type { FileDiffHunks_Hunk } from './FileDiffHunks.gql'
export let hunks: FileDiffHunks_Hunk[]
export let hunks: readonly FileDiffHunks_Hunk[]
</script>
{#if hunks.length === 0}

View File

@ -7,8 +7,8 @@
import Timestamp from '$lib/Timestamp.svelte'
import type { TreeEntryFields } from './api/tree'
export let entries: TreeEntryFields[]
export let commitInfo: TreeEntryWithCommitInfo[]
export let entries: readonly TreeEntryFields[]
export let commitInfo: readonly TreeEntryWithCommitInfo[]
export let revision: string
$: commitInfoByPath = new Map(commitInfo.map(entry => [entry.canonicalURL, entry]))

View File

@ -0,0 +1,20 @@
fragment GitReference_Ref on GitRef {
id
url
displayName
target {
commit {
id
author {
date
person {
displayName
}
}
behindAhead(revspec: $revspec) @include(if: $withBehindAhead) {
ahead
behind
}
}
}
}

View File

@ -1,9 +1,9 @@
<script lang="ts">
import Timestamp from '$lib/Timestamp.svelte'
import { numberWithCommas } from '$lib/common'
import type { GitRefFields } from '$lib/graphql-operations'
import type { GitReference_Ref } from './GitReference.gql'
export let ref: GitRefFields
export let ref: GitReference_Ref
$: authorName = ref.target.commit?.author.person.displayName ?? ''
$: authorDate = ref.target.commit ? new Date(ref.target.commit.author.date) : null

View File

@ -0,0 +1,19 @@
fragment HistoryPanel_HistoryConnection on GitCommitConnection {
nodes {
id
abbreviatedOID
subject
author {
date
person {
displayName
...Avatar_Person
}
}
canonicalURL
}
pageInfo {
hasNextPage
endCursor
}
}

View File

@ -1,5 +1,5 @@
<script lang="ts" context="module">
import { createHistoryResults } from '$testdata'
import { createHistoryResults } from '$testing/testdata'
import { Story } from '@storybook/addon-svelte-csf'
import HistoryPanel from './HistoryPanel.svelte'
export const meta = {

View File

@ -1,6 +1,6 @@
<script lang="ts" context="module">
export interface Capture {
history: HistoryResult | null
history: HistoryPanel_HistoryConnection | null
scroller?: ScrollerCapture
}
</script>
@ -11,17 +11,17 @@
import { page } from '$app/stores'
import { scrollIntoView } from '$lib/actions'
import type { HistoryResult } from '$lib/graphql-operations'
import Icon from '$lib/Icon.svelte'
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import { createHistoryPanelStore } from '$lib/repo/stores'
import Scroller, { type Capture as ScrollerCapture } from '$lib/Scroller.svelte'
import Tooltip from '$lib/Tooltip.svelte'
import UserAvatar from '$lib/UserAvatar.svelte'
import Avatar from '$lib/Avatar.svelte'
import Timestamp from '$lib/Timestamp.svelte'
import type { HistoryPanel_HistoryConnection } from './HistoryPanel.gql'
export let history: Promise<HistoryResult>
export let fetchMoreHandler: (pageInfo: HistoryResult['pageInfo']) => Promise<HistoryResult>
export let history: Promise<HistoryPanel_HistoryConnection | null>
export let fetchMoreHandler: (afterCursor: string | null) => Promise<HistoryPanel_HistoryConnection | null>
export function capture(): Capture {
return {
@ -67,6 +67,7 @@
$: selectedRev = $page.url?.searchParams.get('rev')
$: clearURL = getClearURL()
$: canShowInlineDiff = $page.route.id?.includes('/blob/')
let scroller: Scroller
</script>
@ -77,11 +78,11 @@
{@const selected = commit.abbreviatedOID === selectedRev}
<tr class:selected use:scrollIntoView={selected}>
<td>
<UserAvatar user={commit.author.person} />&nbsp;
<Avatar avatar={commit.author.person} />&nbsp;
{commit.author.person.displayName}
</td>
<td class="subject">
{#if $page.params?.path}
{#if canShowInlineDiff}
<a href="?rev={commit.abbreviatedOID}">{commit.subject}</a>
{:else}
{commit.subject}

View File

@ -2,17 +2,13 @@
import { mdiLink } from '@mdi/js'
import { page } from '$app/stores'
import { isErrorLike, type ErrorLike } from '$lib/common'
import Icon from '$lib/Icon.svelte'
import Tooltip from '$lib/Tooltip.svelte'
import { replaceRevisionInURL } from '$lib/web'
import type { ResolvedRevision } from '$lib/repo/api/repo'
export let resolvedRevision: ResolvedRevision | ErrorLike
export let commitID: string
$: href = !isErrorLike(resolvedRevision)
? replaceRevisionInURL($page.url.toString(), resolvedRevision.commitID)
: ''
$: href = commitID ? replaceRevisionInURL($page.url.toString(), commitID) : ''
</script>
{#if href}

View File

@ -0,0 +1,4 @@
fragment Readme_Blob on GitBlob {
richHTML
content
}

View File

@ -0,0 +1,25 @@
<script lang="ts">
import type { Readme_Blob } from './Readme.gql'
export let file: Readme_Blob
</script>
<div>
{#if file.richHTML}
{@html file.richHTML}
{:else if file.content}
<pre>{file.content}</pre>
{/if}
</div>
<style lang="scss">
div {
:global(img) {
max-width: 100%;
}
pre {
white-space: pre-wrap;
}
}
</style>

View File

@ -1,104 +0,0 @@
import { query, gql, type NodeFromResult } from '$lib/graphql'
import type { BlobResult, BlobVariables, HighlightResult, HighlightVariables, Scalars } from '$lib/graphql-operations'
interface FetchBlobOptions {
repoID: Scalars['ID']['input']
commitID: string
filePath: string
disableTimeout?: boolean
}
/**
* Makes sure that default values are applied consistently for the cache key and the `fetchBlob` function.
*/
function applyDefaultValuesToFetchBlobOptions({
disableTimeout = false,
...options
}: FetchBlobOptions): Required<FetchBlobOptions> {
return {
...options,
disableTimeout,
}
}
type Highlight = NonNullable<
NonNullable<NodeFromResult<HighlightResult['node'], 'Repository'>['commit']>['blob']
>['highlight']
export async function fetchHighlight(options: FetchBlobOptions): Promise<Highlight | null> {
const { repoID, commitID, filePath, disableTimeout } = applyDefaultValuesToFetchBlobOptions(options)
const data = await query<HighlightResult, HighlightVariables>(
gql`
query Highlight($repoID: ID!, $commitID: String!, $filePath: String!, $disableTimeout: Boolean!) {
node(id: $repoID) {
__typename
id
... on Repository {
commit(rev: $commitID) {
id
blob(path: $filePath) {
canonicalURL
highlight(disableTimeout: $disableTimeout, format: JSON_SCIP) {
aborted
lsif
}
}
}
}
}
}
`,
{ repoID, commitID, filePath, disableTimeout }
)
if (data.node?.__typename !== 'Repository' || !data.node?.commit) {
throw new Error('Commit not found')
}
return data.node.commit.blob?.highlight ?? null
}
export type BlobFileFields = NonNullable<
NonNullable<NodeFromResult<BlobResult['node'], 'Repository'>['commit']>['blob']
>
export async function fetchBlobPlaintext(options: FetchBlobOptions): Promise<BlobFileFields | null> {
const { repoID, commitID, filePath } = applyDefaultValuesToFetchBlobOptions(options)
const data = await query<BlobResult, BlobVariables>(
gql`
query Blob($repoID: ID!, $commitID: String!, $filePath: String!) {
node(id: $repoID) {
__typename
id
... on Repository {
commit(rev: $commitID) {
id
blob(path: $filePath) {
canonicalURL
content
richHTML
lfs {
byteSize
}
externalURLs {
url
serviceKind
}
languages
}
}
}
}
}
`,
{ repoID, commitID, filePath }
)
if (data.node?.__typename !== 'Repository' || !data.node?.commit?.blob) {
throw new Error('Commit or file not found')
}
return data.node.commit.blob
}

View File

@ -1,236 +0,0 @@
import { query, gql } from '$lib/graphql'
import type {
RepositoryCommitResult,
Scalars,
HistoryResult,
GitHistoryResult,
GitHistoryVariables,
CommitDiffResult,
CommitDiffVariables,
FileDiffFields,
RepositoryCommitVariables,
} from '$lib/graphql-operations'
// Unfortunately it doesn't seem possible to share fragements across package
// boundaries
const gitCommitFragment = gql`
fragment GitCommitFields on GitCommit {
id
oid
abbreviatedOID
subject
body
author {
...SignatureFields
}
committer {
...SignatureFields
}
parents {
oid
abbreviatedOID
canonicalURL
}
canonicalURL
externalURLs {
...ExternalLinkFields
}
}
fragment SignatureFields on Signature {
person {
avatarURL
name
displayName
user {
id
username
url
displayName
}
}
date
}
fragment ExternalLinkFields on ExternalLink {
url
serviceKind
}
`
const fileDiffHunkFields = gql`
fragment FileDiffHunkFields on FileDiffHunk {
oldNoNewlineAt
oldRange {
startLine
lines
}
newRange {
startLine
lines
}
section
highlight(disableTimeout: false) {
aborted
lines {
kind
html
}
}
}
`
const fileDiffFields = gql`
fragment FileDiffFields on FileDiff {
oldPath
oldFile {
__typename
canonicalURL
binary
byteSize
}
newPath
newFile {
__typename
canonicalURL
binary
byteSize
}
mostRelevantFile {
__typename
canonicalURL
url
}
hunks {
...FileDiffHunkFields
}
stat {
added
deleted
}
internalID
}
${fileDiffHunkFields}
`
const HISTORY_COMMITS_PER_PAGE = 20
const HISTORY_QUERY = gql`
query GitHistory($repo: ID!, $revspec: String!, $first: Int, $afterCursor: String, $filePath: String) {
node(id: $repo) {
__typename
id
... on Repository {
commit(rev: $revspec) {
id
ancestors(first: $first, path: $filePath, afterCursor: $afterCursor) {
...HistoryResult
}
}
}
}
}
fragment HistoryResult on GitCommitConnection {
nodes {
...GitCommitFields
}
pageInfo {
hasNextPage
endCursor
}
}
${gitCommitFragment}
`
const COMMIT_QUERY = gql`
query RepositoryCommit($repo: ID!, $revspec: String!) {
node(id: $repo) {
__typename
id
... on Repository {
id
commit(rev: $revspec) {
__typename # Necessary for error handling to check if commit exists
id
...GitCommitFields
}
}
}
}
${gitCommitFragment}
`
interface FetchRepoCommitsArgs {
repoID: Scalars['ID']['input']
revision: string
filePath?: string
first?: number
pageInfo?: HistoryResult['pageInfo']
}
export async function fetchRepoCommits({
repoID,
revision,
filePath,
first = HISTORY_COMMITS_PER_PAGE,
pageInfo,
}: FetchRepoCommitsArgs): Promise<HistoryResult> {
const emptyResult: HistoryResult = { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } }
const data = await query<GitHistoryResult, GitHistoryVariables>(HISTORY_QUERY, {
repo: repoID,
revspec: revision,
filePath: filePath ?? null,
first,
afterCursor: pageInfo?.endCursor ?? null,
})
if (data.node?.__typename === 'Repository') {
return data.node.commit?.ancestors ?? emptyResult
}
return emptyResult
}
export async function fetchRepoCommit(repoId: string, revision: string): Promise<RepositoryCommitResult> {
return query<RepositoryCommitResult, RepositoryCommitVariables>(COMMIT_QUERY, {
repo: repoId,
revspec: revision,
})
}
export async function fetchDiff(
repoID: Scalars['ID']['input'],
revspec: string,
paths: string[] = []
): Promise<FileDiffFields[]> {
const data = await query<CommitDiffResult, CommitDiffVariables>(
gql`
query CommitDiff($repoID: ID!, $revspec: String!, $paths: [String!], $first: Int) {
node(id: $repoID) {
... on Repository {
__typename
id
commit(rev: $revspec) {
id
diff {
fileDiffs(paths: $paths, first: $first) {
nodes {
...FileDiffFields
}
}
}
}
}
}
}
${fileDiffFields}
`,
{ repoID, revspec, paths, first: paths.length }
)
if (data.node?.__typename !== 'Repository') {
return []
}
return data.node.commit?.diff.fileDiffs.nodes ?? []
}

View File

@ -1,105 +0,0 @@
import { gql, query } from '$lib/graphql'
import type {
PagedRepositoryContributorConnectionFields,
PagedRepositoryContributorsResult,
PagedRepositoryContributorsVariables,
} from '$lib/graphql-operations'
const CONTRIBUTORS_QUERY = gql`
query PagedRepositoryContributors(
$repo: ID!
$first: Int
$last: Int
$after: String
$before: String
$revisionRange: String
$afterDate: String
$path: String
) {
node(id: $repo) {
__typename
id
... on Repository {
contributors(
first: $first
last: $last
before: $before
after: $after
revisionRange: $revisionRange
afterDate: $afterDate
path: $path
) {
...PagedRepositoryContributorConnectionFields
}
}
}
}
fragment PagedRepositoryContributorConnectionFields on RepositoryContributorConnection {
totalCount
pageInfo {
hasNextPage
hasPreviousPage
endCursor
startCursor
}
nodes {
...PagedRepositoryContributorNodeFields
}
}
fragment PagedRepositoryContributorNodeFields on RepositoryContributor {
__typename
person {
name
displayName
email
avatarURL
user {
username
url
displayName
avatarURL
}
}
count
commits(first: 1) {
nodes {
id
oid
abbreviatedOID
canonicalURL
subject
author {
date
}
}
}
}
`
const emptyPage: Extract<PagedRepositoryContributorsResult['node'], { __typename: 'Repository' }>['contributors'] = {
totalCount: 0,
nodes: [] as any[],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
endCursor: null,
startCursor: null,
},
}
export async function fetchContributors(
options: PagedRepositoryContributorsVariables
): Promise<PagedRepositoryContributorConnectionFields> {
const data = await query<PagedRepositoryContributorsResult, PagedRepositoryContributorsVariables>(
CONTRIBUTORS_QUERY,
options
)
if (data?.node?.__typename === 'Repository') {
return data.node.contributors
}
return emptyPage
}

View File

@ -1,154 +0,0 @@
import { gql, query } from '$lib/graphql'
import {
GitRefType,
type GitRefConnectionFields,
type GitRefFields,
type RepositoryGitBranchesOverviewResult,
type RepositoryGitBranchesOverviewVariables,
type RepositoryGitRefsResult,
type RepositoryGitRefsVariables,
type Scalars,
} from '$lib/graphql-operations'
export const gitReferenceFragments = gql`
fragment GitRefFields on GitRef {
__typename
id
displayName
name
abbrevName
url
target {
commit {
id
author {
...SignatureFieldsForReferences
}
committer {
...SignatureFieldsForReferences
}
behindAhead(revspec: "HEAD") @include(if: $withBehindAhead) {
behind
ahead
}
}
}
}
fragment SignatureFieldsForReferences on Signature {
__typename
person {
displayName
user {
username
}
}
date
}
`
export const REPOSITORY_GIT_REFS = gql`
query RepositoryGitRefs($repo: ID!, $first: Int, $query: String, $type: GitRefType!, $withBehindAhead: Boolean!) {
node(id: $repo) {
id
... on Repository {
gitRefs(first: $first, query: $query, type: $type, orderBy: AUTHORED_OR_COMMITTED_AT) {
__typename
...GitRefConnectionFields
}
}
}
}
fragment GitRefConnectionFields on GitRefConnection {
nodes {
__typename
...GitRefFields
}
totalCount
pageInfo {
hasNextPage
}
}
${gitReferenceFragments}
`
export async function queryGitReferences(args: {
repo: Scalars['ID']['input']
first?: number
query?: string
type: GitRefType
withBehindAhead?: boolean
}): Promise<GitRefConnectionFields> {
const data = await query<RepositoryGitRefsResult, RepositoryGitRefsVariables>(REPOSITORY_GIT_REFS, {
query: args.query ?? null,
first: args.first ?? null,
repo: args.repo,
type: args.type,
withBehindAhead:
args.withBehindAhead !== undefined ? args.withBehindAhead : args.type === GitRefType.GIT_BRANCH,
})
if (data?.node?.__typename !== 'Repository' || !data.node.gitRefs) {
throw new Error('Unable to fetch git information')
}
return data.node.gitRefs
}
export interface GitBranchesOverview {
defaultBranch: GitRefFields | null
activeBranches: GitRefFields[]
hasMoreActiveBranches: boolean
}
export async function queryGitBranchesOverview(args: {
repo: Scalars['ID']['input']
first: number
}): Promise<GitBranchesOverview> {
const data = await query<RepositoryGitBranchesOverviewResult, RepositoryGitBranchesOverviewVariables>(
gql`
query RepositoryGitBranchesOverview($repo: ID!, $first: Int!, $withBehindAhead: Boolean!) {
node(id: $repo) {
id
...RepositoryGitBranchesOverviewRepository
}
}
fragment RepositoryGitBranchesOverviewRepository on Repository {
__typename
defaultBranch {
...GitRefFields
}
gitRefs(first: $first, type: GIT_BRANCH, orderBy: AUTHORED_OR_COMMITTED_AT) {
nodes {
...GitRefFields
}
pageInfo {
hasNextPage
}
}
}
${gitReferenceFragments}
`,
{ ...args, withBehindAhead: true }
)
if (!data?.node) {
throw new Error('Unable to fetch repository information')
}
const repo = data.node
if (repo.__typename !== 'Repository') {
throw new Error('Not a GitRef')
}
if (!repo.gitRefs.nodes) {
throw new Error('Unable to fetch repository information')
}
return {
defaultBranch: repo.defaultBranch,
activeBranches: repo.gitRefs.nodes.filter(
// Filter out default branch from activeBranches.
({ id }) => !repo.defaultBranch || repo.defaultBranch.id !== id
),
hasMoreActiveBranches: repo.gitRefs.pageInfo.hasNextPage,
}
}

View File

@ -1,141 +0,0 @@
import { query, gql, fromCache } from '$lib/graphql'
import type { ResolveRepoRevisonResult, ResolveRepoRevisonVariables } from '$lib/graphql-operations'
import {
CloneInProgressError,
RepoNotFoundError,
RepoSeeOtherError,
RevisionNotFoundError,
type RepoSpec,
type RevisionSpec,
type ResolvedRevisionSpec,
} from '$lib/shared'
const resolveRevisionQuery = gql`
query ResolveRepoRevison($repoName: String!, $revision: String!) {
repositoryRedirect(name: $repoName) {
__typename
... on Repository {
id
name
url
sourceType
externalURLs {
url
serviceKind
}
externalRepository {
serviceType
serviceID
}
description
viewerCanAdminister
defaultBranch {
displayName
abbrevName
}
isFork
metadata {
key
value
}
mirrorInfo {
cloneInProgress
cloneProgress
cloned
}
commit(rev: $revision) {
id
oid
tree(path: ".") {
canonicalURL
url
}
}
changelist(cid: $revision) {
cid
canonicalURL
commit {
id
__typename
oid
tree(path: ".") {
canonicalURL
url
}
}
}
defaultBranch {
id
abbrevName
}
}
... on Redirect {
url
}
}
}
`
export interface ResolvedRevision extends ResolvedRevisionSpec {
defaultBranch: string
repo: Extract<ResolveRepoRevisonResult['repositoryRedirect'], { __typename: 'Repository' }>
}
/**
* When `revision` is undefined, the default branch is resolved.
* @returns Promise that emits the commit ID. Errors with a `CloneInProgressError` if the repo is still being cloned.
*/
export async function resolveRepoRevision({
repoName,
revision = '',
}: RepoSpec & Partial<RevisionSpec>): Promise<ResolvedRevision> {
let data = await fromCache<ResolveRepoRevisonResult, ResolveRepoRevisonVariables>(resolveRevisionQuery, {
repoName,
revision,
})
if (
!data ||
(data.repositoryRedirect?.__typename === 'Repository' && data.repositoryRedirect.commit?.oid !== revision)
) {
// We always refetch data when 'revision' is a "symbolic" revision (e.g. a tag or branch name)
data = await query<ResolveRepoRevisonResult, ResolveRepoRevisonVariables>(
resolveRevisionQuery,
{
repoName,
revision,
},
{ fetchPolicy: 'network-only' }
)
}
if (!data.repositoryRedirect) {
throw new RepoNotFoundError(repoName)
}
if (data.repositoryRedirect.__typename === 'Redirect') {
throw new RepoSeeOtherError(data.repositoryRedirect.url)
}
if (data.repositoryRedirect.mirrorInfo.cloneInProgress) {
throw new CloneInProgressError(repoName, data.repositoryRedirect.mirrorInfo.cloneProgress || undefined)
}
if (!data.repositoryRedirect.mirrorInfo.cloned) {
throw new CloneInProgressError(repoName, 'queued for cloning')
}
// The "revision" we queried for could be a commit or a changelist.
const commit = data.repositoryRedirect.commit || data.repositoryRedirect.changelist?.commit
if (!commit) {
throw new RevisionNotFoundError(revision)
}
const defaultBranch = data.repositoryRedirect.defaultBranch?.abbrevName || 'HEAD'
if (!commit.tree) {
throw new RevisionNotFoundError(defaultBranch)
}
return {
repo: data.repositoryRedirect,
commitID: commit.oid,
defaultBranch,
}
}

View File

@ -16,7 +16,7 @@ import {
type DocumentInfo,
} from '$lib/web'
import type { BlobFileFields } from './api/blob'
import type { BlobPage_Blob } from '../../routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/page.gql'
/**
* The minimum number of milliseconds that must elapse before we handle a "Go to
@ -140,12 +140,12 @@ export function openImplementations(
}
interface CombinedBlobData {
blob: BlobFileFields | null
blob: BlobPage_Blob | null
highlights: string | undefined
}
interface BlobDataHandler {
set(blob: Promise<BlobFileFields | null>, highlight: Promise<string | undefined>): void
set(blob: Promise<BlobPage_Blob | null>, highlight: Promise<string | undefined>): void
combinedBlobData: Readable<CombinedBlobData>
loading: Readable<boolean>
}
@ -160,7 +160,7 @@ export function createBlobDataHandler(): BlobDataHandler {
let subscription: Subscription | undefined
return {
set(blob: Promise<BlobFileFields | null>, highlight: Promise<string | undefined>): void {
set(blob: Promise<BlobPage_Blob | null>, highlight: Promise<string | undefined>): void {
subscription?.unsubscribe()
loading.set(true)
subscription = from(blob)

View File

@ -1,13 +1,12 @@
import { get } from 'svelte/store'
import { expect, describe, it, vi, beforeAll, afterAll } from 'vitest'
import type { HistoryResult } from '$lib/graphql-operations'
import { createHistoryResults } from '$testdata'
import { createHistoryResults } from '$testing/testdata'
import { createHistoryPanelStore } from './stores'
describe('createHistoryPanelStore', () => {
const historyResults: HistoryResult[] = createHistoryResults(2, 2)
const historyResults = createHistoryResults(2, 2)
beforeAll(() => {
vi.useFakeTimers()

View File

@ -1,9 +1,10 @@
import { memoize } from 'lodash'
import { writable, type Readable, type Writable } from 'svelte/store'
import type { HistoryResult } from '$lib/graphql-operations'
import { createEmptySingleSelectTreeState, type TreeState } from '$lib/TreeView'
import type { HistoryPanel_HistoryConnection } from './HistoryPanel.gql'
export const sidebarOpen = writable(true)
/**
@ -17,35 +18,39 @@ export const getSidebarFileTreeStateForRepo = memoize(
interface HistoryPanelStoreValue {
loading: boolean
history?: HistoryResult
history?: HistoryPanel_HistoryConnection
error?: Error
}
interface HistoryPanelStore extends Readable<HistoryPanelStoreValue> {
capture(): HistoryResult | null
restore(result: HistoryResult | null): void
loadMore(fetch: (pageInfo: HistoryResult['pageInfo']) => Promise<HistoryResult>): void
capture(): HistoryPanel_HistoryConnection | null
restore(result: HistoryPanel_HistoryConnection | null): void
loadMore(fetch: (afterCursor: string | null) => Promise<HistoryPanel_HistoryConnection | null>): void
}
/**
* Creates a store for properly handling history panel state. Having this logic in a separate
* store makes it easier to handle promises.
*/
export function createHistoryPanelStore(initialHistory: Promise<HistoryResult>): HistoryPanelStore {
export function createHistoryPanelStore(
initialHistory: Promise<HistoryPanel_HistoryConnection | null>
): HistoryPanelStore {
let loading = true
let history: HistoryResult | null = null
let currentPromise: Promise<HistoryResult> | null = initialHistory
let history: HistoryPanel_HistoryConnection | null = null
let currentPromise: Promise<HistoryPanel_HistoryConnection | null> | null = initialHistory
const { subscribe, set, update } = writable<HistoryPanelStoreValue>({ loading })
function processPromise(promise: Promise<HistoryResult>) {
function processPromise(promise: Promise<HistoryPanel_HistoryConnection | null>): void {
currentPromise = promise
loading = true
update(state => ({ ...state, loading, error: undefined }))
promise.then(result => {
if (promise === currentPromise) {
// Don't update data when promise is "stale"
history = { pageInfo: result.pageInfo, nodes: [...(history?.nodes ?? []), ...result.nodes] }
history = result
? { pageInfo: result.pageInfo, nodes: [...(history?.nodes ?? []), ...result.nodes] }
: { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [] }
loading = false
set({ history, loading })
}
@ -70,7 +75,7 @@ export function createHistoryPanelStore(initialHistory: Promise<HistoryResult>):
if (loading || !history || !history.pageInfo.hasNextPage) {
return
}
processPromise(fetch(history.pageInfo))
processPromise(fetch(history.pageInfo.endCursor))
},
capture() {
return history

View File

@ -1,6 +1,6 @@
import { resolvePath } from '@sveltejs/kit'
import type { ResolvedRevision } from '$lib/repo/api/repo'
import type { ResolvedRevision } from '../../routes/[...repo=reporev]/+layout'
const TREE_ROUTE_ID = '/[...repo=reporev]/(validrev)/(code)/-/tree/[...path]'

View File

@ -0,0 +1,14 @@
# Needed for search context suggestions for the current user
fragment SearchInput_AuthenticatedUser on User {
id
organizations {
nodes {
__typename
id
name
displayName
url
settingsURL
}
}
}

View File

@ -2,7 +2,7 @@ import { writable, type Readable } from 'svelte/store'
import { goto } from '$app/navigation'
import { SearchPatternType } from '$lib/graphql-operations'
import { buildSearchURLQuery, type SettingsCascade } from '$lib/shared'
import { buildSearchURLQuery, type Settings } from '$lib/shared'
import { defaultSearchModeFromSettings } from '$lib/web'
// Defined in @sourcegraph/shared/src/search/searchQueryState.tsx
@ -23,7 +23,7 @@ interface Options {
}
type QuerySettings = Pick<
SettingsCascade['final'],
Settings,
'search.defaultCaseSensitive' | 'search.defaultPatternType' | 'search.defaultMode'
> | null
export type QueryOptions = Pick<Options, 'patternType' | 'caseSensitive' | 'searchMode' | 'searchContext'>

View File

@ -7,9 +7,6 @@ export {
parseQueryAndHash,
buildSearchURLQuery,
makeRepoURI,
type RevisionSpec,
type ResolvedRevisionSpec,
type RepoSpec,
} from '@sourcegraph/shared/src/util/url'
export {
isCloneInProgressErrorLike,
@ -58,18 +55,11 @@ export {
rankByLine,
truncateGroups,
} from '@sourcegraph/shared/src/components/ranking/PerFileResultRanking'
export { type AuthenticatedUser, currentAuthStateQuery } from '@sourcegraph/shared/src/auth'
export { filterExists } from '@sourcegraph/shared/src/search/query/validate'
export { FilterType } from '@sourcegraph/shared/src/search/query/filters'
export { getGlobalSearchContextFilter, findFilter, FilterKind } from '@sourcegraph/shared/src/search/query/query'
export { omitFilter, appendFilter, updateFilter } from '@sourcegraph/shared/src/search/query/transformer'
export {
type SettingsCascade,
type SettingsSubject,
type SettingsCascadeOrError,
SettingsProvider,
gqlToCascade,
} from '@sourcegraph/shared/src/settings/settings'
export { type Settings, SettingsProvider } from '@sourcegraph/shared/src/settings/settings'
export { fetchStreamSuggestions } from '@sourcegraph/shared/src/search/suggestions'
export { QueryChangeSource, type QueryState } from '@sourcegraph/shared/src/search/helpers'
export { migrateLocalStorageToTemporarySettings } from '@sourcegraph/shared/src/settings/temporary/migrateLocalStorageToTemporarySettings'

View File

@ -1,15 +1,16 @@
import { getContext } from 'svelte'
import { readable, writable, type Readable, type Writable } from 'svelte/store'
import type { SettingsCascade, AuthenticatedUser, TemporarySettingsStorage } from '$lib/shared'
import type { Settings, TemporarySettingsStorage } from '$lib/shared'
import type { AuthenticatedUser, FeatureFlag } from '../routes/layout.gql'
import type { FeatureFlag } from './featureflags'
import type { GraphQLClient } from './graphql'
export { isLightTheme } from './theme'
export interface SourcegraphContext {
settings: Readable<SettingsCascade['final'] | null>
settings: Readable<Settings | null>
user: Readable<AuthenticatedUser | null>
temporarySettingsStorage: Readable<TemporarySettingsStorage>
featureFlags: Readable<FeatureFlag[]>
@ -31,7 +32,7 @@ export const user = {
}
export const settings = {
subscribe(subscriber: (settings: SettingsCascade['final'] | null) => void) {
subscribe(subscriber: (settings: Settings | null) => void) {
const { settings } = getStores()
return settings.subscribe(subscriber)
},

View File

@ -1,17 +0,0 @@
import { viewerSettingsQuery } from '@sourcegraph/shared/src/backend/settings'
import { gql, query } from '$lib/graphql'
import type { ViewerSettingsResult } from '$lib/graphql/shared'
import { gqlToCascade } from '$lib/shared'
import type { SettingsCascadeOrError } from '$lib/shared'
export { viewerSettingsQuery }
export async function fetchUserSettings(): Promise<SettingsCascadeOrError> {
const data = await query<ViewerSettingsResult>(gql(viewerSettingsQuery))
if (!data?.viewerSettings) {
throw new Error('Unable to fetch user settings')
}
return gqlToCascade(data.viewerSettings)
}

View File

@ -1,8 +0,0 @@
import { query, gql } from '$lib/graphql'
import type { CurrentAuthStateResult } from '$lib/graphql/shared'
import { currentAuthStateQuery, type AuthenticatedUser } from '$lib/shared'
export async function fetchAuthenticatedUser(): Promise<AuthenticatedUser | null> {
const result = await query<CurrentAuthStateResult>(gql(currentAuthStateQuery))
return result.currentUser
}

View File

@ -16,13 +16,13 @@
import { beforeNavigate } from '$app/navigation'
import type { LayoutData, Snapshot } from './$types'
import { createFeatureFlagStore, fetchEvaluatedFeatureFlags } from '$lib/featureflags'
import { createFeatureFlagStore } from '$lib/featureflags'
import InfoBanner from './InfoBanner.svelte'
export let data: LayoutData
const user = writable(data.user ?? null)
const settings = writable(isErrorLike(data.settings) ? null : data.settings.final)
const settings = writable(isErrorLike(data.settings) ? null : data.settings)
// It's OK to set the temporary storage during initialization time because
// sign-in/out currently performs a full page refresh
const temporarySettingsStorage = createTemporarySettingsStorage(
@ -33,13 +33,13 @@
user,
settings,
temporarySettingsStorage,
featureFlags: createFeatureFlagStore(data.featureFlags, fetchEvaluatedFeatureFlags),
featureFlags: createFeatureFlagStore(data.featureFlags, data.fetchEvaluatedFeatureFlags),
client: readable(data.graphqlClient),
})
// Update stores when data changes
$: $user = data.user ?? null
$: $settings = isErrorLike(data.settings) ? null : data.settings.final
$: $settings = isErrorLike(data.settings) ? null : data.settings
// Set initial, user configured theme
// TODO: This should be send be server in the HTML so that we don't flash the wrong theme

View File

@ -1,10 +1,12 @@
import { error } from '@sveltejs/kit'
import { browser } from '$app/environment'
import { fetchEvaluatedFeatureFlags } from '$lib/featureflags'
import { isErrorLike, parseJSONCOrError } from '$lib/common'
import { getGraphQLClient } from '$lib/graphql'
import { fetchUserSettings } from '$lib/user/api/settings'
import { fetchAuthenticatedUser } from '$lib/user/api/user'
import type { Settings } from '$lib/shared'
import type { LayoutLoad } from './$types'
import { Init, EvaluatedFeatureFlagsQuery } from './layout.gql'
// Disable server side rendering for the whole app
export const ssr = false
@ -21,10 +23,24 @@ if (browser) {
}
}
export const load: LayoutLoad = () => ({
graphqlClient: getGraphQLClient(),
user: fetchAuthenticatedUser(),
// Initial user settings
settings: fetchUserSettings(),
featureFlags: fetchEvaluatedFeatureFlags(),
})
export const load: LayoutLoad = async () => {
const graphqlClient = await getGraphQLClient()
const result = await graphqlClient.query({ query: Init, fetchPolicy: 'no-cache' })
const settings = parseJSONCOrError<Settings>(result.data.viewerSettings.final)
if (isErrorLike(settings)) {
throw error(500, `Failed to parse user settings: ${settings.message}`)
}
return {
graphqlClient,
user: result.data.currentUser,
// Initial user settings
settings,
featureFlags: result.data.evaluatedFeatureFlags,
fetchEvaluatedFeatureFlags: async () => {
const result = await graphqlClient.query({ query: EvaluatedFeatureFlagsQuery, fetchPolicy: 'no-cache' })
return result.data.evaluatedFeatureFlags
},
}
}

View File

@ -0,0 +1,3 @@
fragment Header_User on User {
...UserMenu_User
}

View File

@ -2,7 +2,6 @@
import { mdiBookOutline, mdiChartBar, mdiMagnify } from '@mdi/js'
import { mark, svelteLogoEnabled } from '$lib/images'
import type { AuthenticatedUser } from '$lib/shared'
import HeaderNavLink from './HeaderNavLink.svelte'
import { Button } from '$lib/wildcard'
@ -12,8 +11,9 @@
import CodyIcon from '$lib/icons/Cody.svelte'
import CodeMonitoringIcon from '$lib/icons/CodeMonitoring.svelte'
import BatchChangesIcon from '$lib/icons/BatchChanges.svelte'
import type { Header_User } from './Header.gql'
export let authenticatedUser: AuthenticatedUser | null | undefined
export let authenticatedUser: Header_User | null | undefined
$: reactURL = (function (url) {
const urlCopy = new URL(url)
@ -58,7 +58,7 @@
</Tooltip>
<div>
{#if authenticatedUser}
<UserMenu {authenticatedUser} />
<UserMenu user={authenticatedUser} />
{:else}
<Button variant="secondary" outline>
<svelte:fragment slot="custom" let:buttonClass>

View File

@ -0,0 +1,14 @@
fragment UserMenu_User on User {
settingsURL
username
siteAdmin
organizations {
nodes {
name
displayName
url
settingsURL
}
}
...Avatar_User
}

View File

@ -1,7 +1,7 @@
<script lang="ts">
import Icon from '$lib/Icon.svelte'
import UserAvatar from '$lib/UserAvatar.svelte'
import type { AuthenticatedUser } from '$lib/shared'
import Avatar from '$lib/Avatar.svelte'
import type { UserMenu_User } from './UserMenu.gql'
import { humanTheme } from '$lib/theme'
import { DropdownMenu, MenuLink, MenuRadioGroup, MenuSeparator, Submenu } from '$lib/wildcard'
import { getButtonClassName } from '$lib/wildcard/Button'
@ -10,10 +10,10 @@
const MAX_VISIBLE_ORGS = 5
export let authenticatedUser: AuthenticatedUser
export let user: UserMenu_User
const open = writable(false)
$: organizations = authenticatedUser.organizations.nodes
$: organizations = user.organizations.nodes
</script>
<DropdownMenu
@ -22,13 +22,13 @@
aria-label="{$open ? 'Close' : 'Open'} user profile menu"
>
<svelte:fragment slot="trigger">
<UserAvatar user={authenticatedUser} />
<Avatar avatar={user} />
<Icon svgPath={$open ? mdiChevronUp : mdiChevronDown} aria-hidden={true} inline />
</svelte:fragment>
<h6>Signed in as <strong>@{authenticatedUser.username}</strong></h6>
<h6>Signed in as <strong>@{user.username}</strong></h6>
<MenuSeparator />
<MenuLink href={authenticatedUser.settingsURL} data-sveltekit-reload>Settings</MenuLink>
<MenuLink href="/users/{authenticatedUser.username}/searches" data-sveltekit-reload>Saved searches</MenuLink>
<MenuLink href={user.settingsURL} data-sveltekit-reload>Settings</MenuLink>
<MenuLink href="/users/{user.username}/searches" data-sveltekit-reload>Saved searches</MenuLink>
<MenuLink href="/teams" data-sveltekit-reload>Teams</MenuLink>
<MenuSeparator />
<Submenu>
@ -44,11 +44,11 @@
</MenuLink>
{/each}
{#if organizations.length > MAX_VISIBLE_ORGS}
<MenuLink href={authenticatedUser.settingsURL}>Show all organizations</MenuLink>
<MenuLink href={user.settingsURL}>Show all organizations</MenuLink>
{/if}
{/if}
<MenuSeparator />
{#if authenticatedUser.siteAdmin}
{#if user.siteAdmin}
<MenuLink href="/site-admin" data-sveltekit-reload>Site admin</MenuLink>
{/if}
<MenuLink href="/help" target="_blank" rel="noopener">

View File

@ -69,6 +69,15 @@
})
}
function fetchCommitHistory(afterCursor: string | null) {
return data.fetchCommitHistory({
repo: data.resolvedRevision.repo.id,
revspec: data.resolvedRevision.commitID,
filePath: $page.params.path ?? '',
afterCursor,
})
}
$: ({ revision, parentPath, resolvedRevision } = data)
$: commitID = resolvedRevision.commitID
$: repoID = resolvedRevision.repo.id
@ -107,7 +116,7 @@
{/if}
<div class="main">
<slot />
<BottomPanel bind:this={bottomPanel} history={data.deferred.codeCommits} />
<BottomPanel bind:this={bottomPanel} history={data.deferred.commitHistory} {fetchCommitHistory} />
</div>
</section>

View File

@ -1,10 +1,13 @@
import { dirname } from 'path'
import { browser } from '$app/environment'
import { fetchRepoCommits } from '$lib/repo/api/commits'
import type { Scalars } from '$lib/graphql-types'
import { fetchSidebarFileTree } from '$lib/repo/api/tree'
import type { LayoutLoad } from './$types'
import { GitHistoryQuery, type GitHistory_HistoryConnection } from './layout.gql'
const HISTORY_COMMITS_PER_PAGE = 20
// Signifies the path of the repository root
const REPO_ROOT = '.'
@ -24,18 +27,52 @@ if (browser) {
}
}
interface FetchCommitHistoryArgs {
repo: Scalars['ID']['input']
revspec: string
filePath: string
afterCursor: string | null
}
export const load: LayoutLoad = async ({ parent, params }) => {
const { resolvedRevision, repoName } = await parent()
const { resolvedRevision, repoName, graphqlClient } = await parent()
const parentPath = getRootPath(repoName, params.path ? dirname(params.path) : REPO_ROOT)
function fetchCommitHistory({
repo,
revspec,
filePath,
afterCursor,
}: FetchCommitHistoryArgs): Promise<GitHistory_HistoryConnection | null> {
return graphqlClient
.query({
query: GitHistoryQuery,
variables: {
repo,
revspec,
filePath,
first: HISTORY_COMMITS_PER_PAGE,
afterCursor,
},
})
.then(result => {
if (result.data.node?.__typename !== 'Repository') {
throw new Error('Expected repository')
}
return result.data.node.commit?.ancestors ?? null
})
}
return {
parentPath,
fetchCommitHistory,
deferred: {
// Fetches the most recent commits for current blob, tree or repo root
codeCommits: fetchRepoCommits({
repoID: resolvedRevision.repo.id,
revision: resolvedRevision.commitID,
filePath: params.path,
commitHistory: fetchCommitHistory({
repo: resolvedRevision.repo.id,
revspec: resolvedRevision.commitID,
filePath: params.path ?? '',
afterCursor: null,
}),
fileTree: fetchSidebarFileTree({
repoID: resolvedRevision.repo.id,

View File

@ -5,16 +5,14 @@
import SidebarToggleButton from '$lib/repo/SidebarToggleButton.svelte'
import { sidebarOpen } from '$lib/repo/stores'
import { createPromiseStore } from '$lib/utils'
import Readme from '$lib/repo/Readme.svelte'
import type { RepoPage_Readme } from './page.gql'
import type { PageData } from './$types'
export let data: PageData
const {
value: readme,
set: setReadme,
pending: readmePending,
} = createPromiseStore<PageData['deferred']['readme']>()
const { value: readme, set: setReadme, pending: readmePending } = createPromiseStore<RepoPage_Readme | null>()
$: setReadme(data.deferred.readme)
</script>
@ -31,10 +29,8 @@
{/if}
</h3>
<div class="content">
{#if $readme?.richHTML}
{@html $readme.richHTML}
{:else if $readme?.content}
<pre>{$readme.content}</pre>
{#if $readme}
<Readme file={$readme} />
{:else if !$readmePending}
{data.resolvedRevision.repo.description}
{/if}
@ -72,13 +68,5 @@
padding: 1rem;
overflow: auto;
flex: 1;
:global(img) {
max-width: 100%;
}
pre {
white-space: pre-wrap;
}
}
</style>

View File

@ -1,10 +1,10 @@
import { fetchBlobPlaintext } from '$lib/repo/api/blob'
import { findReadme } from '$lib/repo/tree'
import type { PageLoad } from './$types'
import { RepoPageReadmeQuery } from './page.gql'
export const load: PageLoad = async ({ parent }) => {
const { resolvedRevision, deferred } = await parent()
const { resolvedRevision, deferred, graphqlClient } = await parent()
return {
deferred: {
@ -14,19 +14,21 @@ export const load: PageLoad = async ({ parent }) => {
if (!readme) {
return null
}
return fetchBlobPlaintext({
repoID: resolvedRevision.repo.id,
commitID: resolvedRevision.commitID,
filePath: readme.path,
}).then(result =>
result
? {
name: readme.name,
content: result.content,
richHTML: result.richHTML,
}
: null
)
return graphqlClient
.query({
query: RepoPageReadmeQuery,
variables: {
repoID: resolvedRevision.repo.id,
revspec: resolvedRevision.commitID,
path: readme.path,
},
})
.then(result => {
if (result.data.node?.__typename !== 'Repository') {
throw new Error('Expected Repository')
}
return result.data.node.commit?.blob ?? null
})
}),
},
}

View File

@ -10,7 +10,7 @@
import FileHeader from '$lib/repo/FileHeader.svelte'
import Permalink from '$lib/repo/Permalink.svelte'
import FileDiff from '../../../../-/commit/[...revspec]/FileDiff.svelte'
import FileDiff from '$lib/repo/FileDiff.svelte'
import type { PageData } from './$types'
import FormatAction from './FormatAction.svelte'
@ -26,7 +26,8 @@
const {
revision,
resolvedRevision: { commitID, repo },
resolvedRevision: { commitID },
repoName,
filePath,
settings,
graphqlClient,
@ -67,7 +68,7 @@
{#if formatted}
<FormatAction />
{/if}
<Permalink resolvedRevision={data.resolvedRevision} />
<Permalink commitID={data.resolvedRevision.commitID} />
{/if}
</svelte:fragment>
</FileHeader>
@ -94,7 +95,7 @@
...blobData,
revision: revision ?? '',
commitID,
repoName: repo.name,
repoName: repoName,
filePath,
}}
highlights={$combinedBlobData.highlights || ''}

View File

@ -1,31 +1,62 @@
import { fetchHighlight, fetchBlobPlaintext } from '$lib/repo/api/blob'
import { fetchDiff } from '$lib/repo/api/commits'
import type { PageLoad } from './$types'
import { BlobDiffQuery, BlobPageQuery, BlobSyntaxHighlightQuery } from './page.gql'
export const load: PageLoad = async ({ parent, params, url }) => {
const revisionToCompare = url.searchParams.get('rev')
const { resolvedRevision } = await parent()
const { resolvedRevision, graphqlClient } = await parent()
return {
filePath: params.path,
deferred: {
blob: fetchBlobPlaintext({
filePath: params.path,
repoID: resolvedRevision.repo.id,
commitID: resolvedRevision.commitID,
}),
highlights: fetchHighlight({
filePath: params.path,
repoID: resolvedRevision.repo.id,
commitID: resolvedRevision.commitID,
}).then(highlight => highlight?.lsif),
blob: graphqlClient
.query({
query: BlobPageQuery,
variables: {
repoID: resolvedRevision.repo.id,
revspec: resolvedRevision.commitID,
path: params.path,
},
})
.then(result => {
if (result.data.node?.__typename !== 'Repository' || !result.data.node.commit?.blob) {
throw new Error('Commit or file not found')
}
return result.data.node.commit.blob
}),
highlights: graphqlClient
.query({
query: BlobSyntaxHighlightQuery,
variables: {
repoID: resolvedRevision.repo.id,
revspec: resolvedRevision.commitID,
path: params.path,
disableTimeout: false,
},
})
.then(result => {
if (result.data.node?.__typename !== 'Repository') {
throw new Error('Expected Repository')
}
return result.data.node.commit?.blob?.highlight.lsif
}),
compare: revisionToCompare
? {
revisionToCompare,
diff: fetchDiff(resolvedRevision.repo.id, revisionToCompare, [params.path]).then(
nodes => nodes[0]
),
diff: graphqlClient
.query({
query: BlobDiffQuery,
variables: {
repoID: resolvedRevision.repo.id,
revspec: revisionToCompare,
paths: [params.path],
},
})
.then(result => {
if (result.data.node?.__typename !== 'Repository') {
throw new Error('Expected Repository')
}
return result.data.node.commit?.diff.fileDiffs.nodes[0]
}),
}
: null,
},

View File

@ -0,0 +1,57 @@
query BlobDiffQuery($repoID: ID!, $revspec: String!, $paths: [String!]) {
node(id: $repoID) {
... on Repository {
id
commit(rev: $revspec) {
id
diff {
fileDiffs(paths: $paths) {
nodes {
...FileDiff_Diff
}
}
}
}
}
}
}
query BlobPageQuery($repoID: ID!, $revspec: String!, $path: String!) {
node(id: $repoID) {
... on Repository {
id
commit(rev: $revspec) {
id
oid
blob(path: $path) {
...BlobPage_Blob
}
}
}
}
}
fragment BlobPage_Blob on GitBlob {
canonicalURL
content
richHTML
languages
}
query BlobSyntaxHighlightQuery($repoID: ID!, $revspec: String!, $path: String!, $disableTimeout: Boolean!) {
node(id: $repoID) {
... on Repository {
id
commit(rev: $revspec) {
id
blob(path: $path) {
canonicalURL
highlight(disableTimeout: $disableTimeout) {
aborted
lsif
}
}
}
}
}
}

View File

@ -2,22 +2,20 @@
import { mdiFileDocumentOutline, mdiFolderOutline } from '@mdi/js'
import Icon from '$lib/Icon.svelte'
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import FileHeader from '$lib/repo/FileHeader.svelte'
import Permalink from '$lib/repo/Permalink.svelte'
import { createPromiseStore } from '$lib/utils'
import type { TreeWithCommitInfo } from './page.gql'
import FileDiff from '../../../../-/commit/[...revspec]/FileDiff.svelte'
import type { TreePage_TreeWithCommitInfo, TreePage_Readme } from './page.gql'
import type { PageData } from './$types'
import FileTable from '$lib/repo/FileTable.svelte'
import Readme from '$lib/repo/Readme.svelte'
export let data: PageData
const { value: tree, set: setTree } = createPromiseStore<PageData['deferred']['treeEntries']>()
const { value: commitInfo, set: setCommitInfo } = createPromiseStore<Promise<TreeWithCommitInfo | null>>()
const { value: readme, set: setReadme } = createPromiseStore<PageData['deferred']['readme']>()
const { value: commitInfo, set: setCommitInfo } = createPromiseStore<Promise<TreePage_TreeWithCommitInfo | null>>()
const { value: readme, set: setReadme } = createPromiseStore<Promise<TreePage_Readme | null>>()
$: setTree(data.deferred.treeEntries)
$: setCommitInfo(data.deferred.commitInfo)
@ -33,22 +31,12 @@
<FileHeader>
<Icon slot="icon" svgPath={mdiFolderOutline} />
<svelte:fragment slot="actions">
<Permalink resolvedRevision={data.resolvedRevision} />
<Permalink commitID={data.resolvedRevision.commitID} />
</svelte:fragment>
</FileHeader>
<div class="content">
{#if data.deferred.compare}
{#await data.deferred.compare.diff}
<LoadingSpinner />
{:then nodes}
{#each nodes as fileDiff}
<FileDiff {fileDiff} expanded={false} />
{/each}
{/await}
{:else}
<FileTable revision={data.revision ?? ''} {entries} commitInfo={entriesWithCommitInfo} />
{/if}
<FileTable revision={data.revision ?? ''} {entries} commitInfo={entriesWithCommitInfo} />
{#if $readme}
<h4 class="header">
<Icon svgPath={mdiFileDocumentOutline} />
@ -56,11 +44,7 @@
{$readme.name}
</h4>
<div class="readme">
{#if $readme.richHTML}
{@html $readme.richHTML}
{:else if $readme.content}
<pre>{$readme.content}</pre>
{/if}
<Readme file={$readme} />
</div>
{/if}
</div>
@ -83,9 +67,5 @@
.readme {
padding: 1rem;
flex: 1;
pre {
white-space: pre-wrap;
}
}
</style>

View File

@ -1,13 +1,10 @@
import { fetchBlobPlaintext } from '$lib/repo/api/blob'
import { fetchDiff } from '$lib/repo/api/commits'
import { fetchTreeEntries } from '$lib/repo/api/tree'
import { findReadme } from '$lib/repo/tree'
import type { PageLoad } from './$types'
import { TreeEntriesCommitInfo } from './page.gql'
import { TreePageCommitInfoQuery, TreePageReadmeQuery } from './page.gql'
export const load: PageLoad = async ({ params, parent, url }) => {
const revisionToCompare = url.searchParams.get('rev')
export const load: PageLoad = async ({ params, parent }) => {
const { resolvedRevision, graphqlClient } = await parent()
const treeEntries = fetchTreeEntries({
@ -26,7 +23,7 @@ export const load: PageLoad = async ({ params, parent, url }) => {
treeEntries,
commitInfo: graphqlClient
.query({
query: TreeEntriesCommitInfo,
query: TreePageCommitInfoQuery,
variables: {
repoID: resolvedRevision.repo.id,
commitID: resolvedRevision.commitID,
@ -48,21 +45,22 @@ export const load: PageLoad = async ({ params, parent, url }) => {
if (!readme) {
return null
}
return fetchBlobPlaintext({
repoID: resolvedRevision.repo.id,
commitID: resolvedRevision.commitID,
filePath: readme.path,
}).then(result => ({
name: readme.name,
...result,
}))
return graphqlClient
.query({
query: TreePageReadmeQuery,
variables: {
repoID: resolvedRevision.repo.id,
revspec: resolvedRevision.commitID,
path: readme.path,
},
})
.then(result => {
if (result.data.node?.__typename !== 'Repository') {
throw new Error('Expected Repository')
}
return result.data.node.commit?.blob ?? null
})
}),
compare: revisionToCompare
? {
revisionToCompare,
diff: fetchDiff(resolvedRevision.repo.id, revisionToCompare, [params.path]),
}
: null,
},
}
}

View File

@ -1,13 +1,11 @@
#import "$lib/repo/FileTable.gql"
fragment TreeWithCommitInfo on GitTree {
fragment TreePage_TreeWithCommitInfo on GitTree {
canonicalURL
entries(first: $first) {
...TreeEntryWithCommitInfo
}
}
query TreeEntriesCommitInfo($repoID: ID!, $commitID: String!, $filePath: String!, $first: Int) {
query TreePageCommitInfoQuery($repoID: ID!, $commitID: String!, $filePath: String!, $first: Int) {
node(id: $repoID) {
__typename
id
@ -15,9 +13,29 @@ query TreeEntriesCommitInfo($repoID: ID!, $commitID: String!, $filePath: String!
commit(rev: $commitID) {
id
tree(path: $filePath) {
...TreeWithCommitInfo
...TreePage_TreeWithCommitInfo
}
}
}
}
}
query TreePageReadmeQuery($repoID: ID!, $revspec: String!, $path: String!) {
node(id: $repoID) {
... on Repository {
id
commit(rev: $revspec) {
id
blob(path: $path) {
canonicalURL # key field
...RepoPage_Readme
}
}
}
}
}
fragment TreePage_Readme on GitBlob {
name
...Readme_Blob
}

View File

@ -0,0 +1,17 @@
query GitHistoryQuery($repo: ID!, $revspec: String!, $first: Int, $afterCursor: String, $filePath: String) {
node(id: $repo) {
... on Repository {
id
commit(rev: $revspec) {
id
ancestors(first: $first, path: $filePath, afterCursor: $afterCursor) {
...GitHistory_HistoryConnection
}
}
}
}
}
fragment GitHistory_HistoryConnection on GitCommitConnection {
...BottomPanel_HistoryConnection
}

View File

@ -0,0 +1,23 @@
fragment RepoPage_ResolvedRevision on Repository {
description
}
query RepoPageReadmeQuery($repoID: ID!, $revspec: String!, $path: String!) {
node(id: $repoID) {
... on Repository {
id
commit(rev: $revspec) {
id
blob(path: $path) {
canonicalURL # key field
...RepoPage_Readme
}
}
}
}
}
fragment RepoPage_Readme on GitBlob {
name
...Readme_Blob
}

View File

@ -2,15 +2,16 @@
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import GitReference from '$lib/repo/GitReference.svelte'
import { createPromiseStore } from '$lib/utils'
import type { GitBranchesOverview } from './page.gql'
import type { PageData } from './$types'
export let data: PageData
const { pending, value: branches, set } = createPromiseStore<PageData['deferred']['branches']>()
$: set(data.deferred.branches)
const { pending, value: branches, set } = createPromiseStore<GitBranchesOverview>()
$: set(data.deferred.overview)
$: defaultBranch = $branches?.defaultBranch
$: activeBranches = $branches?.activeBranches
$: activeBranches = $branches?.branches.nodes.filter(branch => branch.id !== defaultBranch?.id)
</script>
<svelte:head>

View File

@ -1,12 +1,25 @@
import { queryGitBranchesOverview } from '$lib/repo/api/refs'
import type { PageLoad } from './$types'
import { GitBranchesOverviewQuery } from './page.gql'
export const load: PageLoad = async ({ parent }) => {
const { resolvedRevision } = await parent()
const { resolvedRevision, graphqlClient } = await parent()
return {
deferred: {
branches: queryGitBranchesOverview({ repo: resolvedRevision.repo.id, first: 10 }),
overview: graphqlClient
.query({
query: GitBranchesOverviewQuery,
variables: {
first: 20,
repo: resolvedRevision.repo.id,
withBehindAhead: true,
},
})
.then(result => {
if (result.data.node?.__typename !== 'Repository') {
throw new Error('Expected Repository')
}
return result.data.node
}),
},
}
}

View File

@ -2,15 +2,16 @@
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import GitReference from '$lib/repo/GitReference.svelte'
import { createPromiseStore } from '$lib/utils'
import type { GitBranchesConnection } from './page.gql'
import type { PageData } from './$types'
export let data: PageData
const { pending, value: branches, set } = createPromiseStore<PageData['deferred']['branches']>()
const { pending, value: connection, set } = createPromiseStore<GitBranchesConnection>()
$: set(data.deferred.branches)
$: nodes = $branches?.nodes
$: total = $branches?.totalCount
$: nodes = $connection?.nodes
$: totalCount = $connection?.totalCount
</script>
<svelte:head>
@ -29,8 +30,8 @@
{/each}
</tbody>
</table>
{#if total !== null}
<small class="text-muted">{total} branches total</small>
{#if totalCount !== null}
<small class="text-muted">{totalCount} branches total</small>
{/if}
{/if}

View File

@ -1,17 +1,25 @@
import { GitRefType } from '$lib/graphql-operations'
import { queryGitReferences } from '$lib/repo/api/refs'
import type { PageLoad } from './$types'
import { GitBranchesQuery } from './page.gql'
export const load: PageLoad = async ({ parent }) => {
const { resolvedRevision } = await parent()
const { resolvedRevision, graphqlClient } = await parent()
return {
deferred: {
branches: queryGitReferences({
repo: resolvedRevision.repo.id,
type: GitRefType.GIT_BRANCH,
first: 20,
}),
branches: graphqlClient
.query({
query: GitBranchesQuery,
variables: {
repo: resolvedRevision.repo.id,
first: 20,
withBehindAhead: true,
},
})
.then(result => {
if (result.data.node?.__typename !== 'Repository') {
throw new Error('Expected Repository')
}
return result.data.node.branches
}),
},
}
}

View File

@ -0,0 +1,18 @@
query GitBranchesQuery($repo: ID!, $first: Int!, $withBehindAhead: Boolean!, $revspec: String = "") {
node(id: $repo) {
id
... on Repository {
branches(first: $first, orderBy: AUTHORED_OR_COMMITTED_AT) {
...GitBranchesConnection
}
}
}
}
fragment GitBranchesConnection on GitRefConnection {
nodes {
id
...GitReference_Ref
}
totalCount
}

View File

@ -0,0 +1,18 @@
query GitBranchesOverviewQuery($repo: ID!, $first: Int!, $withBehindAhead: Boolean!, $revspec: String = "") {
node(id: $repo) {
id
...GitBranchesOverview
}
}
fragment GitBranchesOverview on Repository {
defaultBranch {
...GitReference_Ref
}
branches(first: $first, orderBy: AUTHORED_OR_COMMITTED_AT) {
nodes {
id
...GitReference_Ref
}
}
}

View File

@ -4,7 +4,7 @@
import { createPromiseStore } from '$lib/utils'
import type { PageData } from './$types'
import FileDiff from './FileDiff.svelte'
import FileDiff from '$lib/repo/FileDiff.svelte'
type Deferred = PageData['deferred']

View File

@ -1,6 +1,3 @@
#import '$lib/Commit.gql'
#import './FileDiff.gql'
query CommitQuery($repo: ID!, $revspec: String!) {
node(id: $repo) {
... on Repository {

View File

@ -1,5 +1,3 @@
#import "$lib/Commit.gql"
fragment Commits on GitCommitConnection {
nodes {
...Commit

View File

@ -4,9 +4,10 @@
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import Paginator from '$lib/Paginator.svelte'
import Timestamp from '$lib/Timestamp.svelte'
import UserAvatar from '$lib/UserAvatar.svelte'
import Avatar from '$lib/Avatar.svelte'
import { createPromiseStore } from '$lib/utils'
import { Button, ButtonGroup } from '$lib/wildcard'
import type { ContributorConnection } from './page.gql'
import type { PageData } from './$types'
@ -19,15 +20,15 @@
['All time', ''],
]
const { pending, latestValue: contributors, set } = createPromiseStore<PageData['deferred']['contributors']>()
const { pending, latestValue: contributorConnection, set } = createPromiseStore<ContributorConnection | null>()
$: set(data.deferred.contributors)
// We want to show stale contributors data when the user navigates to
// the next or previous page for the current time period. When the user
// changes the time period we want to show a loading indicator instead.
let currentContributors = $contributors
$: if (!$pending && $contributors) {
currentContributors = $contributors
let currentContributorConnection = $contributorConnection
$: if (!$pending && $contributorConnection) {
currentContributorConnection = $contributorConnection
}
$: timePeriod = data.after
@ -38,7 +39,7 @@
const newURL = new URL($page.url)
newURL.search = timePeriod ? `after=${timePeriod}` : ''
// Don't show stale contributors when switching the time period
currentContributors = null
currentContributorConnection = null
await goto(newURL)
}
</script>
@ -67,19 +68,19 @@
{/each}
</ButtonGroup>
</form>
{#if !currentContributors && $pending}
{#if !currentContributorConnection && $pending}
<div class="mt-3">
<LoadingSpinner />
</div>
{:else if currentContributors}
{@const nodes = currentContributors.nodes}
{:else if currentContributorConnection}
{@const nodes = currentContributorConnection.nodes}
<table class="mt-3">
<tbody>
{#each nodes as contributor}
{@const commit = contributor.commits.nodes[0]}
<tr>
<td
><span><UserAvatar user={contributor.person} /></span>&nbsp;<span
><span><Avatar avatar={contributor.person} /></span>&nbsp;<span
>{contributor.person.displayName}</span
></td
>
@ -93,9 +94,9 @@
</tbody>
</table>
<div class="d-flex flex-column align-items-center">
<Paginator disabled={$pending} pageInfo={currentContributors.pageInfo} />
<Paginator disabled={$pending} pageInfo={currentContributorConnection.pageInfo} />
<p class="mt-1 text-muted">
<small>Total contributors: {currentContributors.totalCount}</small>
<small>Total contributors: {currentContributorConnection.totalCount}</small>
</p>
</div>
{/if}

View File

@ -1,25 +1,35 @@
import { getPaginationParams } from '$lib/Paginator'
import { fetchContributors } from '$lib/repo/api/contributors'
import type { PageLoad } from './$types'
import { PagedRepositoryContributors } from './page.gql'
const pageSize = 20
export const load: PageLoad = async ({ url, parent }) => {
const afterDate = url.searchParams.get('after') ?? ''
const { first, last, before, after } = getPaginationParams(url.searchParams, pageSize)
const { resolvedRevision } = await parent()
const { resolvedRevision, graphqlClient } = await parent()
const contributors = fetchContributors({
afterDate,
repo: resolvedRevision.repo.id,
revisionRange: '',
path: '',
first,
last,
after,
before,
})
const contributors = graphqlClient
.query({
query: PagedRepositoryContributors,
variables: {
afterDate,
repo: resolvedRevision.repo.id,
revisionRange: '',
path: '',
first,
last,
after,
before,
},
})
.then(result => {
if (result.data.node?.__typename !== 'Repository') {
return null
}
return result.data.node.contributors
})
return {
after: afterDate,
deferred: {

View File

@ -0,0 +1,55 @@
fragment ContributorConnection on RepositoryContributorConnection {
nodes {
count
person {
...Avatar_Person
}
commits(first: 1) {
# Fetch information about the last commit the user made
nodes {
id
author {
date
}
canonicalURL
subject
}
}
}
pageInfo {
endCursor
startCursor
hasNextPage
hasPreviousPage
}
totalCount
}
query PagedRepositoryContributors(
$repo: ID!
$first: Int
$last: Int
$after: String
$before: String
$revisionRange: String
$afterDate: String
$path: String
) {
node(id: $repo) {
__typename
... on Repository {
id
contributors(
first: $first
last: $last
before: $before
after: $after
revisionRange: $revisionRange
afterDate: $afterDate
path: $path
) {
...ContributorConnection
}
}
}
}

View File

@ -2,16 +2,17 @@
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import GitReference from '$lib/repo/GitReference.svelte'
import { createPromiseStore } from '$lib/utils'
import type { GitTagsConnection } from './page.gql'
import type { PageData } from './$types'
export let data: PageData
const { pending, value: tags, set } = createPromiseStore<PageData['deferred']['tags']>()
const { pending, value: connection, set } = createPromiseStore<GitTagsConnection>()
$: set(data.deferred.tags)
$: nodes = $tags?.nodes
$: total = $tags?.totalCount
$: nodes = $connection?.nodes
$: total = $connection?.totalCount
</script>
<svelte:head>

View File

@ -1,18 +1,26 @@
import { GitRefType } from '$lib/graphql-operations'
import { queryGitReferences } from '$lib/repo/api/refs'
import type { PageLoad } from './$types'
import { GitTagsQuery } from './page.gql'
export const load: PageLoad = async ({ parent }) => {
const { resolvedRevision } = await parent()
const { resolvedRevision, graphqlClient } = await parent()
return {
deferred: {
tags: queryGitReferences({
repo: resolvedRevision.repo.id,
type: GitRefType.GIT_TAG,
first: 20,
}),
tags: graphqlClient
.query({
query: GitTagsQuery,
variables: {
repo: resolvedRevision.repo.id,
first: 20,
withBehindAhead: false,
},
})
.then(result => {
if (result.data.node?.__typename !== 'Repository') {
throw new Error('Expected Repository')
}
return result.data.node.gitRefs
}),
},
}
}

View File

@ -0,0 +1,18 @@
query GitTagsQuery($repo: ID!, $first: Int!, $withBehindAhead: Boolean!, $revspec: String = "") {
node(id: $repo) {
id
... on Repository {
gitRefs(first: $first, type: GIT_TAG, orderBy: AUTHORED_OR_COMMITTED_AT) {
...GitTagsConnection
}
}
}
}
fragment GitTagsConnection on GitRefConnection {
nodes {
id
...GitReference_Ref
}
totalCount
}

View File

@ -1,12 +1,30 @@
import { redirect, error, type Redirect } from '@sveltejs/kit'
import { asError, loadMarkdownSyntaxHighlighting, type ErrorLike } from '$lib/common'
import { resolveRepoRevision, type ResolvedRevision } from '$lib/repo/api/repo'
import { displayRepoName, isRepoSeeOtherErrorLike, isRevisionNotFoundErrorLike, parseRepoRevision } from '$lib/shared'
import type { GraphQLClient } from '$lib/graphql'
import {
CloneInProgressError,
RepoNotFoundError,
RepoSeeOtherError,
RevisionNotFoundError,
displayRepoName,
isRepoSeeOtherErrorLike,
isRevisionNotFoundErrorLike,
parseRepoRevision,
} from '$lib/shared'
import type { LayoutLoad } from './$types'
import { ResolveRepoRevison, ResolvedRepository } from './layout.gql'
export interface ResolvedRevision {
repo: ResolvedRepository
commitID: string
defaultBranch: string
}
export const load: LayoutLoad = async ({ parent, params, url, depends }) => {
const { graphqlClient: client } = await parent()
export const load: LayoutLoad = async ({ params, url, depends }) => {
// This allows other places to reload all repo related data by calling
// invalidate('repo:root')
depends('repo:root')
@ -20,7 +38,7 @@ export const load: LayoutLoad = async ({ params, url, depends }) => {
let resolvedRevisionOrError: ResolvedRevision | ErrorLike
try {
resolvedRevisionOrError = await resolveRepoRevision({ repoName, revision })
resolvedRevisionOrError = await resolveRepoRevision({ client, repoName, revision })
} catch (repoError: unknown) {
const redirect = isRepoSeeOtherErrorLike(repoError)
@ -28,7 +46,7 @@ export const load: LayoutLoad = async ({ params, url, depends }) => {
throw redirectToExternalHost(redirect, url)
}
// TODO: use differen error codes for different types of errors
// TODO: use differenr error codes for different types of errors
// Let revision errors be handled by the nested layout so that we can
// still render the main repo navigation and header
if (!isRevisionNotFoundErrorLike(repoError)) {
@ -55,3 +73,72 @@ function redirectToExternalHost(externalRedirectURL: string, currentURL: URL): R
redirectURL.protocol = externalHostURL.protocol
return redirect(303, redirectURL.toString())
}
async function resolveRepoRevision({
client,
repoName,
revision = '',
}: {
client: GraphQLClient
repoName: string
revision?: string
}): Promise<ResolvedRevision> {
let data = client.readQuery({
query: ResolveRepoRevison,
variables: {
repoName,
revision,
},
})
if (
!data ||
(data.repositoryRedirect?.__typename === 'Repository' && data.repositoryRedirect.commit?.oid !== revision)
) {
// We always refetch data when 'revision' is a "symbolic" revision (e.g. a tag or branch name)
data = await client
.query({
query: ResolveRepoRevison,
variables: {
repoName,
revision,
},
fetchPolicy: 'network-only',
})
.then(result => result.data)
}
if (!data?.repositoryRedirect) {
throw new RepoNotFoundError(repoName)
}
if (data.repositoryRedirect.__typename === 'Redirect') {
throw new RepoSeeOtherError(data.repositoryRedirect.url)
}
if (data.repositoryRedirect.mirrorInfo.cloneInProgress) {
throw new CloneInProgressError(repoName, data.repositoryRedirect.mirrorInfo.cloneProgress || undefined)
}
if (!data.repositoryRedirect.mirrorInfo.cloned) {
throw new CloneInProgressError(repoName, 'queued for cloning')
}
// The "revision" we queried for could be a commit or a changelist.
const commit = data.repositoryRedirect.commit || data.repositoryRedirect.changelist?.commit
if (!commit) {
throw new RevisionNotFoundError(revision)
}
const defaultBranch = data.repositoryRedirect.defaultBranch?.abbrevName || 'HEAD'
/*
* TODO: What exactly is this check for?
if (!commit.tree) {
throw new RevisionNotFoundError(defaultBranch)
}
*/
return {
repo: data.repositoryRedirect,
commitID: commit.oid,
defaultBranch,
}
}

View File

@ -0,0 +1,33 @@
query ResolveRepoRevison($repoName: String!, $revision: String!) {
repositoryRedirect(name: $repoName) {
__typename
... on Repository {
...ResolvedRepository
}
... on Redirect {
url
}
}
}
fragment ResolvedRepository on Repository {
id
commit(rev: $revision) {
oid
}
changelist(cid: $revision) {
commit {
oid
}
}
mirrorInfo {
cloneProgress
cloneInProgress
cloned
}
defaultBranch {
abbrevName
}
...RepoPage_ResolvedRevision
}

View File

@ -0,0 +1,29 @@
query Init {
currentUser {
...AuthenticatedUser
}
viewerSettings {
final
}
evaluatedFeatureFlags {
...FeatureFlag
}
}
query EvaluatedFeatureFlagsQuery {
evaluatedFeatureFlags {
...FeatureFlag
}
}
fragment FeatureFlag on EvaluatedFeatureFlag {
name
value
}
fragment AuthenticatedUser on User {
...Header_User
...SearchInput_AuthenticatedUser
}

View File

@ -7,7 +7,7 @@
import SearchResult from './SearchResult.svelte'
import { getSearchResultsContext } from './searchResultsContext'
import { getOwnerDisplayName, getOwnerMatchURL, buildSearchURLQueryForOwner } from '$lib/search/results'
import UserAvatar from '$lib/UserAvatar.svelte'
import Avatar from '$lib/Avatar.svelte'
import type { PersonMatch } from '$lib/shared'
export let result: PersonMatch
@ -20,7 +20,10 @@
</script>
<SearchResult>
<UserAvatar slot="icon" user={{ ...result.user, displayName }} />
<Avatar
slot="icon"
avatar={{ displayName, username: result.user?.username ?? '', avatarURL: result.user?.avatarURL ?? null }}
/>
<div slot="title">
&nbsp;
{#if ownerURL}

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { Meta, Story, Template } from '@storybook/addon-svelte-csf'
import { Story, Template } from '@storybook/addon-svelte-csf'
import SearchResults from './SearchResults.svelte'
import {
createCommitMatch,
@ -9,7 +9,7 @@
createPersonMatch,
createSymbolMatch,
createTeamMatch,
} from '$testdata'
} from '$testing/testdata'
import FileContentSearchResult from './FileContentSearchResult.svelte'
import { SvelteComponent, setContext } from 'svelte'
import { KEY, type SourcegraphContext } from '$lib/stores'
@ -27,6 +27,21 @@
import { setSearchResultsContext } from './searchResultsContext'
import { createTestGraphqlClient } from '$testing/graphql'
export const meta = {
title: 'search/SearchResults',
component: SearchResults,
parameters: {
msw: {
handlers: {
highlightedFile: graphql.query<HighlightedFileResult, HighlightedFileVariables>(
'HighlightedFile',
(req, res, ctx) => res(ctx.data(createHighlightedFileResult(req.variables.ranges)))
),
},
},
},
}
setContext<SourcegraphContext>(KEY, {
user: readable(null),
settings: readable({}),
@ -61,22 +76,9 @@
function randomizeData(i: number) {
data[i] = results[i][2]()
}
$: parameters = {
msw: {
handlers: {
highlightedFile: graphql.query<HighlightedFileResult, HighlightedFileVariables>(
'HighlightedFile',
(req, res, ctx) => res(ctx.data(createHighlightedFileResult(req.variables.ranges)))
),
},
},
}
</script>
<Meta title="search/SearchResults" component={SearchResults} {parameters} />
<Template>
<Story name="Default">
{#each results as [title, component], i}
<div>
<h2>{title}</h2>
@ -84,9 +86,7 @@
</div>
<svelte:component this={component} result={data[i]} />
{/each}
</Template>
<Story name="Default" />
</Story>
<style lang="scss">
div {

View File

@ -3,7 +3,7 @@ import signale from 'signale'
import { writable, type Readable, type Writable } from 'svelte/store'
import { vi } from 'vitest'
import type { SettingsCascade } from '$lib/shared'
import type { Settings } from '$lib/shared'
import { KEY, type SourcegraphContext } from '$lib/stores'
import type { FeatureFlagName } from '$lib/web'
@ -116,7 +116,7 @@ export function unmockFeatureFlags(): void {
* calling `unmockUserSettings` in between then subsequent calls will update the underlying settings
* store, updating all subscribers.
*/
export function mockUserSettings(settings: Partial<SettingsCascade['final']>): void {
export function mockUserSettings(settings: Partial<Settings>): void {
if (mockedSourcgraphContext.settings === unmocked) {
mockedSourcgraphContext.settings = writable(settings)
} else {

View File

@ -1,8 +1,9 @@
import { faker } from '@faker-js/faker'
import { range } from 'lodash'
import { SymbolKind, type GitCommitFields, type HistoryResult, type SignatureFields } from '$lib/graphql-operations'
import type { HighlightedFileVariables, HighlightedFileResult } from '$lib/graphql-operations'
import type { Commit } from '$lib/Commit.gql'
import { type HighlightedFileVariables, type HighlightedFileResult, SymbolKind } from '$lib/graphql-operations'
import type { HistoryPanel_HistoryConnection } from '$lib/repo/HistoryPanel.gql'
import type { CommitMatch, ContentMatch, PersonMatch, TeamMatch, PathMatch, SymbolMatch } from '$lib/shared'
/**
@ -13,7 +14,7 @@ export function seed(seed?: number): number {
return faker.seed(seed)
}
export function createSignature(): SignatureFields {
export function createSignature() {
const firstName = faker.person.firstName()
const lastName = faker.person.lastName()
const displayName = faker.internet.displayName({ firstName, lastName })
@ -29,41 +30,28 @@ export function createSignature(): SignatureFields {
url: faker.internet.url(),
username: faker.internet.userName({ firstName, lastName }),
},
email: faker.internet.email(),
},
date: faker.date.past().toISOString(),
}
}
export function createGitCommit(initial?: Partial<GitCommitFields>): GitCommitFields {
export function createGitCommit(initial?: Partial<Commit>): Commit {
const oid = faker.git.commitSha()
return {
id: faker.string.uuid(),
oid,
abbreviatedOID: oid.slice(0, 7),
subject: faker.git.commitMessage(),
body: faker.lorem.paragraph(),
author: createSignature(),
committer: faker.helpers.maybe(createSignature) ?? null,
parents: faker.helpers.multiple(
() => {
const oid = faker.git.commitSha()
return {
oid,
abbreviatedOID: oid.slice(0, 7),
url: faker.internet.url(),
canonicalURL: faker.internet.url(),
}
},
{ count: { min: 1, max: 2 } }
),
canonicalURL: faker.internet.url(),
externalURLs: [],
...initial,
}
}
export function createHistoryResults(count: number, pageSize: number): HistoryResult[] {
export function createHistoryResults(count: number, pageSize: number): HistoryPanel_HistoryConnection[] {
return Array.from({ length: count }, (_, index) => ({
nodes: faker.helpers.uniqueArray(createGitCommit, pageSize),
pageInfo: {

View File

@ -52,9 +52,8 @@ const config = {
alias: {
// Makes it easier to refer to files outside packages (such as images)
$root: '../../',
// Used inside tests for easy access to helpers
$testdata: 'src/testdata.ts',
$mocks: 'src/testing/mocks.ts',
// Used inside tests for easy access to helpers
$testing: 'src/testing',
// Map node-module to browser version
path: '../../node_modules/path-browserify',

View File

@ -2,7 +2,6 @@ import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import { generate, type CodegenConfig } from '@graphql-codegen/cli'
import graphql from '@rollup/plugin-graphql'
import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig, mergeConfig, type Plugin, type UserConfig } from 'vite'
import inspect from 'vite-plugin-inspect'
@ -17,13 +16,25 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
// caused duplicate code generation issues.
function generateGraphQLTypes(): Plugin {
const codgegenConfig: CodegenConfig = {
hooks: {
// This hook removes the 'import type * as Types from ...' import from generated files if it's not used.
// The near-operation-file preset generates this import for every file, even if it's not used. This
// generally is not a problem, but the issue is reported by `pnpm check`.
// See https://github.com/dotansimha/graphql-code-generator/issues/4900
beforeOneFileWrite(_path, content) {
if (/^import type \* as Types from/m.test(content) && !/Types(\[|\.)/.test(content)) {
return content.replace(/^import type \* as Types from .+$/m, '').trimStart()
}
return content
},
},
generates: {
// Legacy graphql-operations.ts file that is still used by some components.
'./src/lib/graphql-operations.ts': {
documents: ['src/{lib,routes}/**/*.ts', '!src/lib/graphql-{operations,types}.ts'],
documents: ['src/{lib,routes}/**/*.ts', '!src/lib/graphql-{operations,types}.ts', '!src/**/*.gql.ts'],
config: {
onlyOperationTypes: true,
enumValues: '$lib/graphql-types',
//interfaceNameForOperations: 'SvelteKitGraphQlOperations',
},
plugins: ['typescript', 'typescript-operations'],
},
@ -31,16 +42,17 @@ function generateGraphQLTypes(): Plugin {
plugins: ['typescript'],
},
'src/': {
documents: ['src/**/*.gql', '!src/**/*.gql.d.ts'],
documents: ['src/**/*.gql', '!src/**/*.gql.ts'],
preset: 'near-operation-file',
presetConfig: {
baseTypesPath: 'lib/graphql-types',
extension: '.gql.d.ts',
extension: '.gql.ts',
},
config: {
useTypeImports: true,
documentVariableSuffix: '', // The default is 'Document'
},
plugins: ['typescript-operations', `${__dirname}/dev/typed-document-node.cjs`],
plugins: ['typescript-operations', 'typed-document-node'],
},
},
schema: '../../cmd/frontend/graphqlbackend/*.graphql',
@ -77,7 +89,7 @@ function generateGraphQLTypes(): Plugin {
// Cheap custom function to check whether we should run codegen for the provided path
function shouldRunCodegen(path: string): boolean {
// Do not run codegen for generated files
if (/(graphql-(operations|types)|\.gql\.d)\.ts$/.test(path)) {
if (/(graphql-(operations|types)|\.gql)\.ts$/.test(path)) {
return false
}
if (/\.(ts|gql)$/.test(path)) {
@ -119,11 +131,9 @@ export default defineConfig(({ mode }) => {
let config: UserConfig = {
plugins: [
sveltekit(),
// Generates typescript types for gql-tags and .graphql files
// Generates typescript types for gql-tags and .gql files
generateGraphQLTypes(),
inspect(),
// Parses .graphql files and imports them as AST
graphql(),
],
define:
mode === 'test'
@ -176,6 +186,12 @@ export default defineConfig(({ mode }) => {
find: /^react-icons\/(.+)$/,
replacement: 'react-icons/$1/index.js',
},
// We generate corresponding .gql.ts files for .gql files.
// This alias allows us to import .gql files and have them resolved to the generated .gql.ts files.
{
find: /^(.*)\.gql$/,
replacement: '$1.gql.ts',
},
],
},

View File

@ -881,7 +881,7 @@ interface Caches {
export interface SuggestionsSourceConfig {
graphqlQuery: <T, V extends Record<string, any>>(query: string, variables: V) => Promise<T>
authenticatedUser?: AuthenticatedUser | null
authenticatedUser: Pick<AuthenticatedUser, 'id' | 'organizations'> | null
isSourcegraphDotCom?: boolean
valueType: 'regex' | 'glob'
}

View File

@ -440,7 +440,8 @@
"peerDependencies": {
"@graphql-codegen/typescript": "*",
"@graphql-codegen/typescript-operations": "*",
"@graphql-codegen/near-operation-file-preset": "*"
"@graphql-codegen/near-operation-file-preset": "*",
"@graphql-codegen/typed-document-node": "*"
}
}
}

View File

@ -10,7 +10,7 @@ overrides:
cssnano: 4.1.10
tslib: 2.1.0
packageExtensionsChecksum: 7b118421a6bf16cdf7b3ddb064dd0cee
packageExtensionsChecksum: dd95e2509ba76d0489f870c0fbed3202
importers:
@ -1423,10 +1423,13 @@ importers:
version: 8.0.2
'@graphql-codegen/cli':
specifier: ^5.0.0
version: 5.0.0(@graphql-codegen/near-operation-file-preset@3.0.0)(@graphql-codegen/typescript-operations@4.0.1)(@graphql-codegen/typescript@4.0.1)(@types/node@20.8.0)(graphql@15.4.0)(typescript@5.2.2)
version: 5.0.0(@graphql-codegen/near-operation-file-preset@3.0.0)(@graphql-codegen/typed-document-node@5.0.1)(@graphql-codegen/typescript-operations@4.0.1)(@graphql-codegen/typescript@4.0.1)(@types/node@20.8.0)(graphql@15.4.0)(typescript@5.2.2)
'@graphql-codegen/near-operation-file-preset':
specifier: ^3.0.0
version: 3.0.0(graphql@15.4.0)
'@graphql-codegen/typed-document-node':
specifier: ^5.0.1
version: 5.0.1(graphql@15.4.0)
'@graphql-codegen/typescript':
specifier: ^4.0.1
version: 4.0.1(graphql@15.4.0)
@ -1442,9 +1445,6 @@ importers:
'@playwright/test':
specifier: 1.25.0
version: 1.25.0
'@rollup/plugin-graphql':
specifier: ^2.0.4
version: 2.0.4(graphql@15.4.0)
'@storybook/addon-essentials':
specifier: ^7.2.0
version: 7.2.0(@types/react-dom@18.0.2)(@types/react@18.0.8)(react-dom@18.1.0)(react@18.1.0)
@ -3893,11 +3893,12 @@ packages:
- utf-8-validate
dev: true
/@graphql-codegen/cli@5.0.0(@graphql-codegen/near-operation-file-preset@3.0.0)(@graphql-codegen/typescript-operations@4.0.1)(@graphql-codegen/typescript@4.0.1)(@types/node@20.8.0)(graphql@15.4.0)(typescript@5.2.2):
/@graphql-codegen/cli@5.0.0(@graphql-codegen/near-operation-file-preset@3.0.0)(@graphql-codegen/typed-document-node@5.0.1)(@graphql-codegen/typescript-operations@4.0.1)(@graphql-codegen/typescript@4.0.1)(@types/node@20.8.0)(graphql@15.4.0)(typescript@5.2.2):
resolution: {integrity: sha512-A7J7+be/a6e+/ul2KI5sfJlpoqeqwX8EzktaKCeduyVKgOLA6W5t+NUGf6QumBDXU8PEOqXk3o3F+RAwCWOiqA==}
hasBin: true
peerDependencies:
'@graphql-codegen/near-operation-file-preset': '*'
'@graphql-codegen/typed-document-node': '*'
'@graphql-codegen/typescript': '*'
'@graphql-codegen/typescript-operations': '*'
'@parcel/watcher': ^2.1.0
@ -3912,6 +3913,7 @@ packages:
'@graphql-codegen/core': 4.0.0(graphql@15.4.0)
'@graphql-codegen/near-operation-file-preset': 3.0.0(graphql@15.4.0)
'@graphql-codegen/plugin-helpers': 5.0.1(graphql@15.4.0)
'@graphql-codegen/typed-document-node': 5.0.1(graphql@15.4.0)
'@graphql-codegen/typescript': 4.0.1(graphql@15.4.0)
'@graphql-codegen/typescript-operations': 4.0.1(graphql@15.4.0)
'@graphql-tools/apollo-engine-loader': 8.0.0(graphql@15.4.0)
@ -4061,6 +4063,22 @@ packages:
tslib: 2.1.0
dev: true
/@graphql-codegen/typed-document-node@5.0.1(graphql@15.4.0):
resolution: {integrity: sha512-VFkhCuJnkgtbbgzoCAwTdJe2G1H6sd3LfCrDqWUrQe53y2ukfSb5Ov1PhAIkCBStKCMQBUY9YgGz9GKR40qQ8g==}
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
dependencies:
'@graphql-codegen/plugin-helpers': 5.0.1(graphql@15.4.0)
'@graphql-codegen/visitor-plugin-common': 4.0.1(graphql@15.4.0)
auto-bind: 4.0.0
change-case-all: 1.0.15
graphql: 15.4.0
tslib: 2.1.0
transitivePeerDependencies:
- encoding
- supports-color
dev: true
/@graphql-codegen/typescript-apollo-client-helpers@2.2.6(graphql@15.4.0):
resolution: {integrity: sha512-WEWtjg2D/Clmep7fflKmt6o70rZj/Mqf4ywIO5jF/PI91OHpKhLFM2aWm1ythkqALwQ6wJIFlAjdYqz/EOVYdQ==}
peerDependencies:
@ -7498,21 +7516,6 @@ packages:
resolution: {integrity: sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==}
dev: true
/@rollup/plugin-graphql@2.0.4(graphql@15.4.0):
resolution: {integrity: sha512-TfaqbbK71VHodCDCoRbPnv2+Tsnlvad2OsGEviURHFl+ZBUyf5wfXgXc9RqZ+xKxSl87Z3YbPhD0z6eWYjuByw==}
engines: {node: '>=14.0.0'}
peerDependencies:
graphql: '>=0.9.0'
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@rollup/pluginutils': 5.0.2
graphql: 15.4.0
graphql-tag: 2.12.6(graphql@15.4.0)
dev: true
/@rollup/pluginutils@5.0.2:
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
engines: {node: '>=14.0.0'}