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:_
- `title`: `string` - Display title for table
- `columns`: _Array of [TableAlignment](#tablealignment) items_
- `rows`: _Array of [TableRowPrimitive](#tablerowprimitive) items_
_or_ _Object with properties:_- `title`: `string` - Display title for table
- `columns`: _Array of [TableAlignment](#tablealignment) items_ _or_ _Array of [TableColumnObject](#tablecolumnobject) items_
- `rows`: _Array of [TableRowObject](#tablerowobject) items_
|
+| Property | Description | Type |
+| :------- | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `issues` | List of findings | _Array of [Issue](#issue) items_ |
+| `table` | Table of related findings | _Object with properties:_- `title`: `string` - Display title for table
- `columns`: _Array of [TableAlignment](#tablealignment) items_
- `rows`: _Array of [TableRowPrimitive](#tablerowprimitive) items_
_or_ _Object with properties:_- `title`: `string` - Display title for table
- `columns`: _Array of [TableAlignment](#tablealignment) items_ _or_ _Array of [TableColumnObject](#tablecolumnobject) items_
- `rows`: _Array of [TableRowObject](#tablerowobject) items_
|
+| `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:_- `coverage`: `number` (_≥0, ≤1_) - Coverage ratio
- `missing`: _Array of [CoverageTreeMissingLOC](#coveragetreemissingloc) items_ - Uncovered lines of code
|
+| `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:_
- _parameters:_
- `unknown` (_optional & nullable_)
- _returns:_ [AuditOutputs](#auditoutputs) _or_ _Promise of_ [AuditOutputs](#auditoutputs)
|
+| `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:_- `startLine`: `number` (_int, >0_) - Start line
- `startColumn`: `number` (_int, >0_) - Start column
- `endLine`: `number` (_int, >0_) - End line
- `endColumn`: `number` (_int, >0_) - End column
|
_(\*) 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();
+ });
+});