diff --git a/e2e/nx-plugin-e2e/vite.config.e2e.ts b/e2e/nx-plugin-e2e/vite.config.e2e.ts index 55b6c69a1..c0f955673 100644 --- a/e2e/nx-plugin-e2e/vite.config.e2e.ts +++ b/e2e/nx-plugin-e2e/vite.config.e2e.ts @@ -6,7 +6,7 @@ export default defineConfig({ cacheDir: '../../node_modules/.vite/nx-plugin-e2e', test: { reporters: ['basic'], - testTimeout: 40_000, + testTimeout: 80_000, globals: true, alias: tsconfigPathAliases(), pool: 'threads', diff --git a/e2e/plugin-lighthouse-e2e/vite.config.e2e.ts b/e2e/plugin-lighthouse-e2e/vite.config.e2e.ts index 7ad716e95..1040920ff 100644 --- a/e2e/plugin-lighthouse-e2e/vite.config.e2e.ts +++ b/e2e/plugin-lighthouse-e2e/vite.config.e2e.ts @@ -6,7 +6,7 @@ export default defineConfig({ cacheDir: '../../node_modules/.vite/plugin-lighthouse-e2e', test: { reporters: ['basic'], - testTimeout: 40_000, + testTimeout: 80_000, globals: true, alias: tsconfigPathAliases(), pool: 'threads', diff --git a/package-lock.json b/package-lock.json index 16545827a..d803ef8ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -105,7 +105,7 @@ "verdaccio": "^5.32.2", "vite": "^5.4.8", "vitest": "1.3.1", - "zod2md": "^0.1.3" + "zod2md": "^0.1.7" }, "engines": { "node": ">=22.14" @@ -28380,10 +28380,11 @@ } }, "node_modules/zod2md": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/zod2md/-/zod2md-0.1.3.tgz", - "integrity": "sha512-4IeV5Ti4eKWmJW33OR9c62TTA0pHlTNtsWJHCdAayvS2Z3mcAin0NZlV4gowCIRHXS4QRyyMZsgcv6tHPLg2Bw==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/zod2md/-/zod2md-0.1.7.tgz", + "integrity": "sha512-a/qcON8dBmNpABt0O/8aKiD6bORuYf/d5cSlMq65VGuv7prhF0KnoUacr0V2Y/PMkM2cmd8oKhXPNv/UZqSRgg==", "dev": true, + "license": "MIT", "dependencies": { "@commander-js/extra-typings": "^12.0.0", "bundle-require": "^4.0.2", diff --git a/package.json b/package.json index df3502018..c9faf609d 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "verdaccio": "^5.32.2", "vite": "^5.4.8", "vitest": "1.3.1", - "zod2md": "^0.1.3" + "zod2md": "^0.1.7" }, "optionalDependencies": { "@esbuild/darwin-arm64": "^0.19.12", diff --git a/packages/models/docs/models-reference.md b/packages/models/docs/models-reference.md index c63bdfd93..2100fd650 100644 --- a/packages/models/docs/models-reference.md +++ b/packages/models/docs/models-reference.md @@ -6,10 +6,11 @@ Detailed information _Object containing the following properties:_ -| Property | Description | Type | -| :------- | :------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `issues` | List of findings | _Array of [Issue](#issue) items_ | -| `table` | Table of related findings | _Object with properties:_ _or_ _Object with properties:_ | +| Property | Description | Type | +| :------- | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `issues` | List of findings | _Array of [Issue](#issue) items_ | +| `table` | Table of related findings | _Object with properties:_ _or_ _Object with properties:_ | +| `trees` | Findings in tree structure | _Array of [Tree](#tree) items_ | _All properties are optional._ @@ -97,6 +98,32 @@ _Object containing the following properties:_ _(\*) Required._ +## BasicTreeNode + +_Object containing the following properties:_ + +| Property | Description | Type | +| :-------------- | :--------------------------------------------- | :----------------------------------------------- | +| **`name`** (\*) | Text label for node | `string` (_min length: 1_) | +| `values` | Additional values for node | `Record` | +| `children` | Direct descendants of this node (omit if leaf) | _Array of [BasicTreeNode](#basictreenode) items_ | + +_(\*) Required._ + +## BasicTree + +Generic tree + +_Object containing the following properties:_ + +| Property | Description | Type | +| :-------------- | :----------- | :------------------------------ | +| `title` | Heading | `string` | +| `type` | Discriminant | `'basic'` | +| **`root`** (\*) | Root node | [BasicTreeNode](#basictreenode) | + +_(\*) Required._ + ## CategoryConfig _Object containing the following properties:_ @@ -180,6 +207,57 @@ _Object containing the following properties:_ _(\*) Required._ +## CoverageTreeMissingLOC + +Uncovered line of code, optionally referring to a named function/class/etc. + +_Object containing the following properties:_ + +| Property | Description | Type | +| :------------------- | :-------------------------------- | :------------------- | +| **`startLine`** (\*) | Start line | `number` (_int, >0_) | +| `startColumn` | Start column | `number` (_int, >0_) | +| `endLine` | End line | `number` (_int, >0_) | +| `endColumn` | End column | `number` (_int, >0_) | +| `name` | Identifier of function/class/etc. | `string` | +| `kind` | E.g. "function", "class" | `string` | + +_(\*) Required._ + +## CoverageTreeNode + +_Object containing the following properties:_ + +| Property | Description | Type | +| :---------------- | :-------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`name`** (\*) | File or folder name | `string` (_min length: 1_) | +| **`values`** (\*) | Coverage metrics for file/folder | _Object with properties:_ | +| `children` | Files and folders contained in this folder (omit if file) | _Array of [CoverageTreeNode](#coveragetreenode) items_ | + +_(\*) Required._ + +## CoverageTree + +Coverage for files and folders + +_Object containing the following properties:_ + +| Property | Description | Type | +| :-------------- | :----------- | :------------------------------------ | +| `title` | Heading | `string` | +| **`type`** (\*) | Discriminant | `'coverage'` | +| **`root`** (\*) | Root folder | [CoverageTreeNode](#coveragetreenode) | + +_(\*) Required._ + +## FileName + +_String which matches the regular expression `/^(?!.*[ \\/:*?"<>|]).+$/` and has a minimum length of 1._ + +## FilePath + +_String which has a minimum length of 1._ + ## Format _Enum string, one of the following possible values:_ @@ -1137,27 +1215,15 @@ _Enum string, one of the following possible values:_ -## OnProgress - -_Function._ - -_Parameters:_ - -1. `unknown` (_optional & nullable_) - -_Returns:_ - -- `void` (_optional_) - ## PersistConfig _Object containing the following properties:_ -| Property | Description | Type | -| :---------- | :-------------------------------------- | :-------------------------------------------------------------- | -| `outputDir` | Artifacts folder | `string` (_min length: 1_) | -| `filename` | Artifacts file name (without extension) | `string` (_regex: `/^(?!.*[ \\/:*?"<>\|]).+$/`, min length: 1_) | -| `format` | | _Array of [Format](#format) items_ | +| Property | Description | Type | +| :---------- | :-------------------------------------- | :--------------------------------- | +| `outputDir` | Artifacts folder | [FilePath](#filepath) | +| `filename` | Artifacts file name (without extension) | [FileName](#filename) | +| `format` | | _Array of [Format](#format) items_ | _All properties are optional._ @@ -1264,8 +1330,20 @@ _Object containing the following properties:_ | :-------------------- | :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`command`** (\*) | Shell command to execute | `string` | | `args` | | `Array` | -| **`outputFile`** (\*) | Output path | `string` (_min length: 1_) | +| **`outputFile`** (\*) | Runner output path | [FilePath](#filepath) | | `outputTransform` | | _Function:_
| +| `configFile` | Runner config path | [FilePath](#filepath) | + +_(\*) Required._ + +## RunnerFilesPaths + +_Object containing the following properties:_ + +| Property | Description | Type | +| :-------------------------- | :----------------- | :-------------------- | +| **`runnerConfigPath`** (\*) | Runner config path | [FilePath](#filepath) | +| **`runnerOutputPath`** (\*) | Runner output path | [FilePath](#filepath) | _(\*) Required._ @@ -1275,7 +1353,7 @@ _Function._ _Parameters:_ -1. [OnProgress](#onprogress) (_optional_) +- _none_ _Returns:_ @@ -1289,7 +1367,7 @@ _Object containing the following properties:_ | Property | Description | Type | | :-------------- | :--------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`file`** (\*) | Relative path to source file in Git repo | `string` (_min length: 1_) | +| **`file`** (\*) | Relative path to source file in Git repo | [FilePath](#filepath) | | `position` | Location in file | _Object with properties:_ | _(\*) Required._ @@ -1353,6 +1431,13 @@ Primitive row _Array of [TableCellValue](#tablecellvalue) (\_optional & nullable_) items.\_ +## Tree + +_Union of the following possible types:_ + +- [BasicTree](#basictree) +- [CoverageTree](#coveragetree) + ## UploadConfig _Object containing the following properties:_ diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 8d55ca6ee..fcef50523 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -97,11 +97,11 @@ export { } from './lib/reports-diff.js'; export { runnerConfigSchema, - runnerFunctionSchema, runnerFilesPathsSchema, + runnerFunctionSchema, type RunnerConfig, - type RunnerFunction, type RunnerFilesPaths, + type RunnerFunction, } from './lib/runner-config.js'; export { tableAlignmentSchema, @@ -117,4 +117,18 @@ export { type TableRowObject, type TableRowPrimitive, } from './lib/table.js'; +export { + basicTreeNodeSchema, + basicTreeSchema, + coverageTreeMissingLOCSchema, + coverageTreeNodeSchema, + coverageTreeSchema, + treeSchema, + type BasicTree, + type BasicTreeNode, + type CoverageTree, + type CoverageTreeMissingLOC, + type CoverageTreeNode, + type Tree, +} from './lib/tree.js'; export { uploadConfigSchema, type UploadConfig } from './lib/upload-config.js'; diff --git a/packages/models/src/lib/audit-output.ts b/packages/models/src/lib/audit-output.ts index 4f9c121dc..7c9b611ff 100644 --- a/packages/models/src/lib/audit-output.ts +++ b/packages/models/src/lib/audit-output.ts @@ -7,6 +7,7 @@ import { import { errorItems, hasDuplicateStrings } from './implementation/utils.js'; import { issueSchema } from './issue.js'; import { tableSchema } from './table.js'; +import { treeSchema } from './tree.js'; export const auditValueSchema = nonnegativeNumberSchema.describe('Raw numeric value'); @@ -20,6 +21,9 @@ export const auditDetailsSchema = z.object( .array(issueSchema, { description: 'List of findings' }) .optional(), table: tableSchema('Table of related findings').optional(), + trees: z + .array(treeSchema, { description: 'Findings in tree structure' }) + .optional(), }, { description: 'Detailed information' }, ); diff --git a/packages/models/src/lib/implementation/schemas.ts b/packages/models/src/lib/implementation/schemas.ts index 2b887ba36..5e2a7b1e4 100644 --- a/packages/models/src/lib/implementation/schemas.ts +++ b/packages/models/src/lib/implementation/schemas.ts @@ -223,3 +223,13 @@ type Ref = { weight: number }; function hasNonZeroWeightedRef(refs: Ref[]) { return refs.reduce((acc, { weight }) => weight + acc, 0) !== 0; } + +export const filePositionSchema = z.object( + { + startLine: positiveIntSchema.describe('Start line'), + startColumn: positiveIntSchema.describe('Start column').optional(), + endLine: positiveIntSchema.describe('End line').optional(), + endColumn: positiveIntSchema.describe('End column').optional(), + }, + { description: 'Location in file' }, +); diff --git a/packages/models/src/lib/source.ts b/packages/models/src/lib/source.ts index f1e55f2fb..3b69e90ef 100644 --- a/packages/models/src/lib/source.ts +++ b/packages/models/src/lib/source.ts @@ -1,20 +1,13 @@ import { z } from 'zod'; -import { filePathSchema, positiveIntSchema } from './implementation/schemas.js'; +import { + filePathSchema, + filePositionSchema, +} from './implementation/schemas.js'; export const sourceFileLocationSchema = z.object( { file: filePathSchema.describe('Relative path to source file in Git repo'), - position: z - .object( - { - startLine: positiveIntSchema.describe('Start line'), - startColumn: positiveIntSchema.describe('Start column').optional(), - endLine: positiveIntSchema.describe('End line').optional(), - endColumn: positiveIntSchema.describe('End column').optional(), - }, - { description: 'Location in file' }, - ) - .optional(), + position: filePositionSchema.optional(), }, { description: 'Source file location' }, ); diff --git a/packages/models/src/lib/tree.ts b/packages/models/src/lib/tree.ts new file mode 100644 index 000000000..45477b554 --- /dev/null +++ b/packages/models/src/lib/tree.ts @@ -0,0 +1,77 @@ +import { z } from 'zod'; +import { filePositionSchema } from './implementation/schemas.js'; + +const basicTreeNodeValuesSchema = z.record(z.union([z.number(), z.string()])); +const basicTreeNodeDataSchema = z.object({ + name: z.string().min(1).describe('Text label for node'), + values: basicTreeNodeValuesSchema + .optional() + .describe('Additional values for node'), +}); + +export const basicTreeNodeSchema: z.ZodType = + basicTreeNodeDataSchema.extend({ + children: z + .lazy(() => z.array(basicTreeNodeSchema).optional()) + .describe('Direct descendants of this node (omit if leaf)'), + }); +export type BasicTreeNode = z.infer & { + children?: BasicTreeNode[]; +}; + +export const coverageTreeMissingLOCSchema = filePositionSchema + .extend({ + name: z.string().optional().describe('Identifier of function/class/etc.'), + kind: z.string().optional().describe('E.g. "function", "class"'), + }) + .describe( + 'Uncovered line of code, optionally referring to a named function/class/etc.', + ); +export type CoverageTreeMissingLOC = z.infer< + typeof coverageTreeMissingLOCSchema +>; + +const coverageTreeNodeValuesSchema = z.object({ + coverage: z.number().min(0).max(1).describe('Coverage ratio'), + missing: z + .array(coverageTreeMissingLOCSchema) + .optional() + .describe('Uncovered lines of code'), +}); +const coverageTreeNodeDataSchema = z.object({ + name: z.string().min(1).describe('File or folder name'), + values: coverageTreeNodeValuesSchema.describe( + 'Coverage metrics for file/folder', + ), +}); + +export const coverageTreeNodeSchema: z.ZodType = + coverageTreeNodeDataSchema.extend({ + children: z + .lazy(() => z.array(coverageTreeNodeSchema).optional()) + .describe('Files and folders contained in this folder (omit if file)'), + }); +export type CoverageTreeNode = z.infer & { + children?: CoverageTreeNode[]; +}; + +export const basicTreeSchema = z + .object({ + title: z.string().optional().describe('Heading'), + type: z.literal('basic').optional().describe('Discriminant'), + root: basicTreeNodeSchema.describe('Root node'), + }) + .describe('Generic tree'); +export type BasicTree = z.infer; + +export const coverageTreeSchema = z + .object({ + title: z.string().optional().describe('Heading'), + type: z.literal('coverage').describe('Discriminant'), + root: coverageTreeNodeSchema.describe('Root folder'), + }) + .describe('Coverage for files and folders'); +export type CoverageTree = z.infer; + +export const treeSchema = z.union([basicTreeSchema, coverageTreeSchema]); +export type Tree = z.infer; diff --git a/packages/models/src/lib/tree.unit.test.ts b/packages/models/src/lib/tree.unit.test.ts new file mode 100644 index 000000000..40637ad12 --- /dev/null +++ b/packages/models/src/lib/tree.unit.test.ts @@ -0,0 +1,66 @@ +import { type BasicTree, type CoverageTree, treeSchema } from './tree.js'; + +describe('treeSchema', () => { + it('should accept basic tree', () => { + expect(() => + treeSchema.parse({ + 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 }, + }, + ], + }, + } satisfies BasicTree), + ).not.toThrow(); + }); + + it('should accept coverage tree', () => { + expect(() => + treeSchema.parse({ + type: 'coverage', + title: 'Critical request chain', + 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 }], + }, + }, + ], + }, + ], + }, + } satisfies CoverageTree), + ).not.toThrow(); + }); +});