diff --git a/package-lock.json b/package-lock.json index d803ef8ad..8092299c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@code-pushup/portal-client": "^0.9.0", + "@code-pushup/portal-client": "^0.13.0", "@isaacs/cliui": "^8.0.2", "@nx/devkit": "19.8.13", "@poppinss/cliui": "6.4.1", @@ -2335,9 +2335,10 @@ } }, "node_modules/@code-pushup/portal-client": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@code-pushup/portal-client/-/portal-client-0.9.0.tgz", - "integrity": "sha512-ABQlc6x24UflEm7uO7Tt9KlU0NnhbmbAj7lUv7H9dgxgJ0fcK5R3rSp7hYab3npawb2nX+6+aG2OaPSiYsFPBQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@code-pushup/portal-client/-/portal-client-0.13.0.tgz", + "integrity": "sha512-v94sA9zYMCBfQrGRImBRC0iWW+ZvwRJloWTWU06yYXp/+pIHjBqwXuN34IQr1kmSkPC/hZIKc7vwJG7fiYHfAQ==", + "license": "MIT", "dependencies": { "graphql": "^16.6.0", "graphql-request": "^6.1.0", diff --git a/package.json b/package.json index c9faf609d..207cc8267 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "node": ">=22.14" }, "dependencies": { - "@code-pushup/portal-client": "^0.9.0", + "@code-pushup/portal-client": "^0.13.0", "@isaacs/cliui": "^8.0.2", "@nx/devkit": "19.8.13", "@poppinss/cliui": "6.4.1", diff --git a/packages/core/package.json b/packages/core/package.json index 2506afb42..6c585951d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -44,7 +44,7 @@ "ansis": "^3.3.0" }, "peerDependencies": { - "@code-pushup/portal-client": "^0.9.0" + "@code-pushup/portal-client": "^0.13.0" }, "peerDependenciesMeta": { "@code-pushup/portal-client": { diff --git a/packages/core/src/lib/implementation/report-to-gql.ts b/packages/core/src/lib/implementation/report-to-gql.ts index e5538aa74..0a9358682 100644 --- a/packages/core/src/lib/implementation/report-to-gql.ts +++ b/packages/core/src/lib/implementation/report-to-gql.ts @@ -11,12 +11,19 @@ import type { TableAlignment as PortalTableAlignment, AuditReportTableCell as PortalTableCell, AuditReportTableColumn as PortalTableColumn, + AuditReportTree as PortalTree, + AuditReportTreeNode as PortalTreeNode, + TreeType as PortalTreeType, SaveReportMutationVariables, } from '@code-pushup/portal-client'; import type { AuditReport, + BasicTree, + BasicTreeNode, CategoryConfig, CategoryRef, + CoverageTree, + CoverageTreeNode, Group, Issue, IssueSeverity, @@ -24,6 +31,7 @@ import type { Report, Table, TableAlignment, + Tree, } from '@code-pushup/models'; export function reportToGQL( @@ -75,7 +83,7 @@ function auditToGQL(audit: AuditReport): PortalAudit { displayValue: formattedValue, details, } = audit; - const { issues, table } = details ?? {}; + const { issues, table, trees } = details ?? {}; return { slug, title, @@ -88,6 +96,7 @@ function auditToGQL(audit: AuditReport): PortalAudit { details: { ...(issues && { issues: issues.map(issueToGQL) }), ...(table && { tables: [tableToGQL(table)] }), + ...(trees && { trees: trees.map(treeToGQL) }), }, }), }; @@ -134,6 +143,57 @@ export function tableToGQL(table: Table): PortalTable { }; } +export function treeToGQL(tree: Tree): PortalTree { + if (tree.type === 'coverage') { + return coverageTreeToGQL(tree); + } + return basicTreeToGQL(tree); +} + +function basicTreeToGQL(tree: BasicTree): PortalTree { + return { + type: safeEnum('Basic'), + ...(tree.title && { title: tree.title }), + root: basicTreeNodeToGQL(tree.root), + }; +} + +function basicTreeNodeToGQL(node: BasicTreeNode): PortalTreeNode { + return { + name: node.name, + ...(node.values && { + values: Object.entries(node.values).map(([key, value]) => ({ + key, + value: value.toString(), + })), + }), + ...(node.children?.length && { + children: node.children.map(basicTreeNodeToGQL), + }), + }; +} + +function coverageTreeToGQL(tree: CoverageTree): PortalTree { + return { + type: safeEnum('Coverage'), + ...(tree.title && { title: tree.title }), + root: coverageTreeNodeToGQL(tree.root), + }; +} + +function coverageTreeNodeToGQL(node: CoverageTreeNode): PortalTreeNode { + return { + name: node.name, + coverage: node.values.coverage, + ...(node.values.missing?.length && { + missing: node.values.missing, + }), + ...(node.children?.length && { + children: node.children.map(coverageTreeNodeToGQL), + }), + }; +} + function categoryToGQL(category: CategoryConfig): PortalCategory { return { slug: category.slug, @@ -188,7 +248,8 @@ function safeEnum< | PortalCategoryRefType | PortalIssueSeverity | PortalIssueSourceType - | PortalTableAlignment, + | PortalTableAlignment + | PortalTreeType, >(value: `${T}`): T { return value as T; } diff --git a/packages/core/src/lib/implementation/report-to-gql.unit.test.ts b/packages/core/src/lib/implementation/report-to-gql.unit.test.ts index c8be3b007..1de787cbe 100644 --- a/packages/core/src/lib/implementation/report-to-gql.unit.test.ts +++ b/packages/core/src/lib/implementation/report-to-gql.unit.test.ts @@ -1,5 +1,6 @@ import { describe } from 'vitest'; -import { issueToGQL, tableToGQL } from './report-to-gql.js'; +import { type AuditReportTree, TreeType } from '@code-pushup/portal-client'; +import { issueToGQL, tableToGQL, treeToGQL } from './report-to-gql.js'; describe('issueToGQL', () => { it('transforms issue to GraphQL input type', () => { @@ -81,3 +82,122 @@ describe('tableToGQL', () => { }); }); }); + +describe('treeToGQL', () => { + it('should transform basic tree', () => { + expect( + treeToGQL({ + title: 'Critical request chain', + root: { + name: 'https://example.com', + children: [ + { + name: 'https://example.com/styles/base.css', + values: { size: '2 kB', duration: 20 }, + }, + { + name: 'https://example.com/styles/theme.css', + values: { size: '10 kB', duration: 100 }, + }, + ], + }, + }), + ).toStrictEqual({ + type: TreeType.Basic, + title: 'Critical request chain', + root: { + name: 'https://example.com', + children: [ + { + name: 'https://example.com/styles/base.css', + values: [ + { key: 'size', value: '2 kB' }, + { key: 'duration', value: '20' }, + ], + }, + { + name: 'https://example.com/styles/theme.css', + values: [ + { key: 'size', value: '10 kB' }, + { key: 'duration', value: '100' }, + ], + }, + ], + }, + }); + }); + + it('should transform coverage tree', () => { + expect( + treeToGQL({ + type: 'coverage', + title: 'Function coverage', + root: { + name: '.', + values: { coverage: 0.7 }, + children: [ + { + name: 'src', + values: { coverage: 0.7 }, + children: [ + { + name: 'App.tsx', + values: { + coverage: 0.8, + missing: [ + { + startLine: 42, + endLine: 50, + name: 'login', + kind: 'function', + }, + ], + }, + }, + { + name: 'index.ts', + values: { + coverage: 0, + missing: [{ startLine: 1, endLine: 10 }], + }, + }, + ], + }, + ], + }, + }), + ).toStrictEqual({ + type: TreeType.Coverage, + title: 'Function coverage', + root: { + name: '.', + coverage: 0.7, + children: [ + { + name: 'src', + coverage: 0.7, + children: [ + { + name: 'App.tsx', + coverage: 0.8, + missing: [ + { + startLine: 42, + endLine: 50, + name: 'login', + kind: 'function', + }, + ], + }, + { + name: 'index.ts', + coverage: 0, + missing: [{ startLine: 1, endLine: 10 }], + }, + ], + }, + ], + }, + }); + }); +}); diff --git a/packages/models/src/lib/tree.unit.test.ts b/packages/models/src/lib/tree.unit.test.ts index 40637ad12..447a40de9 100644 --- a/packages/models/src/lib/tree.unit.test.ts +++ b/packages/models/src/lib/tree.unit.test.ts @@ -26,7 +26,7 @@ describe('treeSchema', () => { expect(() => treeSchema.parse({ type: 'coverage', - title: 'Critical request chain', + title: 'Function coverage', root: { name: '.', values: { coverage: 0.7 },