diff --git a/CONTRIBUTING.MD b/CONTRIBUTING.MD index 4c55933..f3a4a54 100644 --- a/CONTRIBUTING.MD +++ b/CONTRIBUTING.MD @@ -73,7 +73,6 @@ This is an Nx monorepo with the following structure: │ ├── minimal-repo/ # Test fixtures and examples │ └── shared/ # Shared libraries │ ├── angular-ast-utils/ # Angular AST parsing -│ ├── angular-cli-utils/ # Angular CLI utilities │ ├── ds-component-coverage/ # Design system analysis │ ├── models/ # Core types and schemas │ ├── styles-ast-utils/ # CSS/SCSS AST parsing diff --git a/README.md b/README.md index 8e943fc..44b641f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ A Model Context Protocol (MCP) server that provides Angular project analysis and ### Prerequisites -- Node.js (version 18 or higher) +- Node.js (version 18 or higher) with ESM support ### Installation & Setup @@ -108,7 +108,7 @@ const dsComponents = [ } ]; -module.exports = dsComponents; +export default dsComponents; ``` ### Example Project Structure diff --git a/docs/architecture-internal-design.md b/docs/architecture-internal-design.md index 8ebf325..2547097 100644 --- a/docs/architecture-internal-design.md +++ b/docs/architecture-internal-design.md @@ -110,7 +110,6 @@ Validation is handled via **Zod** in `angular-mcp-server-options.schema.ts`. models (types & schemas) ├─ utils ├─ styles-ast-utils -├─ angular-cli-utils └─ angular-ast-utils └─ ds-component-coverage (top-level plugin) ``` diff --git a/jest.preset.js b/jest.preset.js deleted file mode 100644 index f078ddc..0000000 --- a/jest.preset.js +++ /dev/null @@ -1,3 +0,0 @@ -const nxPreset = require('@nx/jest/preset').default; - -module.exports = { ...nxPreset }; diff --git a/jest.preset.mjs b/jest.preset.mjs new file mode 100644 index 0000000..7f6ac4d --- /dev/null +++ b/jest.preset.mjs @@ -0,0 +1,3 @@ +import nxPreset from '@nx/jest/preset'; + +export default { ...nxPreset }; diff --git a/package-lock.json b/package-lock.json index b446c2d..8b2f48b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,12 +19,11 @@ "@angular-devkit/schematics": "~19.2.0", "@angular/cli": "~19.2.0", "@angular/compiler": "~19.2.0", - "@code-pushup/core": "^0.69.0", - "@code-pushup/eslint-plugin": "^0.69.0", - "@code-pushup/models": "^0.69.0", - "@code-pushup/utils": "^0.69.0", + "@code-pushup/core": "^0.75.0", + "@code-pushup/eslint-plugin": "^0.75.0", + "@code-pushup/models": "^0.75.0", + "@code-pushup/utils": "^0.75.0", "@modelcontextprotocol/sdk": "^1.12.1", - "@push-based/angular-cli-utils": "^0.0.1", "@push-based/ds-component-coverage": "^0.0.1", "@push-based/models": "^0.0.1", "@push-based/utils": "^0.0.1", @@ -4363,17 +4362,17 @@ "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@code-pushup/core": { - "version": "0.69.2", - "resolved": "https://registry.npmjs.org/@code-pushup/core/-/core-0.69.2.tgz", - "integrity": "sha512-5a0zwlTZN4iujRJgajvwa+y6V4rs8LCi6FsxAaoGi72tU6WVtF6sWQ20d1JfgNfLshfkVK1m76dsirY8o9MEPw==", + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@code-pushup/core/-/core-0.75.0.tgz", + "integrity": "sha512-Dhp6DFjwgfTkohjy5LRzWnw2TR1tkQwCwwUlBWc7fmwzvP1Ogw4qgT6C8U171jJ4wlxIX8bqyBf/Z8o1SsIEsA==", "license": "MIT", "dependencies": { - "@code-pushup/models": "0.69.2", - "@code-pushup/utils": "0.69.2", + "@code-pushup/models": "0.75.0", + "@code-pushup/utils": "0.75.0", "ansis": "^3.3.0" }, "peerDependencies": { - "@code-pushup/portal-client": "^0.13.0" + "@code-pushup/portal-client": "^0.15.0" }, "peerDependenciesMeta": { "@code-pushup/portal-client": { @@ -4382,15 +4381,15 @@ } }, "node_modules/@code-pushup/eslint-plugin": { - "version": "0.69.2", - "resolved": "https://registry.npmjs.org/@code-pushup/eslint-plugin/-/eslint-plugin-0.69.2.tgz", - "integrity": "sha512-QZeIn9ICYEAFrcno6fUj+aZzKJJa38H+RjdCRVKn46+MzYV55gTujRebltFGUKFIr/AXilSRzfnPlCLWMbp15Q==", + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@code-pushup/eslint-plugin/-/eslint-plugin-0.75.0.tgz", + "integrity": "sha512-fpTdtFARehX277M2hrV4xbymj/NuNEsqMs1TO9/jyHJqmSaOk9lp5mg2ePudbcGKDW7E/wJbgBmsFC51WV4GBg==", "license": "MIT", "dependencies": { - "@code-pushup/models": "0.69.2", - "@code-pushup/utils": "0.69.2", + "@code-pushup/models": "0.75.0", + "@code-pushup/utils": "0.75.0", "yargs": "^17.7.2", - "zod": "^3.22.4" + "zod": "^4.0.5" }, "peerDependencies": { "@nx/devkit": ">=17.0.0", @@ -4402,23 +4401,41 @@ } } }, + "node_modules/@code-pushup/eslint-plugin/node_modules/zod": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz", + "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@code-pushup/models": { - "version": "0.69.2", - "resolved": "https://registry.npmjs.org/@code-pushup/models/-/models-0.69.2.tgz", - "integrity": "sha512-MLDZ4xB0kEZAljTi9922SnuFvB40wZCRFN89uSlLcrl4sOAGvxXWm3llWct6/bDd2y/77b4Zr8UeTd1Arh8rmQ==", + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@code-pushup/models/-/models-0.75.0.tgz", + "integrity": "sha512-LYtOOkkyaDeR8fXvAbS+rw2DpzT6J7KFrH2vwz+rL+l4bfXXfpKldMsuxIv7WkkVQyZ8ZLzSO+ikREg/LixN1w==", "license": "MIT", "dependencies": { "vscode-material-icons": "^0.1.0", - "zod": "^3.22.1" + "zod": "^4.0.5" + } + }, + "node_modules/@code-pushup/models/node_modules/zod": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz", + "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/@code-pushup/utils": { - "version": "0.69.2", - "resolved": "https://registry.npmjs.org/@code-pushup/utils/-/utils-0.69.2.tgz", - "integrity": "sha512-KyKDk1jaNKBl/9nm88GjbAxqcszR+s+m3nbAHibUsoH/EBqS3Gm/usE2B5aRb/DCa24r0Y4c/FJMFwqUxnycDg==", + "version": "0.75.0", + "resolved": "https://registry.npmjs.org/@code-pushup/utils/-/utils-0.75.0.tgz", + "integrity": "sha512-IkMxONLe/KpQLOnkY8592LF7CiQTGCork2vlnkTDFIutw9/fwhuUtkUYa7h9+0plF7S/+bwWtsV/t76s59JJqg==", "license": "MIT", "dependencies": { - "@code-pushup/models": "0.69.2", + "@code-pushup/models": "0.75.0", "@isaacs/cliui": "^8.0.2", "@poppinss/cliui": "^6.4.0", "ansis": "^3.3.0", @@ -4428,13 +4445,21 @@ "multi-progress-bars": "^5.0.3", "semver": "^7.6.0", "simple-git": "^3.20.0", - "zod": "^3.23.8", - "zod-validation-error": "^3.4.0" + "zod": "^4.0.5" }, "engines": { "node": ">=17.0.0" } }, + "node_modules/@code-pushup/utils/node_modules/zod": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz", + "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -10709,10 +10734,6 @@ "resolved": "packages/shared/angular-ast-utils", "link": true }, - "node_modules/@push-based/angular-cli-utils": { - "resolved": "packages/shared/angular-cli-utils", - "link": true - }, "node_modules/@push-based/angular-mcp": { "resolved": "packages/angular-mcp", "link": true @@ -31603,18 +31624,6 @@ "zod": "^3.24.1" } }, - "node_modules/zod-validation-error": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.2.tgz", - "integrity": "sha512-mdi7YOLtram5dzJ5aDtm1AG9+mxRma1iaMrZdYIpFO7epdKBUwLHIxTF8CPDeCQ828zAXYtizrKlEJAtzgfgrw==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0" - } - }, "packages/angular-mcp": { "name": "@push-based/angular-mcp", "version": "0.0.1", @@ -31632,7 +31641,8 @@ }, "packages/shared/angular-cli-utils": { "name": "@push-based/angular-cli-utils", - "version": "0.0.1" + "version": "0.0.1", + "extraneous": true }, "packages/shared/ds-component-coverage": { "name": "@push-based/ds-component-coverage", diff --git a/package.json b/package.json index c4bfe6b..b045d46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@push-based/source", "version": "0.0.0", + "type": "module", "license": "MIT", "scripts": {}, "private": true, @@ -61,12 +62,11 @@ "@angular-devkit/schematics": "~19.2.0", "@angular/cli": "~19.2.0", "@angular/compiler": "~19.2.0", - "@code-pushup/core": "^0.69.0", - "@code-pushup/eslint-plugin": "^0.69.0", - "@code-pushup/models": "^0.69.0", - "@code-pushup/utils": "^0.69.0", + "@code-pushup/core": "^0.75.0", + "@code-pushup/eslint-plugin": "^0.75.0", + "@code-pushup/models": "^0.75.0", + "@code-pushup/utils": "^0.75.0", "@modelcontextprotocol/sdk": "^1.12.1", - "@push-based/angular-cli-utils": "^0.0.1", "@push-based/ds-component-coverage": "^0.0.1", "@push-based/models": "^0.0.1", "@push-based/utils": "^0.0.1", diff --git a/packages/angular-mcp-server/package.json b/packages/angular-mcp-server/package.json index de361e8..46991d1 100644 --- a/packages/angular-mcp-server/package.json +++ b/packages/angular-mcp-server/package.json @@ -2,7 +2,7 @@ "name": "@push-based/angular-mcp-server", "version": "0.0.1", "private": true, - "type": "commonjs", + "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/angular-mcp-server/src/index.ts b/packages/angular-mcp-server/src/index.ts index 2f91975..a10c23e 100644 --- a/packages/angular-mcp-server/src/index.ts +++ b/packages/angular-mcp-server/src/index.ts @@ -1 +1 @@ -export * from './lib/angular-mcp-server'; +export * from './lib/angular-mcp-server.js'; diff --git a/packages/angular-mcp-server/src/lib/angular-mcp-server.ts b/packages/angular-mcp-server/src/lib/angular-mcp-server.ts index 11146f8..ff5590e 100644 --- a/packages/angular-mcp-server/src/lib/angular-mcp-server.ts +++ b/packages/angular-mcp-server/src/lib/angular-mcp-server.ts @@ -11,16 +11,16 @@ import { ListResourcesResult, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; -import { TOOLS } from './tools/tools'; -import { toolNotFound } from './tools/utils'; +import { TOOLS } from './tools/tools.js'; +import { toolNotFound } from './tools/utils.js'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { AngularMcpServerOptionsSchema, AngularMcpServerOptions, -} from './validation/angular-mcp-server-options.schema'; -import { validateAngularMcpServerFilesExist } from './validation/file-existence'; -import { validateDeprecatedCssClassesFile } from './validation/ds-components-file.validation'; +} from './validation/angular-mcp-server-options.schema.js'; +import { validateAngularMcpServerFilesExist } from './validation/file-existence.js'; +import { validateDeprecatedCssClassesFile } from './validation/ds-components-file.validation.js'; export class AngularMcpServerWrapper { private readonly mcpServer: McpServer; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts index b536428..4f9acb8 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts @@ -7,6 +7,7 @@ import { buildComponentContract } from './utils/build-contract.js'; import { generateContractSummary } from '../shared/utils/contract-file-ops.js'; import { ContractResult } from './models/types.js'; import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path.js'; +import { createHash } from 'node:crypto'; interface BuildComponentContractOptions extends BaseHandlerOptions { saveLocation: string; @@ -52,10 +53,7 @@ export const buildComponentContractHandler = createHandler< ); const contractString = JSON.stringify(contract, null, 2); - const hash = require('node:crypto') - .createHash('sha256') - .update(contractString) - .digest('hex'); + const hash = createHash('sha256').update(contractString).digest('hex'); const effectiveSaveLocation = resolveCrossPlatformPath(cwd, saveLocation); diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts index fdcb860..4459d2c 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts @@ -1,5 +1,5 @@ import { ToolSchemaOptions } from '@push-based/models'; -import { COMMON_ANNOTATIONS } from '../../../shared'; +import { COMMON_ANNOTATIONS } from '../../../shared/index.js'; /** * Schema for building component contracts diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts index 7a33220..7d05a6e 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts @@ -1,5 +1,5 @@ import { ToolSchemaOptions } from '@push-based/models'; -import { COMMON_ANNOTATIONS } from '../../../shared'; +import { COMMON_ANNOTATIONS } from '../../../shared/index.js'; /** * Schema for diffing component contracts diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/models/types.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/models/types.ts index 79fedc4..329ef82 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/models/types.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/models/types.ts @@ -1,4 +1,4 @@ -import { SourceFileLocation } from '@push-based/models'; +import { SourceFileLocation } from '@code-pushup/models'; export interface DependencyInfo { path: string; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/component-helpers.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/component-helpers.ts index 0008709..a60aa37 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/component-helpers.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/component-helpers.ts @@ -1,6 +1,6 @@ import * as path from 'path'; -import { toUnixPath } from '@push-based/utils'; -import { buildText } from '../../shared/utils/output.utils'; +import { toUnixPath } from '@code-pushup/utils'; +import { buildText } from '../../shared/utils/output.utils.js'; import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { DependencyGraphResult, diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/component-usage-graph-builder.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/component-usage-graph-builder.ts index bd0f574..51562a9 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/component-usage-graph-builder.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/component-usage-graph-builder.ts @@ -1,5 +1,6 @@ import * as path from 'path'; -import { toUnixPath, findAllFiles } from '@push-based/utils'; +import { toUnixPath } from '@code-pushup/utils'; +import { findAllFiles } from '@push-based/utils'; import { BuildComponentUsageGraphOptions, ComponentUsageGraphResult, diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/path-resolver.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/path-resolver.ts index caa4838..0ba5a9e 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/path-resolver.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/path-resolver.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { toUnixPath } from '@push-based/utils'; +import { toUnixPath } from '@code-pushup/utils'; import { DEPENDENCY_ANALYSIS_CONFIG } from '../models/config.js'; const { resolveExtensions, indexFiles } = DEPENDENCY_ANALYSIS_CONFIG; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/unified-ast-analyzer.ts b/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/unified-ast-analyzer.ts index 14e3a25..d95804d 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/unified-ast-analyzer.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/unified-ast-analyzer.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import { toUnixPath } from '@push-based/utils'; +import { toUnixPath } from '@code-pushup/utils'; import { DependencyInfo, diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component/get-deprecated-css-classes.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/component/get-deprecated-css-classes.tool.ts index ceacbf4..1e5dece 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component/get-deprecated-css-classes.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component/get-deprecated-css-classes.tool.ts @@ -33,7 +33,7 @@ export const getDeprecatedCssClassesHandler = createHandler< 'Missing ds.deprecatedCssClassesPath. Provide --ds.deprecatedCssClassesPath in mcp.json file.', ); } - return getDeprecatedCssClasses( + return await getDeprecatedCssClasses( componentName, deprecatedCssClassesPath, cwd, diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component/utils/deprecated-css-helpers.ts b/packages/angular-mcp-server/src/lib/tools/ds/component/utils/deprecated-css-helpers.ts index 124ba69..80add16 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component/utils/deprecated-css-helpers.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component/utils/deprecated-css-helpers.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path.js'; +import { loadDefaultExport } from '@push-based/utils'; export interface DeprecatedCssComponent { componentName: string; @@ -14,11 +15,11 @@ export interface DeprecatedCssComponent { * @returns Array of deprecated CSS classes for the component * @throws Error if file not found, invalid format, or component not found */ -export function getDeprecatedCssClasses( +export async function getDeprecatedCssClasses( componentName: string, deprecatedCssClassesPath: string, cwd: string, -): string[] { +): Promise { if ( !deprecatedCssClassesPath || typeof deprecatedCssClassesPath !== 'string' @@ -31,11 +32,8 @@ export function getDeprecatedCssClasses( throw new Error(`File not found at deprecatedCssClassesPath: ${absPath}`); } - const module = require(absPath); - - // Handle both ES modules (export default) and CommonJS (module.exports) - // Support: export default dsComponents, module.exports = { dsComponents }, or direct module.exports = dsComponents - const dsComponents = module.default || module.dsComponents || module; + const dsComponents = + await loadDefaultExport(absPath); if (!Array.isArray(dsComponents)) { throw new Error('Invalid export: expected dsComponents to be an array'); diff --git a/packages/angular-mcp-server/src/lib/tools/ds/component/utils/metadata-helpers.ts b/packages/angular-mcp-server/src/lib/tools/ds/component/utils/metadata-helpers.ts index 9402e69..824bddb 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/component/utils/metadata-helpers.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/component/utils/metadata-helpers.ts @@ -1,7 +1,7 @@ -import { validateComponentName } from '../../shared/utils/component-validation'; -import { getDeprecatedCssClasses } from './deprecated-css-helpers'; -import { getComponentDocs } from './doc-helpers'; -import { getComponentPathsInfo } from './paths-helpers'; +import { validateComponentName } from '../../shared/utils/component-validation.js'; +import { getDeprecatedCssClasses } from './deprecated-css-helpers.js'; +import { getComponentDocs } from './doc-helpers.js'; +import { getComponentPathsInfo } from './paths-helpers.js'; export interface ComponentMetadataParams { componentName: string; @@ -35,7 +35,7 @@ export async function analyzeComponentMetadata( params.componentName, storybookDocsRoot, ); - const deprecatedCssClassesList = getDeprecatedCssClasses( + const deprecatedCssClassesList = await getDeprecatedCssClasses( params.componentName, deprecatedCssClassesPath, cwd, diff --git a/packages/angular-mcp-server/src/lib/tools/ds/ds.tools.ts b/packages/angular-mcp-server/src/lib/tools/ds/ds.tools.ts index 579a6da..9304f6b 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/ds.tools.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/ds.tools.ts @@ -4,7 +4,7 @@ import { } from './shared/utils/handler-helpers.js'; import { ToolSchemaOptions } from '@push-based/models'; import { dsComponentCoveragePlugin } from '@push-based/ds-component-coverage'; -import { baseToolsSchema } from '../schema'; +import { baseToolsSchema } from '../schema.js'; import { join } from 'node:path'; import { reportViolationsTools, @@ -88,7 +88,10 @@ export const componentCoverageHandler = createHandler< }); const { executePlugin } = await import('@code-pushup/core'); - const result = await executePlugin(pluginConfig as any); + const result = await executePlugin(pluginConfig as any, { + cache: { read: false, write: false }, + persist: { outputDir: '' }, + }); const reducedResult = { ...result, audits: result.audits.filter(({ score }) => score < 1), diff --git a/packages/angular-mcp-server/src/lib/tools/ds/project/report-deprecated-css.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/project/report-deprecated-css.tool.ts index 5af3b82..7e6c569 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/project/report-deprecated-css.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/project/report-deprecated-css.tool.ts @@ -48,7 +48,7 @@ export const reportDeprecatedCssHandler = createHandler< ); } - const deprecated = getDeprecatedCssClasses( + const deprecated = await getDeprecatedCssClasses( componentName, deprecatedCssClassesPath, cwd, diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/index.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/index.ts index 0ea9e88..f68803d 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/index.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/index.ts @@ -1,5 +1,5 @@ -export { reportViolationsTools } from './report-violations.tool'; -export { reportAllViolationsTools } from './report-all-violations.tool'; +export { reportViolationsTools } from './report-violations.tool.js'; +export { reportAllViolationsTools } from './report-all-violations.tool.js'; export type { ReportViolationsOptions, diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/cross-platform-path.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/cross-platform-path.ts index d56e39a..3c56979 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/cross-platform-path.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/utils/cross-platform-path.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as fs from 'fs'; -import { toUnixPath } from '@push-based/utils'; +import { toUnixPath } from '@code-pushup/utils'; /** * Enhanced path resolution with workspace root context for better debugging diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/base-analyzer.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/base-analyzer.ts index 11df2fa..08d17cb 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/base-analyzer.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/base-analyzer.ts @@ -35,9 +35,9 @@ export async function analyzeViolationsBase( ); } - const deprecatedCssClasses = getDeprecatedCssClasses( + const deprecatedCssClasses = await getDeprecatedCssClasses( componentName, - deprecatedCssClassesPath || '', + deprecatedCssClassesPath, cwd, ); diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts index dd58177..fe87133 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts @@ -42,7 +42,10 @@ export async function executeCoveragePlugin( }); const { executePlugin } = await import('@code-pushup/core'); - return (await executePlugin(pluginConfig as any)) as BaseViolationResult; + return (await executePlugin(pluginConfig as any, { + cache: { read: false, write: false }, + persist: { outputDir: '' }, + })) as BaseViolationResult; } /** diff --git a/packages/angular-mcp-server/src/lib/tools/ds/tools.ts b/packages/angular-mcp-server/src/lib/tools/ds/tools.ts index e0ff273..5690766 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/tools.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/tools.ts @@ -2,18 +2,18 @@ import { ToolsConfig } from '@push-based/models'; import { reportViolationsTools, reportAllViolationsTools, -} from './report-violations'; -import { getProjectDependenciesTools } from './project/get-project-dependencies.tool'; -import { reportDeprecatedCssTools } from './project/report-deprecated-css.tool'; -import { buildComponentUsageGraphTools } from './component-usage-graph'; -import { getDsComponentDataTools } from './component/get-ds-component-data.tool'; -import { listDsComponentsTools } from './component/list-ds-components.tool'; -import { getDeprecatedCssClassesTools } from './component/get-deprecated-css-classes.tool'; +} from './report-violations/index.js'; +import { getProjectDependenciesTools } from './project/get-project-dependencies.tool.js'; +import { reportDeprecatedCssTools } from './project/report-deprecated-css.tool.js'; +import { buildComponentUsageGraphTools } from './component-usage-graph/index.js'; +import { getDsComponentDataTools } from './component/get-ds-component-data.tool.js'; +import { listDsComponentsTools } from './component/list-ds-components.tool.js'; +import { getDeprecatedCssClassesTools } from './component/get-deprecated-css-classes.tool.js'; import { buildComponentContractTools, diffComponentContractTools, listComponentContractsTools, -} from './component-contract'; +} from './component-contract/index.js'; export const dsTools: ToolsConfig[] = [ // Project tools diff --git a/packages/angular-mcp-server/src/lib/tools/tools.ts b/packages/angular-mcp-server/src/lib/tools/tools.ts index 7df09a9..9f9dcb4 100644 --- a/packages/angular-mcp-server/src/lib/tools/tools.ts +++ b/packages/angular-mcp-server/src/lib/tools/tools.ts @@ -1,4 +1,4 @@ import { ToolsConfig } from '@push-based/models'; -import { dsTools } from './ds/tools'; +import { dsTools } from './ds/tools.js'; export const TOOLS: ToolsConfig[] = [...dsTools] as const; diff --git a/packages/angular-mcp-server/src/lib/validation/ds-components-file-loader.validation.ts b/packages/angular-mcp-server/src/lib/validation/ds-components-file-loader.validation.ts index 49e130b..f14bd34 100644 --- a/packages/angular-mcp-server/src/lib/validation/ds-components-file-loader.validation.ts +++ b/packages/angular-mcp-server/src/lib/validation/ds-components-file-loader.validation.ts @@ -1,11 +1,11 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { pathToFileURL } from 'node:url'; import { DsComponentsArraySchema, DsComponentSchema, -} from './ds-components.schema'; +} from './ds-components.schema.js'; import { z } from 'zod'; +import { loadDefaultExport } from '@push-based/utils'; export type DsComponent = z.infer; export type DsComponentsArray = z.infer; @@ -21,12 +21,10 @@ export function validateDsComponent(rawComponent: unknown): DsComponent { } export function validateDsComponentsArray(rawData: unknown): DsComponentsArray { - // First check if it's an array if (!Array.isArray(rawData)) { throw new Error(`Expected array of components, received ${typeof rawData}`); } - // Validate each component individually for better error messages const validatedComponents: DsComponent[] = []; for (let i = 0; i < rawData.length; i++) { try { @@ -37,7 +35,6 @@ export function validateDsComponentsArray(rawData: unknown): DsComponentsArray { } } - // Final validation with the array schema for completeness const arrayValidation = DsComponentsArraySchema.safeParse(validatedComponents); if (!arrayValidation.success) { @@ -68,13 +65,8 @@ export async function loadAndValidateDsComponentsFile( } try { - const fileUrl = pathToFileURL(absPath).toString(); - const module = await import(fileUrl); + const rawData = await loadDefaultExport(absPath); - // Handle both ES modules (export default) and CommonJS (module.exports) - const rawData = module.default || module.dsComponents || module; - - // Use the granular validation functions return validateDsComponentsArray(rawData); } catch (ctx) { if ( @@ -83,7 +75,7 @@ export async function loadAndValidateDsComponentsFile( ctx.message.includes('Expected array of components') || ctx.message.includes('Component at index')) ) { - throw ctx; // Re-throw validation errors as-is + throw ctx; } throw new Error( `Failed to load configuration file: ${(ctx as Error).message}`, diff --git a/packages/angular-mcp-server/src/lib/validation/ds-components-file.validation.ts b/packages/angular-mcp-server/src/lib/validation/ds-components-file.validation.ts index 45bd857..dc48008 100644 --- a/packages/angular-mcp-server/src/lib/validation/ds-components-file.validation.ts +++ b/packages/angular-mcp-server/src/lib/validation/ds-components-file.validation.ts @@ -1,7 +1,7 @@ import * as path from 'path'; -import { pathToFileURL } from 'url'; -import { AngularMcpServerOptions } from './angular-mcp-server-options.schema'; -import { DsComponentsArraySchema } from './ds-components.schema'; +import { AngularMcpServerOptions } from './angular-mcp-server-options.schema.js'; +import { DsComponentsArraySchema } from './ds-components.schema.js'; +import { loadDefaultExport } from '@push-based/utils'; export async function validateDeprecatedCssClassesFile( config: AngularMcpServerOptions, @@ -16,25 +16,8 @@ export async function validateDeprecatedCssClassesFile( relPath, ); - let dsComponents; - try { - const fileUrl = pathToFileURL(deprecatedCssClassesAbsPath).toString(); - const module = await import(fileUrl); + const dsComponents = await loadDefaultExport(deprecatedCssClassesAbsPath); - // Handle both ES modules (export default) and CommonJS (module.exports) - dsComponents = module.default || module.dsComponents || module; - } catch (ctx) { - throw new Error( - `Failed to load deprecated CSS classes configuration file: ${deprecatedCssClassesAbsPath}\n\n` + - `Possible causes:\n` + - `- File does not exist\n` + - `- Invalid JavaScript syntax\n` + - `- File permission issues\n\n` + - `Original error: ${ctx}`, - ); - } - - // Validate the schema const validation = DsComponentsArraySchema.safeParse(dsComponents); if (!validation.success) { const actualType = Array.isArray(dsComponents) diff --git a/packages/angular-mcp-server/src/lib/validation/file-existence.ts b/packages/angular-mcp-server/src/lib/validation/file-existence.ts index 6ad8253..f61f702 100644 --- a/packages/angular-mcp-server/src/lib/validation/file-existence.ts +++ b/packages/angular-mcp-server/src/lib/validation/file-existence.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { AngularMcpServerOptions } from './angular-mcp-server-options.schema'; +import { AngularMcpServerOptions } from './angular-mcp-server-options.schema.js'; export function validateAngularMcpServerFilesExist( config: AngularMcpServerOptions, diff --git a/packages/angular-mcp/webpack.config.js b/packages/angular-mcp/webpack.config.cjs similarity index 100% rename from packages/angular-mcp/webpack.config.js rename to packages/angular-mcp/webpack.config.cjs diff --git a/packages/minimal-repo/packages/design-system/component-options.js b/packages/minimal-repo/packages/design-system/component-options.js index 2998940..ae331f0 100644 --- a/packages/minimal-repo/packages/design-system/component-options.js +++ b/packages/minimal-repo/packages/design-system/component-options.js @@ -68,4 +68,4 @@ const dsComponents = [ }, ]; -module.exports = dsComponents; +export default dsComponents; diff --git a/packages/shared/DEPENDENCIES.md b/packages/shared/DEPENDENCIES.md index 681d4b3..82e2ac3 100644 --- a/packages/shared/DEPENDENCIES.md +++ b/packages/shared/DEPENDENCIES.md @@ -8,8 +8,8 @@ This document provides an AI-friendly overview of the shared libraries in the `/ #### `@push-based/models` -- **Purpose**: Core types, interfaces, and Zod schemas for the entire ecosystem -- **Key Exports**: Issue, AuditOutput, PluginConfig, CategoryConfig, ToolSchemaOptions +- **Purpose**: Core types and interfaces for CLI and MCP tooling +- **Key Exports**: CliArgsObject, ArgumentValue, ToolSchemaOptions, ToolsConfig, ToolHandlerContentResult, DiagnosticsAware - **Dependencies**: None (foundation library) - **Used By**: All other shared libraries @@ -25,7 +25,7 @@ This document provides an AI-friendly overview of the shared libraries in the `/ #### `@push-based/utils` - **Purpose**: General utility functions and file system operations -- **Key Exports**: toUnixPath, slugify, pluralize, findFilesWithPattern, resolveFile +- **Key Exports**: findFilesWithPattern, resolveFile - **Dependencies**: models - **Used By**: angular-ast-utils, ds-component-coverage @@ -36,13 +36,6 @@ This document provides an AI-friendly overview of the shared libraries in the `/ - **Dependencies**: models - **Used By**: angular-ast-utils, ds-component-coverage -#### `@push-based/angular-cli-utils` - -- **Purpose**: Angular CLI schema transformation for MCP tools -- **Key Exports**: transformSchemaToMCPParameters, generateMcpSchemaForEachSchematic -- **Dependencies**: models -- **Used By**: None (standalone utility) - ### Advanced Layer (Multiple Dependencies) #### `@push-based/angular-ast-utils` @@ -70,19 +63,18 @@ This document provides an AI-friendly overview of the shared libraries in the `/ ## Dependency Graph ``` -models (foundation) -├── utils +@code-pushup/models (foundation) +├── @code-pushup/utils ├── styles-ast-utils -├── angular-cli-utils └── angular-ast-utils - ├── models - ├── utils + ├── @code-pushup/models + ├── @code-pushup/utils ├── typescript-ast-utils └── styles-ast-utils ds-component-coverage (most complex) -├── models -├── utils +├── @code-pushup/models +├── @code-pushup/utils ├── styles-ast-utils └── angular-ast-utils ``` @@ -92,7 +84,7 @@ ds-component-coverage (most complex) Based on dependencies, the correct build order is: 1. **Foundation**: `models`, `typescript-ast-utils` -2. **Intermediate**: `utils`, `styles-ast-utils`, `angular-cli-utils` +2. **Intermediate**: `utils`, `styles-ast-utils` 3. **Advanced**: `angular-ast-utils` 4. **Top-level**: `ds-component-coverage` @@ -122,22 +114,21 @@ Based on dependencies, the correct build order is: ### When to Use Each Library -- **models**: When you need type definitions, schemas, or interfaces +- **models**: When you need CLI argument types or MCP tooling interfaces - **utils**: For file operations, string manipulation, or general utilities - **typescript-ast-utils**: For TypeScript code analysis and manipulation - **styles-ast-utils**: For CSS/SCSS parsing and analysis - **angular-ast-utils**: For Angular component analysis and template/style processing -- **angular-cli-utils**: For Angular CLI schema transformations - **ds-component-coverage**: For Design System migration analysis and reporting ### Common Import Patterns ```typescript // Foundation types -import { Issue, AuditOutput } from '@push-based/models'; +import { CliArgsObject, ToolSchemaOptions, DiagnosticsAware } from '@push-based/models'; // File operations -import { resolveFile, findFilesWithPattern } from '@push-based/utils'; +import { resolveFile, findFilesWithPattern } from '@code-pushup/utils'; // Angular component parsing import { @@ -157,7 +148,7 @@ import { ### Integration Points -- All libraries use `models` for consistent type definitions +- All libraries use `models` for CLI and MCP tooling type definitions - File operations flow through `utils` - AST operations are specialized by language (TS, CSS, Angular) - Complex analysis combines multiple AST utilities through `angular-ast-utils` diff --git a/packages/shared/LLMS.md b/packages/shared/LLMS.md index e0e3529..d2b6ef2 100644 --- a/packages/shared/LLMS.md +++ b/packages/shared/LLMS.md @@ -24,7 +24,7 @@ Each library provides three types of AI documentation: ## 🏗️ Foundation Layer -### @push-based/models +### @code-pushup/models Core types, interfaces, and Zod schemas for the entire ecosystem. @@ -42,7 +42,7 @@ TypeScript AST parsing and manipulation utilities. ## 🔧 Intermediate Layer -### @push-based/utils +### @code-pushup/utils General utility functions and file system operations. @@ -58,14 +58,6 @@ CSS/SCSS AST parsing and manipulation utilities. - [📖 API Overview](./styles-ast-utils/ai/API.md) - [💡 Examples](./styles-ast-utils/ai/EXAMPLES.md) -### @push-based/angular-cli-utils - -Angular CLI schema transformation for MCP tools. - -- [🔍 Functions Reference](./angular-cli-utils/ai/FUNCTIONS.md) -- [📖 API Overview](./angular-cli-utils/ai/API.md) -- [💡 Examples](./angular-cli-utils/ai/EXAMPLES.md) - ## 🚀 Advanced Layer ### @push-based/angular-ast-utils @@ -106,7 +98,6 @@ Design System component usage analysis and coverage reporting. ### Angular Development - [angular-ast-utils/EXAMPLES.md](./angular-ast-utils/ai/EXAMPLES.md) - Component parsing and analysis -- [angular-cli-utils/EXAMPLES.md](./angular-cli-utils/ai/EXAMPLES.md) - CLI schema transformations ### Design System Analysis @@ -137,7 +128,6 @@ All shared libraries have complete AI documentation: | typescript-ast-utils | ✅ | ✅ | ✅ | Complete | | utils | ✅ | ✅ | ✅ | Complete | | styles-ast-utils | ✅ | ✅ | ✅ | Complete | -| angular-cli-utils | ✅ | ✅ | ✅ | Complete | | angular-ast-utils | ✅ | ✅ | ✅ | Complete | | ds-component-coverage | ✅ | ✅ | ✅ | Complete | diff --git a/packages/shared/angular-ast-utils/jest.config.ts b/packages/shared/angular-ast-utils/jest.config.ts index 318086c..9a743c2 100644 --- a/packages/shared/angular-ast-utils/jest.config.ts +++ b/packages/shared/angular-ast-utils/jest.config.ts @@ -10,7 +10,7 @@ swcJestConfig.swcrc = false; export default { displayName: '@push-based/angular-ast-utils', - preset: '../../../jest.preset.js', + preset: '../../../jest.preset.mjs', testEnvironment: 'node', transform: { '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], diff --git a/packages/shared/angular-ast-utils/package.json b/packages/shared/angular-ast-utils/package.json index 1833e48..38f7a85 100644 --- a/packages/shared/angular-ast-utils/package.json +++ b/packages/shared/angular-ast-utils/package.json @@ -2,7 +2,7 @@ "name": "@push-based/angular-ast-utils", "version": "0.0.1", "private": true, - "type": "commonjs", + "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/shared/angular-ast-utils/src/lib/parse-component.ts b/packages/shared/angular-ast-utils/src/lib/parse-component.ts index 7af1092..068a350 100644 --- a/packages/shared/angular-ast-utils/src/lib/parse-component.ts +++ b/packages/shared/angular-ast-utils/src/lib/parse-component.ts @@ -1,4 +1,4 @@ -import { toUnixPath } from '@push-based/utils'; +import { toUnixPath } from '@code-pushup/utils'; import * as ts from 'typescript'; import { classDecoratorVisitor } from './decorator-config.visitor.js'; diff --git a/packages/shared/angular-ast-utils/src/lib/styles/utils.ts b/packages/shared/angular-ast-utils/src/lib/styles/utils.ts index 447cb99..4c77abf 100644 --- a/packages/shared/angular-ast-utils/src/lib/styles/utils.ts +++ b/packages/shared/angular-ast-utils/src/lib/styles/utils.ts @@ -1,5 +1,5 @@ import type { Root } from 'postcss'; -import { Issue } from '@push-based/models'; +import { Issue } from '@code-pushup/models'; import { Asset, ParsedComponent } from '../types.js'; export async function visitComponentStyles( diff --git a/packages/shared/angular-ast-utils/src/lib/template/utils.ts b/packages/shared/angular-ast-utils/src/lib/template/utils.ts index 23d8390..bcb950e 100644 --- a/packages/shared/angular-ast-utils/src/lib/template/utils.ts +++ b/packages/shared/angular-ast-utils/src/lib/template/utils.ts @@ -5,7 +5,7 @@ import type { ParseSourceSpan, } from '@angular/compiler' with { 'resolution-mode': 'import' }; -import { Issue } from '@push-based/models'; +import { Issue } from '@code-pushup/models'; import { Asset, ParsedComponent } from '../types.js'; /** diff --git a/packages/shared/angular-ast-utils/src/lib/types.ts b/packages/shared/angular-ast-utils/src/lib/types.ts index 8662156..0b6d2ae 100644 --- a/packages/shared/angular-ast-utils/src/lib/types.ts +++ b/packages/shared/angular-ast-utils/src/lib/types.ts @@ -1,7 +1,7 @@ import { Root } from 'postcss'; import type { ParsedTemplate } from '@angular/compiler' with { 'resolution-mode': 'import' }; import { z } from 'zod'; -import { AngularUnitSchema } from './schema'; +import { AngularUnitSchema } from './schema.js'; export type Asset = SourceLink & { parse: () => Promise; diff --git a/packages/shared/angular-ast-utils/src/lib/utils.ts b/packages/shared/angular-ast-utils/src/lib/utils.ts index 18547d2..1a506de 100644 --- a/packages/shared/angular-ast-utils/src/lib/utils.ts +++ b/packages/shared/angular-ast-utils/src/lib/utils.ts @@ -47,7 +47,7 @@ export function assetFromPropertyArrayInitializer( } import { findFilesWithPattern } from '@push-based/utils'; -import { parseComponents } from './parse-component'; +import { parseComponents } from './parse-component.js'; const unitToSearchPattern = { component: '@Component', diff --git a/packages/shared/angular-ast-utils/tsconfig.json b/packages/shared/angular-ast-utils/tsconfig.json index f21f9f3..deb1b92 100644 --- a/packages/shared/angular-ast-utils/tsconfig.json +++ b/packages/shared/angular-ast-utils/tsconfig.json @@ -4,16 +4,13 @@ "include": [], "references": [ { - "path": "../utils" - }, - { - "path": "../styles-ast-utils" + "path": "../typescript-ast-utils" }, { - "path": "../typescript-ast-utils" + "path": "../utils" }, { - "path": "../models" + "path": "../styles-ast-utils" }, { "path": "./tsconfig.lib.json" diff --git a/packages/shared/angular-ast-utils/tsconfig.lib.json b/packages/shared/angular-ast-utils/tsconfig.lib.json index bf2858f..fd78a20 100644 --- a/packages/shared/angular-ast-utils/tsconfig.lib.json +++ b/packages/shared/angular-ast-utils/tsconfig.lib.json @@ -12,16 +12,13 @@ "include": ["src/**/*.ts"], "references": [ { - "path": "../utils/tsconfig.lib.json" - }, - { - "path": "../styles-ast-utils/tsconfig.lib.json" + "path": "../typescript-ast-utils/tsconfig.lib.json" }, { - "path": "../typescript-ast-utils/tsconfig.lib.json" + "path": "../utils/tsconfig.lib.json" }, { - "path": "../models/tsconfig.lib.json" + "path": "../styles-ast-utils/tsconfig.lib.json" } ], "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] diff --git a/packages/shared/angular-cli-utils/.spec.swcrc b/packages/shared/angular-cli-utils/.spec.swcrc deleted file mode 100644 index 3b52a53..0000000 --- a/packages/shared/angular-cli-utils/.spec.swcrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "jsc": { - "target": "es2017", - "parser": { - "syntax": "typescript", - "decorators": true, - "dynamicImport": true - }, - "transform": { - "decoratorMetadata": true, - "legacyDecorator": true - }, - "keepClassNames": true, - "externalHelpers": true, - "loose": true - }, - "module": { - "type": "es6" - }, - "sourceMaps": true, - "exclude": [] -} diff --git a/packages/shared/angular-cli-utils/README.md b/packages/shared/angular-cli-utils/README.md deleted file mode 100644 index 2ac0023..0000000 --- a/packages/shared/angular-cli-utils/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# angular-cli-utils - -This library was generated with [Nx](https://nx.dev). - -## Building - -Run `nx build angular-cli-utils` to build the library. - -## Running unit tests - -Run `nx test angular-cli-utils` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/packages/shared/angular-cli-utils/ai/API.md b/packages/shared/angular-cli-utils/ai/API.md deleted file mode 100644 index 2f1a626..0000000 --- a/packages/shared/angular-cli-utils/ai/API.md +++ /dev/null @@ -1,28 +0,0 @@ -# Angular CLI Utils - -Small, zero‑dependency helpers for **transforming Angular CLI schemas** into Model Context Protocol (MCP) tool parameters. - -## Minimal usage - -```ts -import { generateMcpSchemaForEachSchematic } from '@push-based/angular-cli-utils'; - -const schemas = generateMcpSchemaForEachSchematic(); -console.log(schemas.component.name); // → 'angular_component_generator' -``` - -## Key Features - -- **Schema Transformation**: Convert Angular CLI schemas to MCP parameter format -- **Bulk Processing**: Generate MCP schemas for all Angular CLI schematics -- **Type Safety**: Full TypeScript support with proper type definitions -- **MCP Integration**: Seamless integration with MCP tool systems -- **Angular CLI Support**: Built-in support for all Angular CLI schematics -- **Automatic Generation**: Generate MCP schemas for all available schematics - -## Documentation map - -| Doc | What you'll find | -| ------------------------------ | ------------------------------------------- | -| [FUNCTIONS.md](./FUNCTIONS.md) | A–Z quick reference for every public symbol | -| [EXAMPLES.md](./EXAMPLES.md) | Runnable scenarios with expected output | diff --git a/packages/shared/angular-cli-utils/ai/EXAMPLES.md b/packages/shared/angular-cli-utils/ai/EXAMPLES.md deleted file mode 100644 index 315d865..0000000 --- a/packages/shared/angular-cli-utils/ai/EXAMPLES.md +++ /dev/null @@ -1,128 +0,0 @@ -# Examples - -## 1 — Transforming a single schema - -> Convert an Angular CLI schema definition to MCP tool parameters format. - -```ts -import { transformSchemaToMCPParameters } from '@push-based/angular-cli-utils'; - -const schema = { - title: 'Angular Component Options Schema', - type: 'object', - description: 'Creates a new Angular component', - additionalProperties: false, - properties: { - name: { type: 'string', description: 'The name of the component' }, - standalone: { - type: 'boolean', - description: 'Whether the component is standalone', - default: true, - }, - }, -}; - -const mcpSchema = transformSchemaToMCPParameters(schema); -console.log(mcpSchema.name); // → 'angular_component_generator' -``` - ---- - -## 2 — Generating all Angular CLI schemas - -> Generate MCP schemas for all available Angular CLI schematics at once. - -```ts -import { generateMcpSchemaForEachSchematic } from '@push-based/angular-cli-utils'; - -const allSchemas = generateMcpSchemaForEachSchematic(); -console.log(Object.keys(allSchemas)); // → ['component', 'service', 'module', ...] -``` - ---- - -## 3 — Accessing specific schematic schemas - -> Get MCP schemas for specific Angular CLI schematics. - -```ts -import { generateMcpSchemaForEachSchematic } from '@push-based/angular-cli-utils'; - -const schemas = generateMcpSchemaForEachSchematic(); -const componentSchema = schemas.component; -const serviceSchema = schemas.service; - -console.log(componentSchema.description); // → 'Creates a new Angular component' -console.log(serviceSchema.name); // → 'angular_service_generator' -``` - ---- - -## 4 — Building MCP tool configurations - -> Create MCP tool configurations from Angular CLI schemas for use in MCP servers. - -```ts -import { generateMcpSchemaForEachSchematic } from '@push-based/angular-cli-utils'; -import { ToolsConfig } from '@push-based/models'; - -const schemas = generateMcpSchemaForEachSchematic(); - -const toolsConfig: ToolsConfig[] = Object.entries(schemas).map( - ([name, schema]) => ({ - schema, - handler: async (args) => { - // Execute Angular CLI command - return { content: [{ type: 'text', text: `Generated ${name}` }] }; - }, - }) -); - -console.log(`Created ${toolsConfig.length} MCP tools`); -``` - ---- - -## 5 — Filtering schemas by type - -> Filter Angular CLI schemas to only include specific schematic types. - -```ts -import { generateMcpSchemaForEachSchematic } from '@push-based/angular-cli-utils'; - -const allSchemas = generateMcpSchemaForEachSchematic(); -const commonSchematics = [ - 'component', - 'service', - 'module', - 'directive', - 'pipe', -]; - -const filteredSchemas = Object.entries(allSchemas) - .filter(([name]) => commonSchematics.includes(name)) - .reduce((acc, [name, schema]) => ({ ...acc, [name]: schema }), {}); - -console.log(Object.keys(filteredSchemas)); // → ['component', 'service', 'module', 'directive', 'pipe'] -``` - ---- - -## 6 — Inspecting schema properties - -> Examine the properties and requirements of generated MCP schemas. - -```ts -import { generateMcpSchemaForEachSchematic } from '@push-based/angular-cli-utils'; - -const schemas = generateMcpSchemaForEachSchematic(); -const componentSchema = schemas.component; - -Object.entries(componentSchema.inputSchema.properties).forEach( - ([prop, config]) => { - console.log(`${prop}: ${config.type} (required: ${config.required})`); - } -); -``` - -These examples demonstrate the flexibility and power of the `@push-based/angular-cli-utils` library for various use cases, from simple schema transformations to complex MCP server integrations. diff --git a/packages/shared/angular-cli-utils/ai/FUNCTIONS.md b/packages/shared/angular-cli-utils/ai/FUNCTIONS.md deleted file mode 100644 index 1506559..0000000 --- a/packages/shared/angular-cli-utils/ai/FUNCTIONS.md +++ /dev/null @@ -1,8 +0,0 @@ -# Public API — Quick Reference - -| Symbol | Kind | Signature | Summary | -| ----------------------------------- | --------- | ----------------------------------------------------------------------------- | ----------------------------------------------------- | -| `generateMcpSchemaForEachSchematic` | function | `generateMcpSchemaForEachSchematic(): Record` | Generate MCP schemas for all Angular CLI schematics | -| `SchemaDefinition` | interface | `interface SchemaDefinition` | Complete Angular CLI schema definition | -| `SchemaProperty` | interface | `interface SchemaProperty` | Property definition in an Angular CLI schema | -| `transformSchemaToMCPParameters` | function | `transformSchemaToMCPParameters(schema: SchemaDefinition): ToolSchemaOptions` | Transform Angular CLI schema to MCP parameters format | diff --git a/packages/shared/angular-cli-utils/jest.config.ts b/packages/shared/angular-cli-utils/jest.config.ts deleted file mode 100644 index acafcc1..0000000 --- a/packages/shared/angular-cli-utils/jest.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { readFileSync } from 'fs'; - -// Reading the SWC compilation config for the spec files -const swcJestConfig = JSON.parse( - readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8'), -); - -// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves -swcJestConfig.swcrc = false; - -export default { - displayName: '@push-based/angular-cli-utils', - preset: '../../../jest.preset.js', - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: 'test-output/jest/coverage', -}; diff --git a/packages/shared/angular-cli-utils/package.json b/packages/shared/angular-cli-utils/package.json deleted file mode 100644 index 1987c92..0000000 --- a/packages/shared/angular-cli-utils/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@push-based/angular-cli-utils", - "version": "0.0.1", - "private": true, - "type": "commonjs", - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - "./package.json": "./package.json", - ".": { - "development": "./src/index.ts", - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "nx": { - "tags": [ - "scope:shared", - "type:lib" - ] - } -} diff --git a/packages/shared/angular-cli-utils/src/index.ts b/packages/shared/angular-cli-utils/src/index.ts deleted file mode 100644 index 715e968..0000000 --- a/packages/shared/angular-cli-utils/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/utils.js'; diff --git a/packages/shared/angular-cli-utils/src/lib/utils.ts b/packages/shared/angular-cli-utils/src/lib/utils.ts deleted file mode 100644 index 2ca8dee..0000000 --- a/packages/shared/angular-cli-utils/src/lib/utils.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { JsonObject } from '@angular-devkit/core'; -import { ToolSchemaOptions } from '@push-based/models'; -import schemaJson from '@angular/cli/lib/config/schema.json'; - -export interface SchemaProperty { - type: string; - description: string; - enum?: string[]; - default?: any; - alias?: string; - format?: string; - visible?: boolean; - oneOf?: JsonObject[]; - $default?: { - $source: string; - index?: number; - }; -} - -export interface SchemaDefinition { - title: string; - type: string; - description: string; - additionalProperties: boolean; - properties: { - [key: string]: SchemaProperty; - }; -} - -/** - * Transforms an Angular CLI schema definition into MCP parameters. - * This is useful for converting Angular schematics options into the standard MCP format. - * - * @param schema - The Angular CLI schema definition to transform - * @returns An array of Parameter objects conforming to the MCP specification - * - * @example - * ```typescript - * const componentSchema = { - * title: "Angular Component Options Schema", - * type: "object", - * description: "Creates a new Angular component", - * properties: { - * name: { - * type: "string", - * description: "The name of the component" - * } - * } - * }; - * - * const parameters = transformSchemaToMCPParameters(componentSchema); - * ``` - */ -export function transformSchemaToMCPParameters( - schema: SchemaDefinition, -): ToolSchemaOptions { - return { - name: anglarSchemaToMcpToolTitle(schema.title), - description: schema.description, - inputSchema: { - type: 'object', - properties: { - ...Object.keys(schema.properties).reduce( - (acc, key) => { - const value = schema.properties[key]; - - const type = mapAngularTypeToMCPType(value.type); - - if (type !== 'array') { - acc[key] = { - type: mapAngularTypeToMCPType(value.type), - description: value.description, - enum: value.enum, - default: value.default?.toString(), - required: !('default' in value) && !value.$default, - } as ToolSchemaOptions['inputSchema']['properties']; - } - // - // if (type === 'object') { - // console.log(value); - // } - - return acc; - }, - {} as Record, - ), - }, - }, - } satisfies ToolSchemaOptions; -} - -/** - * Converts an Angular schema name to a more user-friendly title for MCP tools. - * @example - * - * ```typescript - * const title = anglarSchemaToMcpToolTitle('Angular_Class_Options_Schema'); - * console.log(title); // Output: "Angular Class Generator" - * ``` - */ -function anglarSchemaToMcpToolTitle(schemaName: string): string { - const title = schemaName - .replace(/ Options Schema/g, '') - .toLowerCase() - .replace(' ', '_'); - return `${title}_generator`; -} - -/** - * Maps Angular schema types to MCP parameter types. - * - * @param angularType - The type from the Angular schema - * @returns The corresponding MCP parameter type - */ -function mapAngularTypeToMCPType(angularType: string): string { - const typeMap: Record = { - string: 'string', - number: 'number', - integer: 'integer', - boolean: 'boolean', - array: 'array', - object: 'object', - }; - - return typeMap[angularType] || 'string'; -} - -export function generateMcpSchemaForEachSchematic(): Record< - string, - ToolSchemaOptions -> { - const schematics = schemaJson.definitions.schematicOptions.properties; - const schematicsKeys = Object.keys(schematics) as Array< - keyof typeof schematics - >; - - const toolSchemaOptions: Record = {}; - - for (const key of schematicsKeys) { - const schematicName = schematics[key]['$ref'].replace( - '#/definitions/', - '', - ) as keyof typeof schemaJson.definitions; - toolSchemaOptions[key] = transformSchemaToMCPParameters( - schemaJson.definitions[schematicName] as SchemaDefinition, - ); - } - return toolSchemaOptions; -} diff --git a/packages/shared/angular-cli-utils/tsconfig.json b/packages/shared/angular-cli-utils/tsconfig.json deleted file mode 100644 index 594e125..0000000 --- a/packages/shared/angular-cli-utils/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "../models" - }, - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ], - "nx": { - "addTypecheckTarget": false - } -} diff --git a/packages/shared/angular-cli-utils/tsconfig.lib.json b/packages/shared/angular-cli-utils/tsconfig.lib.json deleted file mode 100644 index aca4bb9..0000000 --- a/packages/shared/angular-cli-utils/tsconfig.lib.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "baseUrl": ".", - "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", - "emitDeclarationOnly": false, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "references": [ - { - "path": "../models/tsconfig.lib.json" - } - ], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/packages/shared/angular-cli-utils/tsconfig.spec.json b/packages/shared/angular-cli-utils/tsconfig.spec.json deleted file mode 100644 index e1fa87f..0000000 --- a/packages/shared/angular-cli-utils/tsconfig.spec.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./out-tsc/jest", - "types": ["jest", "node"], - "forceConsistentCasingInFileNames": true - }, - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ], - "references": [ - { - "path": "./tsconfig.lib.json" - } - ] -} diff --git a/packages/shared/angular-cli-utils/vitest.config.mts b/packages/shared/angular-cli-utils/vitest.config.mts deleted file mode 100644 index 66fb4b2..0000000 --- a/packages/shared/angular-cli-utils/vitest.config.mts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - cacheDir: '../../../node_modules/.vite/angular-cli-utils/unit', - root: __dirname, - test: { - include: ['src/**/*.spec.[jt]s?(x)'], - environment: 'node', - watch: false, - globals: true, - passWithNoTests: true, - testTimeout: 25_000, - - coverage: { - provider: 'v8', - reporter: ['text', 'lcov'], - reportsDirectory: '../../../coverage/angular-cli-utils/unit', - exclude: ['**/mocks/**', '**/types.ts', '**/__snapshots__/**'], - }, - - reporters: ['default'], - }, -}); diff --git a/packages/shared/ds-component-coverage/jest.config.ts b/packages/shared/ds-component-coverage/jest.config.ts index 5cc39ac..5333081 100644 --- a/packages/shared/ds-component-coverage/jest.config.ts +++ b/packages/shared/ds-component-coverage/jest.config.ts @@ -10,7 +10,7 @@ swcJestConfig.swcrc = false; export default { displayName: '@push-based/typescript-ast-utils', - preset: '../../../jest.preset.js', + preset: '../../../jest.preset.mjs', testEnvironment: 'node', transform: { '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], diff --git a/packages/shared/ds-component-coverage/package.json b/packages/shared/ds-component-coverage/package.json index 526f55e..55a5767 100644 --- a/packages/shared/ds-component-coverage/package.json +++ b/packages/shared/ds-component-coverage/package.json @@ -2,7 +2,7 @@ "name": "@push-based/ds-component-coverage", "version": "0.0.1", "private": true, - "type": "commonjs", + "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/shared/ds-component-coverage/src/core.config.ts b/packages/shared/ds-component-coverage/src/core.config.ts index 5c239e2..7438315 100644 --- a/packages/shared/ds-component-coverage/src/core.config.ts +++ b/packages/shared/ds-component-coverage/src/core.config.ts @@ -1,13 +1,13 @@ -import { CategoryConfig, CoreConfig } from '@push-based/models'; +import { CategoryConfig, CoreConfig } from '@code-pushup/models'; import angularDsUsagePlugin, { DsComponentUsagePluginConfig, -} from './lib/ds-component-coverage.plugin'; -import { getAngularDsUsageCategoryRefs } from './lib/utils'; +} from './lib/ds-component-coverage.plugin.js'; +import { getAngularDsUsageCategoryRefs } from './lib/utils.js'; export async function dsComponentUsagePluginCoreConfig({ directory, dsComponents, -}: DsComponentUsagePluginConfig) { +}: DsComponentUsagePluginConfig): Promise { return { plugins: [ angularDsUsagePlugin({ diff --git a/packages/shared/ds-component-coverage/src/index.ts b/packages/shared/ds-component-coverage/src/index.ts index 341221d..0e9d736 100644 --- a/packages/shared/ds-component-coverage/src/index.ts +++ b/packages/shared/ds-component-coverage/src/index.ts @@ -1,17 +1,17 @@ -import { dsComponentCoveragePlugin } from './lib/ds-component-coverage.plugin'; +import { dsComponentCoveragePlugin } from './lib/ds-component-coverage.plugin.js'; export { runnerFunction, type CreateRunnerConfig, -} from './lib/runner/create-runner'; +} from './lib/runner/create-runner.js'; export { dsComponentCoveragePlugin, type DsComponentUsagePluginConfig, -} from './lib/ds-component-coverage.plugin'; -export { getAngularDsUsageCategoryRefs } from './lib/utils'; -export { ANGULAR_DS_USAGE_PLUGIN_SLUG } from './lib/constants'; +} from './lib/ds-component-coverage.plugin.js'; +export { getAngularDsUsageCategoryRefs } from './lib/utils.js'; +export { ANGULAR_DS_USAGE_PLUGIN_SLUG } from './lib/constants.js'; export default dsComponentCoveragePlugin; export type { ComponentReplacement, ComponentReplacementSchema, -} from './lib/runner/audits/ds-coverage/schema'; -export { ComponentCoverageRunnerOptionsSchema } from './lib/runner/schema'; +} from './lib/runner/audits/ds-coverage/schema.js'; +export { ComponentCoverageRunnerOptionsSchema } from './lib/runner/schema.js'; diff --git a/packages/shared/ds-component-coverage/src/lib/ds-component-coverage.plugin.ts b/packages/shared/ds-component-coverage/src/lib/ds-component-coverage.plugin.ts index f077cad..b240965 100644 --- a/packages/shared/ds-component-coverage/src/lib/ds-component-coverage.plugin.ts +++ b/packages/shared/ds-component-coverage/src/lib/ds-component-coverage.plugin.ts @@ -1,7 +1,7 @@ -import { PluginConfig } from '@push-based/models'; -import { CreateRunnerConfig, runnerFunction } from './runner/create-runner'; -import { getAudits } from './utils'; -import { ANGULAR_DS_USAGE_PLUGIN_SLUG } from './constants'; +import { PluginConfig } from '@code-pushup/models'; +import { CreateRunnerConfig, runnerFunction } from './runner/create-runner.js'; +import { getAudits } from './utils.js'; +import { ANGULAR_DS_USAGE_PLUGIN_SLUG } from './constants.js'; export type DsComponentUsagePluginConfig = CreateRunnerConfig; diff --git a/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-definition.utils.ts b/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-definition.utils.ts index dbe0050..c7dd073 100644 --- a/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-definition.utils.ts +++ b/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-definition.utils.ts @@ -1,9 +1,9 @@ -import { Issue } from '@push-based/models'; +import { Issue } from '@code-pushup/models'; import { Asset } from '@push-based/angular-ast-utils'; import { type Root } from 'postcss'; -import { ComponentReplacement } from './schema'; +import { ComponentReplacement } from './schema.js'; import { visitEachStyleNode } from '@push-based/styles-ast-utils'; -import { createClassDefinitionVisitor } from './class-definition.visitor'; +import { createClassDefinitionVisitor } from './class-definition.visitor.js'; export async function getClassDefinitionIssues( componentReplacement: ComponentReplacement, diff --git a/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-definition.visitor.ts b/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-definition.visitor.ts index 495686b..e8601f8 100644 --- a/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-definition.visitor.ts +++ b/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-definition.visitor.ts @@ -1,5 +1,6 @@ import { Rule } from 'postcss'; -import { Issue, DiagnosticsAware } from '@push-based/models'; +import { Issue } from '@code-pushup/models'; +import { DiagnosticsAware } from '@push-based/models'; import { CssAstVisitor, styleAstRuleToSource, @@ -9,8 +10,8 @@ import { EXTERNAL_ASSET_ICON, INLINE_ASSET_ICON, STYLES_ASSET_ICON, -} from './constants'; -import { ComponentReplacement } from './schema'; +} from './constants.js'; +import { ComponentReplacement } from './schema.js'; export type ClassDefinitionVisitor = CssAstVisitor & DiagnosticsAware; diff --git a/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-usage.utils.ts b/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-usage.utils.ts index ce806ba..36633b5 100644 --- a/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-usage.utils.ts +++ b/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-usage.utils.ts @@ -1,6 +1,6 @@ import { Asset, visitEachTmplChild } from '@push-based/angular-ast-utils'; -import { ComponentReplacement } from './schema'; -import { ClassUsageVisitor } from './class-usage.visitor'; +import { ComponentReplacement } from './schema.js'; +import { ClassUsageVisitor } from './class-usage.visitor.js'; import type { ParsedTemplate, TmplAstNode, diff --git a/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-usage.visitor.ts b/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-usage.visitor.ts index 15a6872..8b0d4c0 100644 --- a/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-usage.visitor.ts +++ b/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-usage.visitor.ts @@ -26,7 +26,9 @@ import type { TmplAstVariable, TmplAstVisitor, } from '@angular/compiler' with { 'resolution-mode': 'import' }; -import { Issue, DiagnosticsAware } from '@push-based/models'; +import { Issue } from '@code-pushup/models'; +import { DiagnosticsAware } from '@push-based/models'; + import { tmplAstElementToSource, parseClassNames, @@ -37,9 +39,9 @@ import { EXTERNAL_ASSET_ICON, INLINE_ASSET_ICON, TEMPLATE_ASSET_ICON, -} from './constants'; +} from './constants.js'; -import { ComponentReplacement } from './schema'; +import { ComponentReplacement } from './schema.js'; function generateClassUsageMessage({ element, diff --git a/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/ds-coverage.audit.ts b/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/ds-coverage.audit.ts index e6e6ece..c00c9c4 100644 --- a/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/ds-coverage.audit.ts +++ b/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/ds-coverage.audit.ts @@ -1,5 +1,5 @@ -import { getCompCoverageAuditOutput } from './utils'; -import { AuditOutputs, Issue } from '@push-based/models'; +import { getCompCoverageAuditOutput } from './utils.js'; +import { AuditOutputs, Issue } from '@code-pushup/models'; import { ParsedComponent, visitComponentStyles, @@ -7,9 +7,9 @@ import { Asset, } from '@push-based/angular-ast-utils'; import type { ParsedTemplate } from '@angular/compiler' with { 'resolution-mode': 'import' }; -import { ComponentReplacement } from './schema'; -import { getClassUsageIssues } from './class-usage.utils'; -import { getClassDefinitionIssues } from './class-definition.utils'; +import { ComponentReplacement } from './schema.js'; +import { getClassUsageIssues } from './class-usage.utils.js'; +import { getClassDefinitionIssues } from './class-definition.utils.js'; export function dsCompCoverageAuditOutputs( dsComponents: ComponentReplacement[], diff --git a/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/utils.ts b/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/utils.ts index 660d263..cba9df5 100644 --- a/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/utils.ts +++ b/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/utils.ts @@ -1,6 +1,6 @@ -import { Audit, AuditOutput, Issue } from '@push-based/models'; -import { pluralize, slugify } from '@push-based/utils'; -import { ComponentReplacement } from './schema'; +import { Audit, AuditOutput, Issue } from '@code-pushup/models'; +import { pluralize, slugify } from '@code-pushup/utils'; +import { ComponentReplacement } from './schema.js'; /** * Creates a scored audit output. diff --git a/packages/shared/ds-component-coverage/src/lib/runner/create-runner.ts b/packages/shared/ds-component-coverage/src/lib/runner/create-runner.ts index cbb6ae8..e0ff8a8 100644 --- a/packages/shared/ds-component-coverage/src/lib/runner/create-runner.ts +++ b/packages/shared/ds-component-coverage/src/lib/runner/create-runner.ts @@ -1,7 +1,7 @@ -import { AuditOutputs } from '@push-based/models'; +import { AuditOutputs } from '@code-pushup/models'; import { parseAngularUnit } from '@push-based/angular-ast-utils'; -import { dsCompCoverageAuditOutputs } from './audits/ds-coverage/ds-coverage.audit'; -import { ComponentCoverageRunnerOptions } from './schema'; +import { dsCompCoverageAuditOutputs } from './audits/ds-coverage/ds-coverage.audit.js'; +import { ComponentCoverageRunnerOptions } from './schema.js'; export type CreateRunnerConfig = ComponentCoverageRunnerOptions; diff --git a/packages/shared/ds-component-coverage/src/lib/runner/schema.ts b/packages/shared/ds-component-coverage/src/lib/runner/schema.ts index 46738bb..340520e 100644 --- a/packages/shared/ds-component-coverage/src/lib/runner/schema.ts +++ b/packages/shared/ds-component-coverage/src/lib/runner/schema.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { ComponentReplacementSchema } from './audits/ds-coverage/schema'; +import { ComponentReplacementSchema } from './audits/ds-coverage/schema.js'; export const baseToolsSchema = { inputSchema: { diff --git a/packages/shared/ds-component-coverage/src/lib/utils.ts b/packages/shared/ds-component-coverage/src/lib/utils.ts index e368526..b39dd37 100644 --- a/packages/shared/ds-component-coverage/src/lib/utils.ts +++ b/packages/shared/ds-component-coverage/src/lib/utils.ts @@ -1,7 +1,7 @@ -import { Audit, CategoryRef } from '@push-based/models'; -import { ANGULAR_DS_USAGE_PLUGIN_SLUG } from './constants'; -import { getCompUsageAudits } from './runner/audits/ds-coverage/utils'; -import { ComponentReplacement } from './runner/audits/ds-coverage/schema'; +import { Audit, CategoryRef } from '@code-pushup/models'; +import { ANGULAR_DS_USAGE_PLUGIN_SLUG } from './constants.js'; +import { getCompUsageAudits } from './runner/audits/ds-coverage/utils.js'; +import { ComponentReplacement } from './runner/audits/ds-coverage/schema.js'; export function getAudits( componentReplacements: ComponentReplacement[], diff --git a/packages/shared/ds-component-coverage/tsconfig.json b/packages/shared/ds-component-coverage/tsconfig.json index 1782f28..35a9aca 100644 --- a/packages/shared/ds-component-coverage/tsconfig.json +++ b/packages/shared/ds-component-coverage/tsconfig.json @@ -4,7 +4,7 @@ "include": [], "references": [ { - "path": "../utils" + "path": "../models" }, { "path": "../styles-ast-utils" @@ -12,9 +12,6 @@ { "path": "../angular-ast-utils" }, - { - "path": "../models" - }, { "path": "./tsconfig.lib.json" }, diff --git a/packages/shared/ds-component-coverage/tsconfig.lib.json b/packages/shared/ds-component-coverage/tsconfig.lib.json index 4958943..1f499c6 100644 --- a/packages/shared/ds-component-coverage/tsconfig.lib.json +++ b/packages/shared/ds-component-coverage/tsconfig.lib.json @@ -14,16 +14,13 @@ "include": ["src/**/*.ts"], "references": [ { - "path": "../utils/tsconfig.lib.json" + "path": "../models/tsconfig.lib.json" }, { "path": "../styles-ast-utils/tsconfig.lib.json" }, { "path": "../angular-ast-utils/tsconfig.lib.json" - }, - { - "path": "../models/tsconfig.lib.json" } ], "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] diff --git a/packages/shared/models/.spec.swcrc b/packages/shared/models/.spec.swcrc deleted file mode 100644 index 3b52a53..0000000 --- a/packages/shared/models/.spec.swcrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "jsc": { - "target": "es2017", - "parser": { - "syntax": "typescript", - "decorators": true, - "dynamicImport": true - }, - "transform": { - "decoratorMetadata": true, - "legacyDecorator": true - }, - "keepClassNames": true, - "externalHelpers": true, - "loose": true - }, - "module": { - "type": "es6" - }, - "sourceMaps": true, - "exclude": [] -} diff --git a/packages/shared/models/ai/API.md b/packages/shared/models/ai/API.md index ebdae54..29cf256 100644 --- a/packages/shared/models/ai/API.md +++ b/packages/shared/models/ai/API.md @@ -1,30 +1,41 @@ # Models -Comprehensive **Zod schemas and TypeScript types** for Code PushUp configuration, reports, and data validation. +Simple **TypeScript types** for Angular MCP toolkit shared interfaces and utilities. ## Minimal usage ```ts -import { auditSchema, reportSchema } from '@push-based/models'; - -const audit = auditSchema.parse({ - slug: 'performance-audit', - title: 'Performance Audit', -}); - -const isValidReport = reportSchema.safeParse(reportData).success; +import { type CliArgsObject, type ToolsConfig } from '@push-based/models'; + +// CLI argument types +const args: CliArgsObject = { + directory: './src', + componentName: 'DsButton', + _: ['command'], +}; + +// MCP tool configuration +const toolConfig: ToolsConfig = { + schema: { + name: 'my-tool', + description: 'A custom tool', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + handler: async (request) => { + return { content: [{ type: 'text', text: 'Result' }] }; + }, +}; ``` ## Key Features -- **Zod Schema Validation**: Comprehensive validation schemas for all Code PushUp data structures -- **TypeScript Types**: Full type safety with proper TypeScript definitions -- **Configuration Models**: Schemas for core config, plugins, categories, and groups -- **Report Models**: Complete report structure validation and types -- **Audit Models**: Detailed audit output and metadata schemas -- **MCP Integration**: Model Context Protocol tool schema definitions -- **Table Models**: Flexible table data structures for reports -- **Diff Models**: Report comparison and difference tracking +- **CLI Types**: Type-safe command line argument handling +- **MCP Integration**: Model Context Protocol tool schema definitions and handlers +- **Diagnostics**: Interface for objects that can report issues and diagnostics +- **Lightweight**: Minimal dependencies, focused on essential shared types ## Documentation map diff --git a/packages/shared/models/ai/EXAMPLES.md b/packages/shared/models/ai/EXAMPLES.md index 4d0c1dd..fac14a9 100644 --- a/packages/shared/models/ai/EXAMPLES.md +++ b/packages/shared/models/ai/EXAMPLES.md @@ -1,349 +1,227 @@ # Examples -## 1 — Validating audit data +## 1 — Working with CLI arguments -> Validate audit configuration and output using Zod schemas. +> Type-safe handling of command line arguments. ```ts -import { auditSchema, auditOutputSchema } from '@push-based/models'; - -// Validate audit configuration -const auditConfig = { - slug: 'performance-budget', - title: 'Performance Budget Check', - description: 'Ensures performance metrics stay within budget', - docsUrl: 'https://web.dev/performance-budgets/', +import { type CliArgsObject, type ArgumentValue } from '@push-based/models'; + +// Basic CLI arguments +const args: CliArgsObject = { + directory: './src/components', + componentName: 'DsButton', + groupBy: 'file', + _: ['report-violations'], }; -const validatedAudit = auditSchema.parse(auditConfig); -console.log(validatedAudit.slug); // → 'performance-budget' +console.log(args.directory); // → './src/components' +console.log(args._); // → ['report-violations'] -// Validate audit output -const auditResult = { - slug: 'performance-budget', - score: 0.85, - value: 1200, - displayValue: '1.2s', -}; - -const validatedOutput = auditOutputSchema.parse(auditResult); -console.log(validatedOutput.score); // → 0.85 -``` - ---- - -## 2 — Creating plugin configurations - -> Build and validate plugin configurations with audits and groups. +// Typed CLI arguments with specific structure +interface MyToolArgs { + directory: string; + componentName: string; + groupBy?: 'file' | 'folder'; + verbose?: boolean; +} -```ts -import { pluginConfigSchema, type PluginConfig } from '@push-based/models'; - -const eslintPlugin: PluginConfig = { - slug: 'eslint', - title: 'ESLint', - icon: 'eslint', - runner: { - command: 'npx eslint', - args: ['src/**/*.ts', '--format', 'json'], - outputFile: 'eslint-results.json', - }, - audits: [ - { - slug: 'no-unused-vars', - title: 'No unused variables', - description: 'Disallow unused variables', - }, - { - slug: 'prefer-const', - title: 'Prefer const', - description: - 'Require const declarations for variables that are never reassigned', - }, - ], - groups: [ - { - slug: 'best-practices', - title: 'Best Practices', - refs: [ - { slug: 'no-unused-vars', weight: 1 }, - { slug: 'prefer-const', weight: 1 }, - ], - }, - ], +const typedArgs: CliArgsObject = { + directory: './packages/shared/models', + componentName: 'DsCard', + groupBy: 'folder', + verbose: true, + _: ['analyze'], }; -const validatedPlugin = pluginConfigSchema.parse(eslintPlugin); -console.log(`Plugin: ${validatedPlugin.title}`); // → 'Plugin: ESLint' +console.log(`Analyzing ${typedArgs.componentName} in ${typedArgs.directory}`); +// → 'Analyzing DsCard in ./packages/shared/models' ``` --- -## 3 — Building core configuration +## 2 — Creating MCP tools -> Create a complete Code PushUp configuration with plugins and categories. +> Build Model Context Protocol tools with proper typing. ```ts -import { coreConfigSchema, type CoreConfig } from '@push-based/models'; - -const config: CoreConfig = { - plugins: [ - { - slug: 'lighthouse', - title: 'Lighthouse', - icon: 'lighthouse', - runner: { - command: 'lighthouse', - args: ['https://example.com', '--output=json'], - outputFile: 'lighthouse-report.json', - }, - audits: [ - { slug: 'first-contentful-paint', title: 'First Contentful Paint' }, - { slug: 'largest-contentful-paint', title: 'Largest Contentful Paint' }, - ], - groups: [ - { - slug: 'performance', - title: 'Performance', - refs: [ - { slug: 'first-contentful-paint', weight: 1 }, - { slug: 'largest-contentful-paint', weight: 2 }, - ], +import { type ToolsConfig, type ToolSchemaOptions } from '@push-based/models'; + +// Define a simple MCP tool +const reportViolationsTool: ToolsConfig = { + schema: { + name: 'report-violations', + description: 'Report deprecated DS CSS usage in a directory', + inputSchema: { + type: 'object', + properties: { + directory: { + type: 'string', + description: 'The relative path to the directory to scan', }, - ], - }, - ], - categories: [ - { - slug: 'performance', - title: 'Performance', - refs: [ - { - plugin: 'lighthouse', - slug: 'performance', - type: 'group', - weight: 1, + componentName: { + type: 'string', + description: 'The class name of the component (e.g., DsButton)', }, - ], + groupBy: { + type: 'string', + enum: ['file', 'folder'], + default: 'file', + description: 'How to group the results', + }, + }, + required: ['directory', 'componentName'], }, - ], - persist: { - outputDir: '.code-pushup', - filename: 'report', - format: ['json', 'md'], - }, -}; - -const validatedConfig = coreConfigSchema.parse(config); -console.log(`Categories: ${validatedConfig.categories?.length}`); // → 'Categories: 1' -``` - ---- - -## 4 — Working with reports - -> Parse and validate Code PushUp reports. - -```ts -import { reportSchema, type Report } from '@push-based/models'; - -const report: Report = { - packageName: '@push-based/cli', - version: '1.0.0', - date: new Date().toISOString(), - duration: 45000, - commit: { - hash: 'abcdef0123456789abcdef0123456789abcdef01', - message: 'Add performance optimizations', - author: 'Developer', - date: new Date(), }, - plugins: [ - { - slug: 'lighthouse', - title: 'Lighthouse', - icon: 'lighthouse', - date: new Date().toISOString(), - duration: 30000, - audits: [ + handler: async (request) => { + const { directory, componentName, groupBy = 'file' } = request.params.arguments as { + directory: string; + componentName: string; + groupBy?: 'file' | 'folder'; + }; + + // Tool implementation logic here + const violations = await analyzeViolations(directory, componentName, groupBy); + + return { + content: [ { - slug: 'performance-score', - title: 'Performance Score', - score: 0.92, - value: 92, - displayValue: '92', + type: 'text', + text: `Found ${violations.length} violations in ${directory}`, }, ], - }, - ], + }; + }, }; -const validatedReport = reportSchema.parse(report); -console.log(`Report duration: ${validatedReport.duration}ms`); // → 'Report duration: 45000ms' +// Use the tool configuration +console.log(`Tool: ${reportViolationsTool.schema.name}`); +// → 'Tool: report-violations' ``` --- -## 5 — Creating table data +## 3 — Implementing diagnostics -> Build structured table data for reports. +> Create objects that can report issues and diagnostics. ```ts -import { tableSchema, type Table } from '@push-based/models'; - -const performanceTable: Table = { - title: 'Performance Metrics', - columns: [ - { key: 'metric', label: 'Metric', align: 'left' }, - { key: 'value', label: 'Value', align: 'right' }, - { key: 'threshold', label: 'Threshold', align: 'right' }, - ], - rows: [ - { - metric: 'First Contentful Paint', - value: '1.2s', - threshold: '1.8s', - }, - { - metric: 'Largest Contentful Paint', - value: '2.1s', - threshold: '2.5s', - }, - { - metric: 'Cumulative Layout Shift', - value: '0.05', - threshold: '0.1', - }, - ], -}; +import { type DiagnosticsAware } from '@push-based/models'; + +class ComponentAnalyzer implements DiagnosticsAware { + private issues: Array<{ code?: number; message: string; severity: string }> = []; + + analyze(componentPath: string): void { + // Simulate analysis + if (!componentPath.endsWith('.ts')) { + this.issues.push({ + code: 1001, + message: 'Component file should have .ts extension', + severity: 'error', + }); + } + + if (componentPath.includes('deprecated')) { + this.issues.push({ + code: 2001, + message: 'Component uses deprecated patterns', + severity: 'warning', + }); + } + } + + getIssues() { + return this.issues; + } + + clear(): void { + this.issues = []; + } +} -const validatedTable = tableSchema().parse(performanceTable); -console.log(`Table has ${validatedTable.rows.length} rows`); // → 'Table has 3 rows' +// Usage +const analyzer = new ComponentAnalyzer(); +analyzer.analyze('src/components/deprecated-button.js'); + +const issues = analyzer.getIssues(); +console.log(`Found ${issues.length} issues:`); +issues.forEach((issue) => { + console.log(` ${issue.severity}: ${issue.message} (code: ${issue.code})`); +}); +// → Found 2 issues: +// → error: Component file should have .ts extension (code: 1001) +// → warning: Component uses deprecated patterns (code: 2001) + +analyzer.clear(); +console.log(`Issues after clear: ${analyzer.getIssues().length}`); // → 0 ``` --- -## 6 — Comparing reports +## 4 — Advanced MCP tool with content results -> Create report comparisons and diffs. +> Create sophisticated MCP tools that return structured content. ```ts -import { reportsDiffSchema, type ReportsDiff } from '@push-based/models'; - -const reportsDiff: ReportsDiff = { - packageName: '@push-based/cli', - version: '1.0.0', - date: new Date().toISOString(), - duration: 5000, - commits: { - before: { - hash: 'abc123def456abc123def456abc123def456abc1', - message: 'Previous commit', - author: 'Developer', - date: new Date('2024-01-01'), - }, - after: { - hash: 'def456abc123def456abc123def456abc123def4', - message: 'Current commit', - author: 'Developer', - date: new Date('2024-01-02'), +import { + type ToolsConfig, + type ToolHandlerContentResult, +} from '@push-based/models'; + +const buildComponentContractTool: ToolsConfig = { + schema: { + name: 'build-component-contract', + description: 'Generate a static surface contract for a component', + inputSchema: { + type: 'object', + properties: { + directory: { type: 'string' }, + templateFile: { type: 'string' }, + styleFile: { type: 'string' }, + typescriptFile: { type: 'string' }, + dsComponentName: { type: 'string' }, + }, + required: ['directory', 'templateFile', 'styleFile', 'typescriptFile', 'dsComponentName'], }, }, - categories: { - changed: [ + handler: async (request) => { + const params = request.params.arguments as { + directory: string; + templateFile: string; + styleFile: string; + typescriptFile: string; + dsComponentName: string; + }; + + // Generate contract + const contract = await generateContract(params); + + const content: ToolHandlerContentResult[] = [ { - slug: 'performance', - title: 'Performance', - scores: { - before: 0.85, - after: 0.92, - diff: 0.07, - }, + type: 'text', + text: `Generated contract for ${params.dsComponentName}`, }, - ], - unchanged: [], - added: [], - removed: [], - }, - groups: { - changed: [], - unchanged: [], - added: [], - removed: [], - }, - audits: { - changed: [ { - slug: 'lcp', - title: 'Largest Contentful Paint', - plugin: { - slug: 'lighthouse', - title: 'Lighthouse', - }, - scores: { - before: 0.8, - after: 0.9, - diff: 0.1, - }, - values: { - before: 2500, - after: 2100, - diff: -400, - }, - displayValues: { - before: '2.5s', - after: '2.1s', - }, + type: 'text', + text: `Template inputs: ${contract.templateInputs.length}`, }, - ], - unchanged: [], - added: [], - removed: [], - }, -}; - -const validatedDiff = reportsDiffSchema.parse(reportsDiff); -console.log( - `Performance improved by ${validatedDiff.categories.changed[0]?.scores.diff}` -); -// → 'Performance improved by 0.07' -``` - ---- - -## 7 — Safe parsing with error handling - -> Use safe parsing to handle validation errors gracefully. - -```ts -import { auditSchema, coreConfigSchema } from '@push-based/models'; + { + type: 'text', + text: `Style classes: ${contract.styleClasses.length}`, + }, + ]; -// Safe parsing with error handling -const invalidAudit = { - slug: 'Invalid Slug!', // Invalid: contains spaces and special characters - title: 'Test Audit', + return { content }; + }, }; -const auditResult = auditSchema.safeParse(invalidAudit); -if (!auditResult.success) { - console.log('Validation failed:', auditResult.error.issues[0]?.message); - // → 'Validation failed: The slug has to follow the pattern...' -} else { - console.log('Valid audit:', auditResult.data); +// Mock contract generation function +async function generateContract(params: any) { + return { + templateInputs: ['@Input() label: string', '@Input() disabled: boolean'], + styleClasses: ['.ds-button', '.ds-button--primary'], + }; } - -// Batch validation -const configs = [ - { plugins: [] }, // Invalid: empty plugins array - { plugins: [{ slug: 'test', title: 'Test' }] }, // Invalid: missing required fields -]; - -const validConfigs = configs - .map((config) => coreConfigSchema.safeParse(config)) - .filter((result) => result.success) - .map((result) => result.data); - -console.log(`${validConfigs.length} valid configurations found`); ``` -These examples demonstrate the comprehensive validation capabilities and practical usage patterns of the `@push-based/models` library for various Code PushUp data structures and workflows. +These examples demonstrate the practical usage patterns of the `@push-based/models` library for building type-safe CLI tools, MCP integrations, and diagnostic utilities in the Angular MCP toolkit. diff --git a/packages/shared/models/ai/FUNCTIONS.md b/packages/shared/models/ai/FUNCTIONS.md index b676fa1..5b6ece7 100644 --- a/packages/shared/models/ai/FUNCTIONS.md +++ b/packages/shared/models/ai/FUNCTIONS.md @@ -1,100 +1,10 @@ # Public API — Quick Reference -| Symbol | Kind | Summary | -| ------------------------------- | --------- | --------------------------------------------------------- | -| `ArgumentValue` | type | CLI argument value types (number, string, boolean, array) | -| `Audit` | type | Audit metadata and configuration | -| `AuditDetails` | type | Detailed audit information with issues and tables | -| `AuditDiff` | type | Audit comparison between two reports | -| `AuditOutput` | type | Audit execution result with score and value | -| `AuditOutputs` | type | Array of audit outputs from plugin execution | -| `AuditReport` | type | Combined audit metadata and output | -| `AuditResult` | type | Audit result in report comparison | -| `CategoryConfig` | type | Category configuration with weighted references | -| `CategoryDiff` | type | Category comparison between two reports | -| `CategoryRef` | type | Weighted reference to audit or group in category | -| `CategoryResult` | type | Category result in report comparison | -| `CliArgsObject` | type | CLI arguments object with typed values | -| `Commit` | type | Git commit information | -| `CoreConfig` | type | Main Code PushUp configuration | -| `DiagnosticsAware` | interface | Interface for objects that can report issues | -| `Format` | type | Report output format (json, md) | -| `Group` | type | Group configuration with audit references | -| `GroupDiff` | type | Group comparison between two reports | -| `GroupMeta` | type | Group metadata information | -| `GroupRef` | type | Weighted reference to group | -| `GroupResult` | type | Group result in report comparison | -| `Issue` | type | Issue information with severity and source location | -| `IssueSeverity` | type | Issue severity level (info, warning, error) | -| `MaterialIcon` | type | Material Design icon name | -| `PersistConfig` | type | Configuration for persisting reports | -| `PluginConfig` | type | Plugin configuration with runner and audits | -| `PluginMeta` | type | Plugin metadata information | -| `PluginReport` | type | Plugin execution report | -| `Report` | type | Complete Code PushUp report | -| `ReportsDiff` | type | Comparison between two reports | -| `RunnerConfig` | type | Runner configuration for plugin execution | -| `RunnerFilesPaths` | type | File paths for runner configuration and output | -| `RunnerFunction` | type | Function-based runner implementation | -| `SourceFileLocation` | type | Source file location with position information | -| `Table` | type | Table data structure for reports | -| `TableAlignment` | type | Table column alignment (left, center, right) | -| `TableCellValue` | type | Table cell value types | -| `TableColumnObject` | type | Table column configuration object | -| `TableColumnPrimitive` | type | Primitive table column alignment | -| `TableRowObject` | type | Table row as key-value object | -| `TableRowPrimitive` | type | Table row as array of values | -| `ToolHandlerContentResult` | type | MCP tool handler content result | -| `ToolSchemaOptions` | type | MCP tool schema configuration options | -| `ToolsConfig` | type | MCP tools configuration | -| `UploadConfig` | type | Configuration for uploading reports to portal | -| `auditDetailsSchema` | schema | Zod schema for audit details validation | -| `auditDiffSchema` | schema | Zod schema for audit diff validation | -| `auditOutputSchema` | schema | Zod schema for audit output validation | -| `auditOutputsSchema` | schema | Zod schema for audit outputs array validation | -| `auditReportSchema` | schema | Zod schema for audit report validation | -| `auditResultSchema` | schema | Zod schema for audit result validation | -| `auditSchema` | schema | Zod schema for audit validation | -| `categoryConfigSchema` | schema | Zod schema for category configuration validation | -| `categoryDiffSchema` | schema | Zod schema for category diff validation | -| `categoryRefSchema` | schema | Zod schema for category reference validation | -| `categoryResultSchema` | schema | Zod schema for category result validation | -| `commitSchema` | schema | Zod schema for git commit validation | -| `coreConfigSchema` | schema | Zod schema for core configuration validation | -| `fileNameSchema` | schema | Zod schema for file name validation | -| `filePathSchema` | schema | Zod schema for file path validation | -| `formatSchema` | schema | Zod schema for format validation | -| `groupRefSchema` | schema | Zod schema for group reference validation | -| `groupResultSchema` | schema | Zod schema for group result validation | -| `groupSchema` | schema | Zod schema for group validation | -| `issueSchema` | schema | Zod schema for issue validation | -| `issueSeveritySchema` | schema | Zod schema for issue severity validation | -| `materialIconSchema` | schema | Zod schema for material icon validation | -| `persistConfigSchema` | schema | Zod schema for persist configuration validation | -| `pluginConfigSchema` | schema | Zod schema for plugin configuration validation | -| `pluginMetaSchema` | schema | Zod schema for plugin metadata validation | -| `pluginReportSchema` | schema | Zod schema for plugin report validation | -| `reportSchema` | schema | Zod schema for report validation | -| `reportsDiffSchema` | schema | Zod schema for reports diff validation | -| `runnerConfigSchema` | schema | Zod schema for runner configuration validation | -| `runnerFilesPathsSchema` | schema | Zod schema for runner file paths validation | -| `runnerFunctionSchema` | schema | Zod schema for runner function validation | -| `sourceFileLocationSchema` | schema | Zod schema for source file location validation | -| `tableAlignmentSchema` | schema | Zod schema for table alignment validation | -| `tableCellValueSchema` | schema | Zod schema for table cell value validation | -| `tableColumnObjectSchema` | schema | Zod schema for table column object validation | -| `tableColumnPrimitiveSchema` | schema | Zod schema for table column primitive validation | -| `tableRowObjectSchema` | schema | Zod schema for table row object validation | -| `tableRowPrimitiveSchema` | schema | Zod schema for table row primitive validation | -| `tableSchema` | function | Function that returns Zod schema for table validation | -| `uploadConfigSchema` | schema | Zod schema for upload configuration validation | -| `CONFIG_FILE_NAME` | constant | Default configuration file name | -| `DEFAULT_PERSIST_FILENAME` | constant | Default persist filename | -| `DEFAULT_PERSIST_FORMAT` | constant | Default persist format array | -| `DEFAULT_PERSIST_OUTPUT_DIR` | constant | Default persist output directory | -| `MAX_DESCRIPTION_LENGTH` | constant | Maximum description length limit | -| `MAX_ISSUE_MESSAGE_LENGTH` | constant | Maximum issue message length limit | -| `MAX_SLUG_LENGTH` | constant | Maximum slug length limit | -| `MAX_TITLE_LENGTH` | constant | Maximum title length limit | -| `SUPPORTED_CONFIG_FILE_FORMATS` | constant | Array of supported configuration file formats | -| `exists` | function | Utility function to check if value exists (non-null) | +| Symbol | Kind | Summary | +| -------------------------- | --------- | -------------------------------------------------------- | +| `ArgumentValue` | type | CLI argument value types (number, string, boolean, array) | +| `CliArgsObject` | type | CLI arguments object with typed values | +| `DiagnosticsAware` | interface | Interface for objects that can report issues | +| `ToolHandlerContentResult` | type | MCP tool handler content result | +| `ToolSchemaOptions` | type | MCP tool schema configuration options | +| `ToolsConfig` | type | MCP tools configuration with schema and handler | diff --git a/packages/shared/models/jest.config.ts b/packages/shared/models/jest.config.ts deleted file mode 100644 index 633aaf6..0000000 --- a/packages/shared/models/jest.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { readFileSync } from 'fs'; - -// Reading the SWC compilation config for the spec files -const swcJestConfig = JSON.parse( - readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8'), -); - -// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves -swcJestConfig.swcrc = false; - -export default { - displayName: '@push-based/models', - preset: '../../../jest.preset.js', - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: 'test-output/jest/coverage', -}; diff --git a/packages/shared/models/package.json b/packages/shared/models/package.json index fb80f4d..3248be5 100644 --- a/packages/shared/models/package.json +++ b/packages/shared/models/package.json @@ -2,7 +2,7 @@ "name": "@push-based/models", "version": "0.0.1", "private": true, - "type": "commonjs", + "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/shared/models/src/index.ts b/packages/shared/models/src/index.ts index ae8df49..c57d003 100644 --- a/packages/shared/models/src/index.ts +++ b/packages/shared/models/src/index.ts @@ -1,127 +1,8 @@ -export { - tableCellValueSchema, - type TableCellValue, -} from './lib/implementation/schemas.js'; -export { - sourceFileLocationSchema, - type SourceFileLocation, -} from './lib/source.js'; +export type { CliArgsObject, ArgumentValue } from './lib/cli.js'; -export { - auditDetailsSchema, - auditOutputSchema, - auditOutputsSchema, - type AuditDetails, - type AuditOutput, - type AuditOutputs, -} from './lib/audit-output.js'; -export { auditSchema, type Audit } from './lib/audit.js'; -export { - categoryConfigSchema, - categoryRefSchema, - type CategoryConfig, - type CategoryRef, -} from './lib/category-config.js'; -export { commitSchema, type Commit } from './lib/commit.js'; -export { coreConfigSchema, type CoreConfig } from './lib/core-config.js'; -export { - groupRefSchema, - groupSchema, - type Group, - type GroupMeta, - type GroupRef, -} from './lib/group.js'; -export { - CONFIG_FILE_NAME, - SUPPORTED_CONFIG_FILE_FORMATS, -} from './lib/implementation/configuration.js'; -export { - DEFAULT_PERSIST_FILENAME, - DEFAULT_PERSIST_FORMAT, - DEFAULT_PERSIST_OUTPUT_DIR, -} from './lib/implementation/constants.js'; -export { - MAX_DESCRIPTION_LENGTH, - MAX_ISSUE_MESSAGE_LENGTH, - MAX_SLUG_LENGTH, - MAX_TITLE_LENGTH, -} from './lib/implementation/limits.js'; -export { - fileNameSchema, - filePathSchema, - materialIconSchema, - type MaterialIcon, -} from './lib/implementation/schemas.js'; -export { exists } from './lib/implementation/utils.js'; -export { - issueSchema, - issueSeveritySchema, - type Issue, - type IssueSeverity, -} from './lib/issue.js'; -export { - formatSchema, - persistConfigSchema, - type Format, - type PersistConfig, -} from './lib/persist-config.js'; -export { - pluginConfigSchema, - pluginMetaSchema, - type PluginConfig, - type PluginMeta, -} from './lib/plugin-config.js'; -export { - auditReportSchema, - pluginReportSchema, - reportSchema, - type AuditReport, - type PluginReport, - type Report, -} from './lib/report.js'; -export { - auditDiffSchema, - auditResultSchema, - categoryDiffSchema, - categoryResultSchema, - groupDiffSchema, - groupResultSchema, - reportsDiffSchema, - type AuditDiff, - type AuditResult, - type CategoryDiff, - type CategoryResult, - type GroupDiff, - type GroupResult, - type ReportsDiff, -} from './lib/reports-diff.js'; -export { - runnerConfigSchema, - runnerFunctionSchema, - runnerFilesPathsSchema, - type RunnerConfig, - type RunnerFunction, - type RunnerFilesPaths, -} from './lib/runner-config.js'; -export { - tableAlignmentSchema, - tableColumnObjectSchema, - tableColumnPrimitiveSchema, - tableRowObjectSchema, - tableRowPrimitiveSchema, - tableSchema, - type Table, - type TableAlignment, - type TableColumnObject, - type TableColumnPrimitive, - type TableRowObject, - type TableRowPrimitive, -} from './lib/table.js'; -export { uploadConfigSchema, type UploadConfig } from './lib/upload-config.js'; -export { type DiagnosticsAware } from './lib/diagnostics.js'; export type { ToolSchemaOptions, ToolsConfig, ToolHandlerContentResult, } from './lib/mcp.js'; -export type { CliArgsObject, ArgumentValue } from './lib/cli.js'; +export { type DiagnosticsAware } from './lib/diagnostics.js'; diff --git a/packages/shared/models/src/lib/audit-output.ts b/packages/shared/models/src/lib/audit-output.ts deleted file mode 100644 index 4ff94e9..0000000 --- a/packages/shared/models/src/lib/audit-output.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { z } from 'zod'; -import { - nonnegativeNumberSchema, - scoreSchema, - slugSchema, -} from './implementation/schemas.js'; -import { errorItems, hasDuplicateStrings } from './implementation/utils.js'; -import { issueSchema } from './issue.js'; -import { tableSchema } from './table.js'; - -export const auditValueSchema = - nonnegativeNumberSchema.describe('Raw numeric value'); -export const auditDisplayValueSchema = z - .string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }) - .optional(); - -export const auditDetailsSchema = z.object( - { - issues: z - .array(issueSchema, { description: 'List of findings' }) - .optional(), - table: tableSchema('Table of related findings').optional(), - }, - { description: 'Detailed information' }, -); -export type AuditDetails = z.infer; - -export const auditOutputSchema = z.object( - { - slug: slugSchema.describe('Reference to audit'), - displayValue: auditDisplayValueSchema, - value: auditValueSchema, - score: scoreSchema, - details: auditDetailsSchema.optional(), - }, - { description: 'Audit information' }, -); - -export type AuditOutput = z.infer; - -export const auditOutputsSchema = z - .array(auditOutputSchema, { - description: - 'List of JSON formatted audit output emitted by the runner process of a plugin', - }) - // audit slugs are unique - .refine( - (audits) => !getDuplicateSlugsInAudits(audits), - (audits) => ({ message: duplicateSlugsInAuditsErrorMsg(audits) }), - ); -export type AuditOutputs = z.infer; - -// helper for validator: audit slugs are unique -function duplicateSlugsInAuditsErrorMsg(audits: AuditOutput[]) { - const duplicateRefs = getDuplicateSlugsInAudits(audits); - return `In plugin audits the slugs are not unique: ${errorItems( - duplicateRefs, - )}`; -} - -function getDuplicateSlugsInAudits(audits: AuditOutput[]) { - return hasDuplicateStrings(audits.map(({ slug }) => slug)); -} diff --git a/packages/shared/models/src/lib/audit-output.unit.test.ts b/packages/shared/models/src/lib/audit-output.unit.test.ts deleted file mode 100644 index 64160ce..0000000 --- a/packages/shared/models/src/lib/audit-output.unit.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - type AuditOutput, - type AuditOutputs, - auditOutputSchema, - auditOutputsSchema, -} from './audit-output.js'; - -describe('auditOutputSchema', () => { - it('should accept a valid audit output without details', () => { - expect(() => - auditOutputSchema.parse({ - slug: 'cypress-e2e-test-results', - score: 0.8, - value: 80, - displayValue: '80 %', - } satisfies AuditOutput), - ).not.toThrow(); - }); - - it('should accept a valid audit output with details issues', () => { - expect(() => - auditOutputSchema.parse({ - slug: 'speed-index', - score: 0.3, - value: 4500, - displayValue: '4.5 s', - details: { - issues: [ - { - message: 'The progress chart was blocked for 4 seconds.', - severity: 'info', - }, - ], - }, - } satisfies AuditOutput), - ).not.toThrow(); - }); - - it('should accept a valid audit output with details table', () => { - expect(() => - auditOutputSchema.parse({ - slug: 'largest-contentful-paint', - score: 0.83, - value: 3090, - displayValue: '3.1 s', - details: { - table: { - rows: [ - { - selector: - '#title-card-3-1 > div.ptrack-content > a > div > img', - html: 'How to change your mind', - }, - ], - }, - }, - } satisfies AuditOutput), - ).not.toThrow(); - }); - - it('should accept a valid audit output with details table and issues', () => { - expect(() => - auditOutputSchema.parse({ - slug: 'speed-index', - score: 0.3, - value: 4500, - displayValue: '4.5 s', - details: { - issues: [ - { - message: 'The progress chart was blocked for 4 seconds.', - severity: 'info', - }, - ], - table: { - rows: [ - { - selector: - '#title-card-3-1 > div.ptrack-content > a > div > img', - html: 'How to change your mind', - }, - ], - }, - }, - } satisfies AuditOutput), - ).not.toThrow(); - }); - - it('should accept a decimal value', () => { - expect(() => - auditOutputSchema.parse({ - slug: 'first-meaningful-paint', - score: 1, - value: 883.4785, - } satisfies AuditOutput), - ).not.toThrow(); - }); - - it('should throw for a negative value', () => { - expect(() => - auditOutputSchema.parse({ - slug: 'speed-index', - score: 1, - value: -100, - } satisfies AuditOutput), - ).toThrow('too_small'); - }); - - it('should throw for a score outside 0-1 range', () => { - expect(() => - auditOutputSchema.parse({ - slug: 'maximum-layout-shift', - score: 9, - value: 90, - } satisfies AuditOutput), - ).toThrow('too_big'); - }); - - it('should throw for a missing score', () => { - expect(() => - auditOutputSchema.parse({ - slug: 'total-blocking-time', - value: 2500, - }), - ).toThrow('invalid_type'); - }); - - it('should throw for an invalid slug', () => { - expect(() => - auditOutputSchema.parse({ - slug: 'Lighthouse', - value: 2500, - }), - ).toThrow('slug has to follow the pattern'); - }); -}); - -describe('auditOutputsSchema', () => { - it('should accept a valid audit output array', () => { - expect(() => - auditOutputsSchema.parse([ - { - slug: 'total-blocking-time', - value: 2500, - score: 0.8, - }, - { - slug: 'speed-index', - score: 1, - value: 250, - }, - ] satisfies AuditOutputs), - ).not.toThrow(); - }); - - it('should accept an empty output array', () => { - expect(() => - auditOutputsSchema.parse([] satisfies AuditOutputs), - ).not.toThrow(); - }); - - it('should throw for duplicate outputs', () => { - expect(() => - auditOutputsSchema.parse([ - { - slug: 'total-blocking-time', - value: 2500, - score: 0.8, - }, - { - slug: 'speed-index', - score: 1, - value: 250, - }, - { - slug: 'total-blocking-time', - value: 4300, - score: 0.75, - }, - ] satisfies AuditOutputs), - ).toThrow('slugs are not unique: total-blocking-time'); - }); -}); diff --git a/packages/shared/models/src/lib/audit.ts b/packages/shared/models/src/lib/audit.ts deleted file mode 100644 index c90b84b..0000000 --- a/packages/shared/models/src/lib/audit.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { z } from 'zod'; -import { metaSchema, slugSchema } from './implementation/schemas.js'; -import { errorItems, hasDuplicateStrings } from './implementation/utils.js'; - -export const auditSchema = z - .object({ - slug: slugSchema.describe('ID (unique within plugin)'), - }) - .merge( - metaSchema({ - titleDescription: 'Descriptive name', - descriptionDescription: 'Description (markdown)', - docsUrlDescription: 'Link to documentation (rationale)', - description: 'List of scorable metrics for the given plugin', - isSkippedDescription: 'Indicates whether the audit is skipped', - }), - ); - -export type Audit = z.infer; -export const pluginAuditsSchema = z - .array(auditSchema, { - description: 'List of audits maintained in a plugin', - }) - .min(1) - // audit slugs are unique - .refine( - (auditMetadata) => !getDuplicateSlugsInAudits(auditMetadata), - (auditMetadata) => ({ - message: duplicateSlugsInAuditsErrorMsg(auditMetadata), - }), - ); - -// ======================= - -// helper for validator: audit slugs are unique -function duplicateSlugsInAuditsErrorMsg(audits: Audit[]) { - const duplicateRefs = getDuplicateSlugsInAudits(audits); - return `In plugin audits the following slugs are not unique: ${errorItems( - duplicateRefs, - )}`; -} - -function getDuplicateSlugsInAudits(audits: Audit[]) { - return hasDuplicateStrings(audits.map(({ slug }) => slug)); -} diff --git a/packages/shared/models/src/lib/audit.unit.test.ts b/packages/shared/models/src/lib/audit.unit.test.ts deleted file mode 100644 index 0c637ca..0000000 --- a/packages/shared/models/src/lib/audit.unit.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { type Audit, auditSchema, pluginAuditsSchema } from './audit.js'; - -describe('auditSchema', () => { - it('should accept a valid audit with all information', () => { - expect(() => - auditSchema.parse({ - slug: 'no-conditionals-in-tests', - title: 'No conditional logic is used in tests.', - description: 'Conditional logic does not produce stable results.', - docsUrl: - 'https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/no-conditional-in-test.md', - } satisfies Audit), - ).not.toThrow(); - }); - - it('should accept a valid audit with minimum information', () => { - expect(() => - auditSchema.parse({ - slug: 'jest-unit-test-results', - title: 'Jest unit tests results.', - } satisfies Audit), - ).not.toThrow(); - }); - - it('should ignore invalid docs URL', () => { - expect( - auditSchema.parse({ - slug: 'consistent-test-it', - title: 'Use a consistent test function.', - docsUrl: 'invalid-url', - } satisfies Audit), - ).toEqual({ - slug: 'consistent-test-it', - title: 'Use a consistent test function.', - docsUrl: '', - }); - }); -}); - -describe('pluginAuditsSchema', () => { - it('should parse a valid audit array', () => { - expect(() => - pluginAuditsSchema.parse([ - { - slug: 'consistent-test-it', - title: 'Use a consistent test function.', - }, - { - slug: 'jest-unit-test-results', - title: 'Jest unit tests results.', - }, - ] satisfies Audit[]), - ).not.toThrow(); - }); - - it('should throw for an empty array', () => { - expect(() => pluginAuditsSchema.parse([])).toThrow('too_small'); - }); - - it('should throw for duplicate audits', () => { - expect(() => - pluginAuditsSchema.parse([ - { - slug: 'consistent-test-it', - title: 'Use a consistent test function.', - }, - { - slug: 'jest-unit-test-results', - title: 'Jest unit tests results.', - }, - { - slug: 'jest-unit-test-results', - title: 'Jest unit tests results.', - }, - ] satisfies Audit[]), - ).toThrow('slugs are not unique: jest-unit-test-results'); - }); -}); diff --git a/packages/shared/models/src/lib/category-config.ts b/packages/shared/models/src/lib/category-config.ts deleted file mode 100644 index dc3cecd..0000000 --- a/packages/shared/models/src/lib/category-config.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { z } from 'zod'; -import { - metaSchema, - scorableSchema, - slugSchema, - weightedRefSchema, -} from './implementation/schemas.js'; -import { errorItems, hasDuplicateStrings } from './implementation/utils.js'; - -export const categoryRefSchema = weightedRefSchema( - 'Weighted references to audits and/or groups for the category', - 'Slug of an audit or group (depending on `type`)', -).merge( - z.object({ - type: z.enum(['audit', 'group'], { - description: - 'Discriminant for reference kind, affects where `slug` is looked up', - }), - plugin: slugSchema.describe( - 'Plugin slug (plugin should contain referenced audit or group)', - ), - }), -); -export type CategoryRef = z.infer; - -export const categoryConfigSchema = scorableSchema( - 'Category with a score calculated from audits and groups from various plugins', - categoryRefSchema, - getDuplicateRefsInCategoryMetrics, - duplicateRefsInCategoryMetricsErrorMsg, -) - .merge( - metaSchema({ - titleDescription: 'Category Title', - docsUrlDescription: 'Category docs URL', - descriptionDescription: 'Category description', - description: 'Meta info for category', - }), - ) - .merge( - z.object({ - isBinary: z - .boolean({ - description: - 'Is this a binary category (i.e. only a perfect score considered a "pass")?', - }) - .optional(), - }), - ); - -export type CategoryConfig = z.infer; - -// helper for validator: categories have unique refs to audits or groups -export function duplicateRefsInCategoryMetricsErrorMsg(metrics: CategoryRef[]) { - const duplicateRefs = getDuplicateRefsInCategoryMetrics(metrics); - return `In the categories, the following audit or group refs are duplicates: ${errorItems( - duplicateRefs, - )}`; -} - -function getDuplicateRefsInCategoryMetrics(metrics: CategoryRef[]) { - return hasDuplicateStrings( - metrics.map(({ slug, type, plugin }) => `${type} :: ${plugin} / ${slug}`), - ); -} - -export const categoriesSchema = z - .array(categoryConfigSchema, { - description: 'Categorization of individual audits', - }) - .refine( - (categoryCfg) => !getDuplicateSlugCategories(categoryCfg), - (categoryCfg) => ({ - message: duplicateSlugCategoriesErrorMsg(categoryCfg), - }), - ); - -// helper for validator: categories slugs are unique -function duplicateSlugCategoriesErrorMsg(categories: CategoryConfig[]) { - const duplicateStringSlugs = getDuplicateSlugCategories(categories); - return `In the categories, the following slugs are duplicated: ${errorItems( - duplicateStringSlugs, - )}`; -} - -function getDuplicateSlugCategories(categories: CategoryConfig[]) { - return hasDuplicateStrings(categories.map(({ slug }) => slug)); -} diff --git a/packages/shared/models/src/lib/category-config.unit.test.ts b/packages/shared/models/src/lib/category-config.unit.test.ts deleted file mode 100644 index d234c82..0000000 --- a/packages/shared/models/src/lib/category-config.unit.test.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - type CategoryConfig, - type CategoryRef, - categoriesSchema, - categoryConfigSchema, - categoryRefSchema, -} from './category-config.js'; - -describe('categoryRefSchema', () => { - it('should accept a valid category reference audit', () => { - expect(() => - categoryRefSchema.parse({ - plugin: 'eslint', - slug: 'no-magic-numbers', - type: 'audit', - weight: 1, - } satisfies CategoryRef), - ).not.toThrow(); - }); - - it('should accept a valid category reference group', () => { - expect(() => - categoryRefSchema.parse({ - plugin: 'lighthouse', - slug: 'lighthouse-performance', - type: 'group', - weight: 5, - } satisfies CategoryRef), - ).not.toThrow(); - }); - - it('should accept a zero-weight reference', () => { - expect(() => - categoryRefSchema.parse({ - plugin: 'npm-audit', - slug: 'npm-audit-experimental', - type: 'audit', - weight: 0, - } satisfies CategoryRef), - ).not.toThrow(); - }); - - it('should throw for a negative weight', () => { - expect(() => - categoryRefSchema.parse({ - plugin: 'npm-audit', - slug: 'npm-audit-experimental', - type: 'audit', - weight: -2, - } satisfies CategoryRef), - ).toThrow('Number must be greater than or equal to 0'); - }); - - it('should throw for an invalid reference type', () => { - expect(() => - categoryRefSchema.parse({ - plugin: 'cypress', - slug: 'cypress-e2e', - type: 'issue', - weight: 1, - }), - ).toThrow('Invalid enum value'); - }); - - it('should throw for a missing weight', () => { - expect(() => - categoryRefSchema.parse({ - plugin: 'cypress', - slug: 'cypress-e2e', - type: 'audit', - }), - ).toThrow('invalid_type'); - }); - - it('should throw for an invalid slug', () => { - expect(() => - categoryRefSchema.parse({ - plugin: 'jest', - slug: '-invalid-jest-slug', - type: 'audit', - }), - ).toThrow('The slug has to follow the pattern'); - }); -}); - -describe('categoryConfigSchema', () => { - it('should accept a valid category configuration with all entities', () => { - expect(() => - categoryConfigSchema.parse({ - slug: 'test-results', - title: 'Test results', - description: 'This category collects test results.', - docsUrl: 'https://www.cypress.io/', - isBinary: false, - refs: [ - { - plugin: 'cypress', - slug: 'cypress-e2e', - type: 'audit', - weight: 1, - }, - ], - } satisfies CategoryConfig), - ).not.toThrow(); - }); - - it('should accept a minimal category configuration', () => { - expect(() => - categoryConfigSchema.parse({ - slug: 'bug-prevention', - title: 'Bug prevention', - refs: [ - { - plugin: 'eslint', - slug: 'no-magic-numbers', - type: 'audit', - weight: 1, - }, - ], - } satisfies CategoryConfig), - ).not.toThrow(); - }); - - it('should throw for an empty category', () => { - expect(() => - categoryConfigSchema.parse({ - slug: 'in-progress', - title: 'This category is empty for now', - refs: [], - } satisfies CategoryConfig), - ).toThrow('In a category, there has to be at least one ref'); - }); - - it('should throw for duplicate category references', () => { - expect(() => - categoryConfigSchema.parse({ - slug: 'jest', - title: 'Jest results', - refs: [ - { - plugin: 'jest', - slug: 'jest-unit-tests', - type: 'audit', - weight: 1, - }, - { - plugin: 'jest', - slug: 'jest-unit-tests', - type: 'audit', - weight: 2, - }, - ], - } satisfies CategoryConfig), - ).toThrow('audit or group refs are duplicates'); - }); - - it('should throw for a category with only zero-weight references', () => { - expect(() => - categoryConfigSchema.parse({ - slug: 'informational', - title: 'This category is informational', - refs: [ - { - plugin: 'eslint', - slug: 'functional/immutable-data', - type: 'audit', - weight: 0, - }, - { - plugin: 'lighthouse', - slug: 'lighthouse-experimental', - type: 'group', - weight: 0, - }, - ], - } satisfies CategoryConfig), - ).toThrow( - 'In a category, there has to be at least one ref with weight > 0. Affected refs: functional/immutable-data, lighthouse-experimental', - ); - }); -}); - -describe('categoriesSchema', () => { - it('should accept a valid category array', () => { - expect(() => - categoriesSchema.parse([ - { - slug: 'bug-prevention', - title: 'Bug prevention', - refs: [ - { - plugin: 'eslint', - slug: 'no-magic-numbers', - type: 'audit', - weight: 1, - }, - ], - }, - { - slug: 'code-style', - title: 'Code style', - refs: [ - { - plugin: 'eslint', - slug: 'consistent-test-it', - type: 'audit', - weight: 1, - }, - ], - }, - ] satisfies CategoryConfig[]), - ).not.toThrow(); - }); - - it('should accept an empty category array', () => { - expect(() => categoriesSchema.parse([])).not.toThrow(); - }); - - it('should throw for category duplicates', () => { - expect(() => - categoriesSchema.parse([ - { - slug: 'bug-prevention', - title: 'Bug prevention', - refs: [ - { - plugin: 'eslint', - slug: 'no-magic-numbers', - type: 'audit', - weight: 1, - }, - ], - }, - { - slug: 'bug-prevention', - title: 'Test results', - refs: [ - { - plugin: 'jest', - slug: 'jest-unit-tests', - type: 'audit', - weight: 1, - }, - ], - }, - ] satisfies CategoryConfig[]), - ).toThrow( - 'In the categories, the following slugs are duplicated: bug-prevention', - ); - }); -}); diff --git a/packages/shared/models/src/lib/commit.ts b/packages/shared/models/src/lib/commit.ts deleted file mode 100644 index 8442b80..0000000 --- a/packages/shared/models/src/lib/commit.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z } from 'zod'; - -export const commitSchema = z.object( - { - hash: z - .string({ description: 'Commit SHA (full)' }) - .regex( - /^[\da-f]{40}$/, - 'Commit SHA should be a 40-character hexadecimal string', - ), - message: z.string({ description: 'Commit message' }), - date: z.coerce.date({ - description: 'Date and time when commit was authored', - }), - author: z - .string({ - description: 'Commit author name', - }) - .trim(), - }, - { description: 'Git commit' }, -); - -export type Commit = z.infer; diff --git a/packages/shared/models/src/lib/commit.unit.test.ts b/packages/shared/models/src/lib/commit.unit.test.ts deleted file mode 100644 index f554eea..0000000 --- a/packages/shared/models/src/lib/commit.unit.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { type Commit, commitSchema } from './commit.js'; - -describe('commitSchema', () => { - it('should accept valid git commit data', () => { - expect(() => - commitSchema.parse({ - hash: 'abcdef0123456789abcdef0123456789abcdef01', - message: 'Minor fixes', - author: 'John Doe', - date: new Date(), - } satisfies Commit), - ).not.toThrow(); - }); - - it('should coerce date string into Date object', () => { - expect( - commitSchema.parse({ - hash: 'abcdef0123456789abcdef0123456789abcdef01', - message: 'Minor fixes', - author: 'John Doe', - date: '2024-03-06T17:30:12+01:00', - }), - ).toEqual({ - hash: 'abcdef0123456789abcdef0123456789abcdef01', - message: 'Minor fixes', - author: 'John Doe', - date: new Date('2024-03-06T17:30:12+01:00'), - }); - }); - - it('should throw for invalid hash', () => { - expect(() => - commitSchema.parse({ - hash: '12345678', // too short - message: 'Minor fixes', - author: 'John Doe', - date: new Date(), - } satisfies Commit), - ).toThrow('Commit SHA should be a 40-character hexadecimal string'); - }); -}); diff --git a/packages/shared/models/src/lib/core-config.ts b/packages/shared/models/src/lib/core-config.ts deleted file mode 100644 index 4c53075..0000000 --- a/packages/shared/models/src/lib/core-config.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { z } from 'zod'; -import { categoriesSchema } from './category-config.js'; -import { - getMissingRefsForCategories, - missingRefsForCategoriesErrorMsg, -} from './implementation/utils.js'; -import { persistConfigSchema } from './persist-config.js'; -import { pluginConfigSchema } from './plugin-config.js'; -import { uploadConfigSchema } from './upload-config.js'; - -export const unrefinedCoreConfigSchema = z.object({ - plugins: z - .array(pluginConfigSchema, { - description: - 'List of plugins to be used (official, community-provided, or custom)', - }) - .min(1), - /** portal configuration for persisting results */ - persist: persistConfigSchema.optional(), - /** portal configuration for uploading results */ - upload: uploadConfigSchema.optional(), - categories: categoriesSchema.optional(), -}); - -export const coreConfigSchema = refineCoreConfig(unrefinedCoreConfigSchema); - -/** - * Add refinements to coreConfigSchema - * workaround for zod issue: https://github.com/colinhacks/zod/issues/454 - * - */ -export function refineCoreConfig(schema: typeof unrefinedCoreConfigSchema) { - // categories point to existing audit or group refs - return schema.refine( - ({ categories, plugins }) => - !getMissingRefsForCategories(categories, plugins), - ({ categories, plugins }) => ({ - message: missingRefsForCategoriesErrorMsg(categories, plugins), - }), - ); -} - -export type CoreConfig = z.infer; diff --git a/packages/shared/models/src/lib/core-config.unit.test.ts b/packages/shared/models/src/lib/core-config.unit.test.ts deleted file mode 100644 index edfb6fa..0000000 --- a/packages/shared/models/src/lib/core-config.unit.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { type CoreConfig, coreConfigSchema } from './core-config.js'; - -describe('coreConfigSchema', () => { - it('should accept a valid core configuration with all entities', () => { - expect(() => - coreConfigSchema.parse({ - categories: [ - { - slug: 'test-results', - title: 'Test results', - refs: [ - { - plugin: 'jest', - slug: 'unit-tests', - type: 'group', - weight: 1, - }, - ], - }, - ], - plugins: [ - { - slug: 'jest', - title: 'Jest', - icon: 'jest', - runner: { command: 'npm run test', outputFile: 'jest-output.json' }, - audits: [{ slug: 'jest-unit-tests', title: 'Jest unit tests.' }], - groups: [ - { - slug: 'unit-tests', - title: 'Unit tests', - refs: [{ slug: 'jest-unit-tests', weight: 2 }], - }, - ], - }, - ], - persist: { format: ['md'] }, - upload: { - apiKey: 'AP7-K3Y', - organization: 'code-pushup', - project: 'cli', - server: 'https://api.code-pushup.org', - }, - } satisfies CoreConfig), - ).not.toThrow(); - }); - - it('should accept a minimal core configuration', () => { - expect(() => - coreConfigSchema.parse({ - plugins: [ - { - slug: 'eslint', - title: 'ESLint', - icon: 'eslint', - runner: { command: 'npm run lint', outputFile: 'output.json' }, - audits: [{ slug: 'no-magic-numbers', title: 'No magic numbers.' }], - }, - ], - } satisfies CoreConfig), - ).not.toThrow(); - }); - - it('should throw for an empty configuration with no plugins', () => { - expect(() => coreConfigSchema.parse({ plugins: [] })).toThrow('too_small'); - }); - - it('should throw for a category reference not found in audits', () => { - expect(() => - coreConfigSchema.parse({ - categories: [ - { - slug: 'bug-prevention', - title: 'Bug prevention', - refs: [ - { - plugin: 'vitest', - slug: 'unit-tests', - type: 'audit', - weight: 1, - }, - ], - }, - ], - plugins: [ - { - slug: 'jest', - title: 'Jest', - icon: 'jest', - runner: { command: 'npm run test', outputFile: 'output.json' }, - audits: [{ slug: 'unit-tests', title: 'Jest unit tests.' }], - }, - ], - } satisfies CoreConfig), - ).toThrow( - 'category references need to point to an audit or group: vitest/unit-tests', - ); - }); - - it('should throw for a category reference not found in groups', () => { - expect(() => - coreConfigSchema.parse({ - categories: [ - { - slug: 'bug-prevention', - title: 'Bug prevention', - refs: [ - { - plugin: 'eslint', - slug: 'eslint-errors', - type: 'group', - weight: 1, - }, - ], - }, - ], - plugins: [ - { - slug: 'eslint', - title: 'ESLint', - icon: 'eslint', - runner: { command: 'npm run lint', outputFile: 'output.json' }, - audits: [{ slug: 'eslint-errors', title: 'ESLint errors.' }], - groups: [ - { - slug: 'eslint-suggestions', - title: 'ESLint suggestions', - refs: [{ slug: 'eslint-errors', weight: 1 }], - }, - ], - }, - ], - } satisfies CoreConfig), - ).toThrow( - 'category references need to point to an audit or group: eslint#eslint-errors (group)', - ); - }); - - it('should throw for a category with a zero-weight audit', () => { - const config = { - categories: [ - { - slug: 'performance', - title: 'Performance', - refs: [ - { - slug: 'performance', - weight: 1, - type: 'group', - plugin: 'lighthouse', - }, - ], - }, - { - slug: 'best-practices', - title: 'Best practices', - refs: [ - { - slug: 'best-practices', - weight: 1, - type: 'group', - plugin: 'lighthouse', - }, - ], - }, - ], - plugins: [ - { - slug: 'lighthouse', - title: 'Lighthouse', - icon: 'lighthouse', - runner: { command: 'npm run lint', outputFile: 'output.json' }, - audits: [ - { - slug: 'csp-xss', - title: 'Ensure CSP is effective against XSS attacks', - }, - ], - groups: [ - { - slug: 'best-practices', - title: 'Best practices', - refs: [{ slug: 'csp-xss', weight: 0 }], - }, - ], - }, - ], - } satisfies CoreConfig; - - expect(() => coreConfigSchema.parse(config)).toThrow( - 'In a category, there has to be at least one ref with weight > 0. Affected refs: csp-xss', - ); - }); -}); diff --git a/packages/shared/models/src/lib/diagnostics.ts b/packages/shared/models/src/lib/diagnostics.ts index 25e86fb..4cbe8a5 100644 --- a/packages/shared/models/src/lib/diagnostics.ts +++ b/packages/shared/models/src/lib/diagnostics.ts @@ -1,4 +1,4 @@ -import { Issue } from './issue.js'; +import { Issue } from '@code-pushup/models'; export interface DiagnosticsAware { // @TODO use Set diff --git a/packages/shared/models/src/lib/group.ts b/packages/shared/models/src/lib/group.ts deleted file mode 100644 index 40bdc0b..0000000 --- a/packages/shared/models/src/lib/group.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { z } from 'zod'; -import { - type WeightedRef, - metaSchema, - scorableSchema, - weightedRefSchema, -} from './implementation/schemas.js'; -import { - errorItems, - exists, - hasDuplicateStrings, -} from './implementation/utils.js'; - -export const groupRefSchema = weightedRefSchema( - 'Weighted reference to a group', - "Reference slug to a group within this plugin (e.g. 'max-lines')", -); -export type GroupRef = z.infer; - -export const groupMetaSchema = metaSchema({ - titleDescription: 'Descriptive name for the group', - descriptionDescription: 'Description of the group (markdown)', - docsUrlDescription: 'Group documentation site', - description: 'Group metadata', - isSkippedDescription: 'Indicates whether the group is skipped', -}); -export type GroupMeta = z.infer; - -export const groupSchema = scorableSchema( - 'A group aggregates a set of audits into a single score which can be referenced from a category. ' + - 'E.g. the group slug "performance" groups audits and can be referenced in a category', - groupRefSchema, - getDuplicateRefsInGroups, - duplicateRefsInGroupsErrorMsg, -).merge(groupMetaSchema); - -export type Group = z.infer; - -export const groupsSchema = z - .array(groupSchema, { - description: 'List of groups', - }) - .optional() - .refine( - (groups) => !getDuplicateSlugsInGroups(groups), - (groups) => ({ - message: duplicateSlugsInGroupsErrorMsg(groups), - }), - ); - -// ============ - -// helper for validator: group refs are unique -function duplicateRefsInGroupsErrorMsg(groups: WeightedRef[]) { - const duplicateRefs = getDuplicateRefsInGroups(groups); - return `In plugin groups the following references are not unique: ${errorItems( - duplicateRefs, - )}`; -} - -function getDuplicateRefsInGroups(groups: WeightedRef[]) { - return hasDuplicateStrings(groups.map(({ slug: ref }) => ref).filter(exists)); -} - -// helper for validator: group refs are unique -function duplicateSlugsInGroupsErrorMsg(groups: Group[] | undefined) { - const duplicateRefs = getDuplicateSlugsInGroups(groups); - return `In groups the following slugs are not unique: ${errorItems( - duplicateRefs, - )}`; -} - -function getDuplicateSlugsInGroups(groups: Group[] | undefined) { - return Array.isArray(groups) - ? hasDuplicateStrings(groups.map(({ slug }) => slug)) - : false; -} diff --git a/packages/shared/models/src/lib/group.unit.test.ts b/packages/shared/models/src/lib/group.unit.test.ts deleted file mode 100644 index e0f9b51..0000000 --- a/packages/shared/models/src/lib/group.unit.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - type Group, - type GroupRef, - groupRefSchema, - groupSchema, - groupsSchema, -} from './group.js'; - -describe('groupRefSchema', () => { - it('should accept a valid group reference', () => { - expect(() => - groupRefSchema.parse({ - slug: 'speed-index', - weight: 1, - } satisfies GroupRef), - ).not.toThrow(); - }); - - it('should throw for a group reference with invalid slug', () => { - expect(() => - groupRefSchema.parse({ - slug: '-invalid-blocking-time', - weight: 1, - } satisfies GroupRef), - ).toThrow('slug has to follow the pattern'); - }); - - it('should throw for a group reference with negative weight', () => { - expect(() => - groupRefSchema.parse({ - slug: 'total-blocking-time', - weight: -1, - } satisfies GroupRef), - ).toThrow('too_small'); - }); -}); - -describe('groupSchema', () => { - it('should accept a valid group with all information', () => { - expect(() => - groupSchema.parse({ - refs: [ - { slug: 'lighthouse-bug-prevention', weight: 2 }, - { slug: 'lighthouse-performance', weight: 1 }, - ], - slug: 'lighthouse', - title: 'Lighthouse', - description: 'Lighthouse is a performance and analysis tool.', - docsUrl: 'https://developer.chrome.com/docs/lighthouse/overview', - } satisfies Group), - ).not.toThrow(); - }); - - it('should accept a group with minimal information', () => { - expect(() => - groupSchema.parse({ - slug: 'lighthouse-quality', - title: 'Lighthouse quality plugin', - refs: [{ slug: 'lighthouse-bug-prevention', weight: 1 }], - } satisfies Group), - ).not.toThrow(); - }); - - it('should throw for an empty group', () => { - expect(() => - groupSchema.parse({ - slug: 'empty-group', - title: 'Empty group', - refs: [], - } satisfies Group), - ).toThrow('In a category, there has to be at least one ref'); - }); - - it('should throw for duplicate group references', () => { - expect(() => - groupSchema.parse({ - slug: 'lighthouse-quality', - title: 'Lighthouse quality plugin', - refs: [ - { slug: 'lighthouse-bug-prevention', weight: 1 }, - { slug: 'lighthouse-bug-prevention', weight: 2 }, - ], - } satisfies Group), - ).toThrow('following references are not unique: lighthouse-bug-prevention'); - }); -}); - -describe('groupsSchema', () => { - it('should accept a valid group array', () => { - expect(() => - groupsSchema.parse([ - { - slug: 'lighthouse', - title: 'Lighthouse', - refs: [{ slug: 'lighthouse-performance', weight: 1 }], - }, - { - slug: 'jest', - title: 'Jest', - refs: [{ slug: 'jest-unit-tests', weight: 2 }], - }, - ] satisfies Group[]), - ).not.toThrow(); - }); - - it('should accept an empty group array', () => { - expect(() => groupsSchema.parse([])).not.toThrow(); - }); - - it('should throw for duplicate group slugs', () => { - expect(() => - groupsSchema.parse([ - { - slug: 'lighthouse', - title: 'Lighthouse', - refs: [{ slug: 'lighthouse-performance', weight: 1 }], - }, - { - slug: 'lighthouse', - title: 'Lighthouse', - refs: [{ slug: 'lighthouse-bug-prevention', weight: 2 }], - }, - { - slug: 'jest', - title: 'Jest', - refs: [{ slug: 'jest-unit-tests', weight: 2 }], - }, - ] satisfies Group[]), - ).toThrow('slugs are not unique: lighthouse'); - }); -}); diff --git a/packages/shared/models/src/lib/implementation/configuration.ts b/packages/shared/models/src/lib/implementation/configuration.ts deleted file mode 100644 index 9682f27..0000000 --- a/packages/shared/models/src/lib/implementation/configuration.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const CONFIG_FILE_NAME = 'code-pushup.config'; -export const SUPPORTED_CONFIG_FILE_FORMATS: string[] = ['ts', 'mjs', 'js']; diff --git a/packages/shared/models/src/lib/implementation/constants.ts b/packages/shared/models/src/lib/implementation/constants.ts deleted file mode 100644 index 6cdc055..0000000 --- a/packages/shared/models/src/lib/implementation/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Format } from '../persist-config.js'; - -export const DEFAULT_PERSIST_OUTPUT_DIR = '.code-pushup'; -export const DEFAULT_PERSIST_FILENAME = 'report'; -export const DEFAULT_PERSIST_FORMAT: Format[] = ['json', 'md']; diff --git a/packages/shared/models/src/lib/implementation/limits.ts b/packages/shared/models/src/lib/implementation/limits.ts deleted file mode 100644 index 40b1767..0000000 --- a/packages/shared/models/src/lib/implementation/limits.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const MAX_SLUG_LENGTH = 128; -export const MAX_TITLE_LENGTH = 256; -export const MAX_DESCRIPTION_LENGTH = 65_536; -export const MAX_ISSUE_MESSAGE_LENGTH = 1024; diff --git a/packages/shared/models/src/lib/implementation/schemas.ts b/packages/shared/models/src/lib/implementation/schemas.ts deleted file mode 100644 index cb58a15..0000000 --- a/packages/shared/models/src/lib/implementation/schemas.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { type ZodObject, type ZodOptional, type ZodString, z } from 'zod'; -import { - MAX_DESCRIPTION_LENGTH, - MAX_SLUG_LENGTH, - MAX_TITLE_LENGTH, -} from './limits.js'; -import { filenameRegex, slugRegex } from './utils.js'; - -export const tableCellValueSchema = z - .union([z.string(), z.number(), z.boolean(), z.null()]) - .default(null); -export type TableCellValue = z.infer; - -/** - * Schema for execution meta date - */ -export function executionMetaSchema( - options: { - descriptionDate: string; - descriptionDuration: string; - } = { - descriptionDate: 'Execution start date and time', - descriptionDuration: 'Execution duration in ms', - }, -) { - return z.object({ - date: z.string({ description: options.descriptionDate }), - duration: z.number({ description: options.descriptionDuration }), - }); -} - -/** Schema for a slug of a categories, plugins or audits. */ -export const slugSchema = z - .string({ description: 'Unique ID (human-readable, URL-safe)' }) - .regex(slugRegex, { - message: - 'The slug has to follow the pattern [0-9a-z] followed by multiple optional groups of -[0-9a-z]. e.g. my-slug', - }) - .max(MAX_SLUG_LENGTH, { - message: `The slug can be max ${MAX_SLUG_LENGTH} characters long`, - }); - -/** Schema for a general description property */ -export const descriptionSchema = z - .string({ description: 'Description (markdown)' }) - .max(MAX_DESCRIPTION_LENGTH) - .optional(); - -/* Schema for a URL */ -export const urlSchema = z.string().url(); - -/** Schema for a docsUrl */ -export const docsUrlSchema = urlSchema - .optional() - .or(z.literal('')) // allow empty string (no URL validation) - // eslint-disable-next-line unicorn/prefer-top-level-await, unicorn/catch-error-name - .catch((ctx) => { - // if only URL validation fails, supress error since this metadata is optional anyway - if ( - ctx.error.errors.length === 1 && - ctx.error.errors[0]?.code === 'invalid_string' && - ctx.error.errors[0].validation === 'url' - ) { - return ''; - } - throw ctx.error; - }) - .describe('Documentation site'); - -/** Schema for a title of a plugin, category and audit */ -export const titleSchema = z - .string({ description: 'Descriptive name' }) - .max(MAX_TITLE_LENGTH); - -/** Schema for score of audit, category or group */ -export const scoreSchema = z - .number({ - description: 'Value between 0 and 1', - }) - .min(0) - .max(1); - -/** Schema for a property indicating whether an entity is filtered out */ -export const isSkippedSchema = z.boolean().optional(); - -/** - * Used for categories, plugins and audits - * @param options - */ -export function metaSchema(options?: { - titleDescription?: string; - descriptionDescription?: string; - docsUrlDescription?: string; - description?: string; - isSkippedDescription?: string; -}) { - const { - descriptionDescription, - titleDescription, - docsUrlDescription, - description, - isSkippedDescription, - } = options ?? {}; - return z.object( - { - title: titleDescription - ? titleSchema.describe(titleDescription) - : titleSchema, - description: descriptionDescription - ? descriptionSchema.describe(descriptionDescription) - : descriptionSchema, - docsUrl: docsUrlDescription - ? docsUrlSchema.describe(docsUrlDescription) - : docsUrlSchema, - isSkipped: isSkippedDescription - ? isSkippedSchema.describe(isSkippedDescription) - : isSkippedSchema, - }, - { description }, - ); -} - -/** Schema for a generalFilePath */ -export const filePathSchema = z - .string() - .trim() - .min(1, { message: 'The path is invalid' }); - -/** Schema for a fileNameSchema */ -export const fileNameSchema = z - .string() - .trim() - .regex(filenameRegex, { - message: `The filename has to be valid`, - }) - .min(1, { message: 'The file name is invalid' }); - -/** Schema for a positiveInt */ -export const positiveIntSchema = z.number().int().positive(); - -export const nonnegativeNumberSchema = z.number().nonnegative(); - -export function packageVersionSchema(options?: { - versionDescription?: string; - required?: TRequired; -}) { - const { versionDescription = 'NPM version of the package', required } = - options ?? {}; - const packageSchema = z.string({ description: 'NPM package name' }); - const versionSchema = z.string({ description: versionDescription }); - return z.object( - { - packageName: required ? packageSchema : packageSchema.optional(), - version: required ? versionSchema : versionSchema.optional(), - }, - { description: 'NPM package name and version of a published package' }, - ) as ZodObject<{ - packageName: TRequired extends true ? ZodString : ZodOptional; - version: TRequired extends true ? ZodString : ZodOptional; - }>; -} - -/** Schema for a weight */ -export const weightSchema = nonnegativeNumberSchema.describe( - 'Coefficient for the given score (use weight 0 if only for display)', -); - -export function weightedRefSchema( - description: string, - slugDescription: string, -) { - return z.object( - { - slug: slugSchema.describe(slugDescription), - weight: weightSchema.describe('Weight used to calculate score'), - }, - { description }, - ); -} - -export type WeightedRef = z.infer>; - -export function scorableSchema>( - description: string, - refSchema: T, - duplicateCheckFn: (metrics: z.infer[]) => false | string[], - duplicateMessageFn: (metrics: z.infer[]) => string, -) { - return z.object( - { - slug: slugSchema.describe('Human-readable unique ID, e.g. "performance"'), - refs: z - .array(refSchema) - .min(1, { message: 'In a category, there has to be at least one ref' }) - // refs are unique - .refine( - (refs) => !duplicateCheckFn(refs), - (refs) => ({ - message: duplicateMessageFn(refs), - }), - ) - // category weights are correct - .refine(hasNonZeroWeightedRef, (refs) => { - const affectedRefs = refs.map((ref) => ref.slug).join(', '); - return { - message: `In a category, there has to be at least one ref with weight > 0. Affected refs: ${affectedRefs}`, - }; - }), - }, - { description }, - ); -} - -export const materialIconSchema = z - .string({ - description: 'Icon name', - }) - .optional(); -export type MaterialIcon = z.infer; - -type Ref = { weight: number }; - -function hasNonZeroWeightedRef(refs: Ref[]) { - return refs.reduce((acc, { weight }) => weight + acc, 0) !== 0; -} diff --git a/packages/shared/models/src/lib/implementation/schemas.unit.test.ts b/packages/shared/models/src/lib/implementation/schemas.unit.test.ts deleted file mode 100644 index 4370cc9..0000000 --- a/packages/shared/models/src/lib/implementation/schemas.unit.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - type TableCellValue, - docsUrlSchema, - tableCellValueSchema, - weightSchema, -} from './schemas.js'; - -describe('primitiveValueSchema', () => { - it('should accept a valid union', () => { - const value: TableCellValue = 'test'; - expect(() => tableCellValueSchema.parse(value)).not.toThrow(); - }); - - it('should throw for a invalid union', () => { - const value = new Date(); - expect(() => tableCellValueSchema.parse(value)).toThrow('invalid_union'); - }); -}); - -describe('weightSchema', () => { - it('should accept an integer', () => { - expect(() => weightSchema.parse(1)).not.toThrow(); - }); - - it('should accept a float', () => { - expect(() => weightSchema.parse(0.5)).not.toThrow(); - }); - - it('should accept zero', () => { - expect(() => weightSchema.parse(0)).not.toThrow(); - }); - - it('should throw for negative number', () => { - expect(() => weightSchema.parse(-1)).toThrow('too_small'); - }); -}); - -describe('docsUrlSchema', () => { - it('should accept a valid URL', () => { - expect(() => - docsUrlSchema.parse( - 'https://eslint.org/docs/latest/rules/no-unused-vars', - ), - ).not.toThrow(); - }); - - it('should accept an empty string', () => { - expect(() => docsUrlSchema.parse('')).not.toThrow(); - }); - - it('should tolerate invalid URL - treat as missing and log warning', () => { - expect( - docsUrlSchema.parse( - '/home/user/project/tools/eslint-rules/rules/my-custom-rule.ts', - ), - ).toBe(''); - expect(console.warn).toHaveBeenCalledWith( - 'Ignoring invalid docsUrl: /home/user/project/tools/eslint-rules/rules/my-custom-rule.ts', - ); - }); - - it('should throw if not a string', () => { - expect(() => docsUrlSchema.parse(false)).toThrow('invalid_type'); - }); -}); diff --git a/packages/shared/models/src/lib/implementation/utils.ts b/packages/shared/models/src/lib/implementation/utils.ts deleted file mode 100644 index e147b89..0000000 --- a/packages/shared/models/src/lib/implementation/utils.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { CategoryConfig } from '../category-config.js'; -import type { PluginConfig } from '../plugin-config.js'; -import type { PluginReport } from '../report.js'; - -/** - * Regular expression to validate a slug for categories, plugins and audits. - * - audit (e.g. 'max-lines') - * - category (e.g. 'performance') - * Also validates ``and ` ` - */ -export const slugRegex = /^[a-z\d]+(?:-[a-z\d]+)*$/; - -/** - * Regular expression to validate a filename. - */ -export const filenameRegex = /^(?!.*[ \\/:*?"<>|]).+$/; - -/** - * helper function to validate string arrays - * - * @param strings - */ -export function hasDuplicateStrings(strings: string[]): string[] | false { - const sortedStrings = [...strings].sort(); - const duplStrings = sortedStrings.filter( - (item: string, index: number) => - index !== 0 && item === sortedStrings[index - 1], - ); - - return duplStrings.length === 0 ? false : [...new Set(duplStrings)]; -} - -/** - * helper function to validate string arrays - * - * @param toCheck - * @param existing - */ -export function hasMissingStrings( - toCheck: string[], - existing: string[], -): string[] | false { - const nonExisting = toCheck.filter((s) => !existing.includes(s)); - return nonExisting.length === 0 ? false : nonExisting; -} - -/** - * helper for error items - */ -export function errorItems( - items: string[] | false, - transform: (itemArr: string[]) => string = (itemArr) => itemArr.join(', '), -): string { - return transform(items || []); -} - -export function exists(value: T): value is NonNullable { - return value != null; -} - -/** - * Get category references that do not point to any audit or group - * @param categories - * @param plugins - * @returns Array of missing references. - */ -export function getMissingRefsForCategories( - categories: CategoryConfig[] | undefined, - plugins: PluginConfig[] | PluginReport[], -) { - if (!categories || categories.length === 0) { - return false; - } - - const auditRefsFromCategory = categories.flatMap(({ refs }) => - refs - .filter(({ type }) => type === 'audit') - .map(({ plugin, slug }) => `${plugin}/${slug}`), - ); - const auditRefsFromPlugins = plugins.flatMap(({ audits, slug: pluginSlug }) => - audits.map(({ slug }) => `${pluginSlug}/${slug}`), - ); - const missingAuditRefs = hasMissingStrings( - auditRefsFromCategory, - auditRefsFromPlugins, - ); - - const groupRefsFromCategory = categories.flatMap(({ refs }) => - refs - .filter(({ type }) => type === 'group') - .map(({ plugin, slug }) => `${plugin}#${slug} (group)`), - ); - const groupRefsFromPlugins = plugins.flatMap( - ({ groups, slug: pluginSlug }) => - Array.isArray(groups) - ? groups.map(({ slug }) => `${pluginSlug}#${slug} (group)`) - : [], - ); - const missingGroupRefs = hasMissingStrings( - groupRefsFromCategory, - groupRefsFromPlugins, - ); - - const missingRefs = [missingAuditRefs, missingGroupRefs] - .filter((refs): refs is string[] => Array.isArray(refs) && refs.length > 0) - .flat(); - - return missingRefs.length > 0 ? missingRefs : false; -} - -export function missingRefsForCategoriesErrorMsg( - categories: CategoryConfig[] | undefined, - plugins: PluginConfig[] | PluginReport[], -) { - const missingRefs = getMissingRefsForCategories(categories, plugins); - return `The following category references need to point to an audit or group: ${errorItems( - missingRefs, - )}`; -} diff --git a/packages/shared/models/src/lib/implementation/utils.unit.test.ts b/packages/shared/models/src/lib/implementation/utils.unit.test.ts deleted file mode 100644 index 2592770..0000000 --- a/packages/shared/models/src/lib/implementation/utils.unit.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - filenameRegex, - hasDuplicateStrings, - hasMissingStrings, - slugRegex, -} from './utils.js'; - -describe('slugRegex', () => { - it.each([ - 'hello', - 'hello-world', - 'hello-123-world', - '123', - '123-456', - '123-world-789', - ])(`should match a valid slug %p`, (slug) => { - expect(slug).toMatch(slugRegex); - }); - - it.each([ - '', - ' ', - 'hello world', - 'hello_world', - 'hello-World', - 'hello-world-', - '-hello-world', - 'hello--world', - '123-', - '-123', - '123--456', - ])(`should not match an invalid slug %p`, (invalidSlug) => { - expect(invalidSlug).not.toMatch(slugRegex); - }); -}); - -describe('filenameRegex', () => { - it.each(['report', 'report.mock', 'report-test.mock'])( - `should match a valid file name %p`, - (filename) => { - expect(filename).toMatch(filenameRegex); - }, - ); - - it.each([ - '', - ' ', - 'file/name', - 'file:name', - 'file*name', - 'file?name', - 'file"name', - 'filename', - 'file|name', - ])(`should not match an invalid file name %p`, (invalidFilename) => { - expect(invalidFilename).not.toMatch(filenameRegex); - }); -}); - -describe('hasDuplicateStrings', () => { - it('should return false for a list of unique strings', () => { - expect(hasDuplicateStrings(['a', 'b'])).toBe(false); - }); - - it('should return a list of duplicates for a list with duplicates', () => { - expect(hasDuplicateStrings(['a', 'b', 'a', 'c'])).toEqual(['a']); - }); - - it('should return a duplicate only once', () => { - expect(hasDuplicateStrings(['a', 'b', 'a', 'a'])).toEqual(['a']); - }); - - it('should return false for a list with 1 item', () => { - expect(hasDuplicateStrings(['a'])).toBe(false); - }); -}); - -describe('hasMissingStrings', () => { - it('should return false for two identical arrays', () => { - expect(hasMissingStrings(['a', 'b'], ['a', 'b'])).toBe(false); - }); - - it('should return false for an array subset', () => { - expect(hasMissingStrings(['b'], ['a', 'b'])).toBe(false); - }); - - it('should return false for two empty arrays', () => { - expect(hasMissingStrings([], [])).toBe(false); - }); - - it('should return a list of strings from source that are missing in target', () => { - expect(hasMissingStrings(['a', 'b'], ['a', 'c'])).toEqual(['b']); - }); -}); diff --git a/packages/shared/models/src/lib/issue.ts b/packages/shared/models/src/lib/issue.ts deleted file mode 100644 index c7184f1..0000000 --- a/packages/shared/models/src/lib/issue.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from 'zod'; -import { MAX_ISSUE_MESSAGE_LENGTH } from './implementation/limits.js'; -import { sourceFileLocationSchema } from './source.js'; - -export const issueSeveritySchema = z.enum(['info', 'warning', 'error'], { - description: 'Severity level', -}); -export type IssueSeverity = z.infer; -export const issueSchema = z.object( - { - message: z - .string({ description: 'Descriptive error message' }) - .max(MAX_ISSUE_MESSAGE_LENGTH), - severity: issueSeveritySchema, - source: sourceFileLocationSchema.optional(), - }, - { description: 'Issue information' }, -); -export type Issue = z.infer; diff --git a/packages/shared/models/src/lib/issue.unit.test.ts b/packages/shared/models/src/lib/issue.unit.test.ts deleted file mode 100644 index 0ffcf96..0000000 --- a/packages/shared/models/src/lib/issue.unit.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { type Issue, issueSchema } from './issue.js'; - -describe('issueSchema', () => { - it('should accept a valid issue without source file information', () => { - expect(() => - issueSchema.parse({ - message: 'Do not use console.log()', - severity: 'error', - } satisfies Issue), - ).not.toThrow(); - }); - - it('should accept a valid issue with source file information', () => { - expect(() => - issueSchema.parse({ - message: 'Use const instead of let.', - severity: 'error', - source: { - file: 'my/code/index.ts', - position: { startLine: 1, startColumn: 4, endLine: 1, endColumn: 10 }, - }, - } satisfies Issue), - ).not.toThrow(); - }); - - it('should throw for a missing message', () => { - expect(() => - issueSchema.parse({ - severity: 'error', - source: { file: 'my/code/index.ts' }, - }), - ).toThrow('invalid_type'); - }); - - it('should throw for an invalid issue severity', () => { - expect(() => - issueSchema.parse({ - message: 'Use const instead of let.', - severity: 'critical', - }), - ).toThrow('Invalid enum value'); - }); - - it('should throw for invalid file position', () => { - expect(() => - issueSchema.parse({ - message: 'Use const instead of let.', - severity: 'warning', - source: { - file: 'src/utils.ts', - position: { startLine: 0, endLine: 3 }, - }, - } satisfies Issue), - ).toThrow('Number must be greater than 0'); - }); -}); diff --git a/packages/shared/models/src/lib/models.spec.ts b/packages/shared/models/src/lib/models.spec.ts deleted file mode 100644 index 9995c3c..0000000 --- a/packages/shared/models/src/lib/models.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { models } from './models.js'; - -describe('models', () => { - it('should work', () => { - expect(models()).toEqual('models'); - }); -}); diff --git a/packages/shared/models/src/lib/models.ts b/packages/shared/models/src/lib/models.ts deleted file mode 100644 index b090ae5..0000000 --- a/packages/shared/models/src/lib/models.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function models(): string { - return 'models'; -} diff --git a/packages/shared/models/src/lib/persist-config.ts b/packages/shared/models/src/lib/persist-config.ts deleted file mode 100644 index b349343..0000000 --- a/packages/shared/models/src/lib/persist-config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from 'zod'; -import { fileNameSchema, filePathSchema } from './implementation/schemas.js'; - -export const formatSchema = z.enum(['json', 'md']); -export type Format = z.infer; - -export const persistConfigSchema = z.object({ - outputDir: filePathSchema.describe('Artifacts folder').optional(), - filename: fileNameSchema - .describe('Artifacts file name (without extension)') - .optional(), - format: z.array(formatSchema).optional(), -}); - -export type PersistConfig = z.infer; diff --git a/packages/shared/models/src/lib/persist-config.unit.test.ts b/packages/shared/models/src/lib/persist-config.unit.test.ts deleted file mode 100644 index 9693217..0000000 --- a/packages/shared/models/src/lib/persist-config.unit.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { type PersistConfig, persistConfigSchema } from './persist-config.js'; - -describe('persistConfigSchema', () => { - it('should accept a valid configuration with all information', () => { - expect(() => - persistConfigSchema.parse({ - filename: 'report.json', - format: ['md'], - outputDir: 'dist', - } as PersistConfig), - ).not.toThrow(); - }); - - it('should accept an empty configuration', () => { - expect(persistConfigSchema.parse({})).toEqual({}); - }); - - it('should accept empty format', () => { - expect(() => persistConfigSchema.parse({ format: [] })).not.toThrow(); - }); - - it('should throw for an empty file name', () => { - expect(() => - persistConfigSchema.parse({ filename: ' ' } as PersistConfig), - ).toThrow('file name is invalid'); - }); - - it('should throw for an empty output directory', () => { - expect(() => - persistConfigSchema.parse({ outputDir: ' ' } as PersistConfig), - ).toThrow('path is invalid'); - }); - - it('should throw for an invalid format', () => { - expect(() => persistConfigSchema.parse({ format: ['html'] })).toThrow( - 'Invalid enum value', - ); - }); -}); diff --git a/packages/shared/models/src/lib/plugin-config.ts b/packages/shared/models/src/lib/plugin-config.ts deleted file mode 100644 index c4fc280..0000000 --- a/packages/shared/models/src/lib/plugin-config.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { z } from 'zod'; -import { pluginAuditsSchema } from './audit.js'; -import { groupsSchema } from './group.js'; -import { - materialIconSchema, - metaSchema, - packageVersionSchema, - slugSchema, -} from './implementation/schemas.js'; -import { errorItems, hasMissingStrings } from './implementation/utils.js'; -import { runnerConfigSchema, runnerFunctionSchema } from './runner-config.js'; - -export const pluginMetaSchema = packageVersionSchema() - .merge( - metaSchema({ - titleDescription: 'Descriptive name', - descriptionDescription: 'Description (markdown)', - docsUrlDescription: 'Plugin documentation site', - description: 'Plugin metadata', - }), - ) - .merge( - z.object({ - slug: slugSchema.describe('Unique plugin slug within core config'), - icon: materialIconSchema, - }), - ); -export type PluginMeta = z.infer; - -export const pluginDataSchema = z.object({ - runner: z.union([runnerConfigSchema, runnerFunctionSchema]), - audits: pluginAuditsSchema, - groups: groupsSchema, -}); -type PluginData = z.infer; - -export const pluginConfigSchema = pluginMetaSchema - .merge(pluginDataSchema) - // every listed group ref points to an audit within the plugin - .refine( - (pluginCfg) => !getMissingRefsFromGroups(pluginCfg), - (pluginCfg) => ({ - message: missingRefsFromGroupsErrorMsg(pluginCfg), - }), - ); - -export type PluginConfig = z.infer; - -// helper for validator: every listed group ref points to an audit within the plugin -function missingRefsFromGroupsErrorMsg(pluginCfg: PluginData) { - const missingRefs = getMissingRefsFromGroups(pluginCfg); - return `The following group references need to point to an existing audit in this plugin config: ${errorItems( - missingRefs, - )}`; -} - -function getMissingRefsFromGroups(pluginCfg: PluginData) { - return hasMissingStrings( - pluginCfg.groups?.flatMap(({ refs: audits }) => - audits.map(({ slug: ref }) => ref), - ) ?? [], - pluginCfg.audits.map(({ slug }) => slug), - ); -} diff --git a/packages/shared/models/src/lib/plugin-config.unit.test.ts b/packages/shared/models/src/lib/plugin-config.unit.test.ts deleted file mode 100644 index 56c0dde..0000000 --- a/packages/shared/models/src/lib/plugin-config.unit.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { type PluginConfig, pluginConfigSchema } from './plugin-config.js'; - -describe('pluginConfigSchema', () => { - it('should accept a valid plugin configuration with all entities', () => { - expect(() => - pluginConfigSchema.parse({ - slug: 'eslint', - title: 'ESLint plugin', - description: 'This plugin checks ESLint in configured files.', - docsUrl: 'https://eslint.org/', - icon: 'eslint', - runner: { command: 'node', outputFile: 'output.json' }, - audits: [ - { slug: 'no-magic-numbers', title: 'Use defined constants' }, - { slug: 'require-await', title: 'Every async has await.' }, - ], - groups: [ - { - slug: 'typescript-eslint', - title: 'TypeScript ESLint rules', - refs: [{ slug: 'require-await', weight: 2 }], - }, - ], - packageName: 'cli', - version: 'v0.5.2', - } satisfies PluginConfig), - ).not.toThrow(); - }); - - it('should accept a minimal plugin configuration', () => { - expect(() => - pluginConfigSchema.parse({ - slug: 'cypress', - title: 'Cypress testing', - icon: 'cypress', - runner: { command: 'npx cypress run', outputFile: 'e2e-output.json' }, - audits: [{ slug: 'cypress-e2e', title: 'Cypress E2E results' }], - } satisfies PluginConfig), - ).not.toThrow(); - }); - - it('should throw for a plugin configuration without audits', () => { - expect(() => - pluginConfigSchema.parse({ - slug: 'jest', - title: 'Jest', - icon: 'jest', - runner: { command: 'npm run test', outputFile: 'jest-output.json' }, - audits: [], - } satisfies PluginConfig), - ).toThrow('too_small'); - }); - - it('should throw for a configuration with a group reference missing among audits', () => { - expect(() => - pluginConfigSchema.parse({ - slug: 'cypress', - title: 'Cypress testing', - icon: 'cypress', - runner: { command: 'npx cypress run', outputFile: 'output.json' }, - audits: [{ slug: 'jest', title: 'Jest' }], - groups: [ - { - slug: 'cyct', - title: 'Cypress component testing', - refs: [{ slug: 'cyct', weight: 5 }], - }, - ], - } satisfies PluginConfig), - ).toThrow( - 'group references need to point to an existing audit in this plugin config: cyct', - ); - }); - - it('should throw for a plugin configuration that has a group but empty audits', () => { - expect(() => - pluginConfigSchema.parse({ - slug: 'cypress', - title: 'Cypress testing', - icon: 'cypress', - runner: { command: 'npx cypress run', outputFile: 'output.json' }, - audits: [], - groups: [ - { - slug: 'cyct', - title: 'Cypress component testing', - refs: [{ slug: 'cyct', weight: 5 }], - }, - ], - } satisfies PluginConfig), - ).toThrow( - 'group references need to point to an existing audit in this plugin config: cyct', - ); - }); - - it('should throw for an invalid plugin slug', () => { - expect(() => - pluginConfigSchema.parse({ - slug: '-invalid-jest', - title: 'Jest', - icon: 'jest', - runner: { command: 'npm run test', outputFile: 'jest-output.json' }, - audits: [], - } satisfies PluginConfig), - ).toThrow('slug has to follow the pattern'); - }); -}); diff --git a/packages/shared/models/src/lib/report.ts b/packages/shared/models/src/lib/report.ts deleted file mode 100644 index 7b9a3f0..0000000 --- a/packages/shared/models/src/lib/report.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { z } from 'zod'; -import { auditOutputSchema } from './audit-output.js'; -import { auditSchema } from './audit.js'; -import { categoryConfigSchema } from './category-config.js'; -import { commitSchema } from './commit.js'; -import { type Group, groupSchema } from './group.js'; -import { - executionMetaSchema, - packageVersionSchema, -} from './implementation/schemas.js'; -import { - errorItems, - getMissingRefsForCategories, - hasMissingStrings, - missingRefsForCategoriesErrorMsg, -} from './implementation/utils.js'; -import { pluginMetaSchema } from './plugin-config.js'; - -export const auditReportSchema = auditSchema.merge(auditOutputSchema); -export type AuditReport = z.infer; - -export const pluginReportSchema = pluginMetaSchema - .merge( - executionMetaSchema({ - descriptionDate: 'Start date and time of plugin run', - descriptionDuration: 'Duration of the plugin run in ms', - }), - ) - .merge( - z.object({ - audits: z.array(auditReportSchema).min(1), - groups: z.array(groupSchema).optional(), - }), - ) - .refine( - (pluginReport) => - !getMissingRefsFromGroups(pluginReport.audits, pluginReport.groups ?? []), - (pluginReport) => ({ - message: missingRefsFromGroupsErrorMsg( - pluginReport.audits, - pluginReport.groups ?? [], - ), - }), - ); - -export type PluginReport = z.infer; - -// every listed group ref points to an audit within the plugin report -function missingRefsFromGroupsErrorMsg(audits: AuditReport[], groups: Group[]) { - const missingRefs = getMissingRefsFromGroups(audits, groups); - return `group references need to point to an existing audit in this plugin report: ${errorItems( - missingRefs, - )}`; -} - -function getMissingRefsFromGroups(audits: AuditReport[], groups: Group[]) { - return hasMissingStrings( - groups.flatMap(({ refs: auditRefs }) => - auditRefs.map(({ slug: ref }) => ref), - ), - audits.map(({ slug }) => slug), - ); -} - -export const reportSchema = packageVersionSchema({ - versionDescription: 'NPM version of the CLI', - required: true, -}) - .merge( - executionMetaSchema({ - descriptionDate: 'Start date and time of the collect run', - descriptionDuration: 'Duration of the collect run in ms', - }), - ) - .merge( - z.object( - { - plugins: z.array(pluginReportSchema).min(1), - categories: z.array(categoryConfigSchema).optional(), - commit: commitSchema - .describe('Git commit for which report was collected') - .nullable(), - }, - { description: 'Collect output data' }, - ), - ) - .refine( - ({ categories, plugins }) => - !getMissingRefsForCategories(categories, plugins), - ({ categories, plugins }) => ({ - message: missingRefsForCategoriesErrorMsg(categories, plugins), - }), - ); - -export type Report = z.infer; diff --git a/packages/shared/models/src/lib/report.unit.test.ts b/packages/shared/models/src/lib/report.unit.test.ts deleted file mode 100644 index 900532a..0000000 --- a/packages/shared/models/src/lib/report.unit.test.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - type AuditReport, - type PluginReport, - type Report, - auditReportSchema, - pluginReportSchema, - reportSchema, -} from './report.js'; - -describe('auditReportSchema', () => { - it('should accept a valid audit report with all entities', () => { - expect(() => - auditReportSchema.parse({ - slug: 'vitest', - title: 'Vitest', - score: 0.9, - value: 90, - displayValue: '90 %', - description: 'Vitest unit tests', - docsUrl: 'https://vitest.dev/', - details: { - issues: [ - { - message: 'expected to throw an error "invalid_type"', - severity: 'error', - }, - ], - }, - } satisfies AuditReport), - ).not.toThrow(); - }); - - it('should accept a minimal audit report', () => { - expect(() => - auditReportSchema.parse({ - slug: 'no-any', - title: 'Do not use any', - score: 1, - value: 0, - } satisfies AuditReport), - ).not.toThrow(); - }); - - it('should throw for a missing score', () => { - expect(() => - auditReportSchema.parse({ - slug: 'cummulative-layout-shift', - title: 'Cummulative layout shift', - value: 500, - }), - ).toThrow('invalid_type'); - }); - - it('should throw for score outside 0-1 range', () => { - expect(() => - auditReportSchema.parse({ - slug: 'no-magic-numbers', - title: 'Do not use magic numbers', - value: 2, - score: -20, - } satisfies AuditReport), - ).toThrow('too_small'); - }); -}); - -describe('pluginReportSchema', () => { - it('should accept a plugin report with all entities', () => { - expect(() => - pluginReportSchema.parse({ - slug: 'cli-report', - title: 'Code PushUp CLI report', - icon: 'npm', - date: '2024-01-11T11:00:00.000Z', - duration: 100_000, - audits: [ - { - slug: 'collect', - title: 'CLI main branch report', - score: 0.93, - value: 93, - }, - { - slug: 'perf-collect', - title: 'CLI performance branch report', - score: 0.82, - value: 82, - }, - ], - groups: [ - { - slug: 'perf', - title: 'Performance metrics', - refs: [{ slug: 'perf-collect', weight: 1 }], - }, - ], - description: 'Code PushUp CLI', - docsUrl: 'https://github.com/code-pushup/cli/wiki', - packageName: 'code-pushup/core', - version: '1.0.1', - } satisfies PluginReport), - ).not.toThrow(); - }); - - it('should accept a minimal plugin report', () => { - expect(() => - pluginReportSchema.parse({ - slug: 'cypress', - title: 'Cypress', - icon: 'cypress', - date: '2024-01-11T11:00:00.000Z', - duration: 123_000, - audits: [ - { - slug: 'cyct', - title: 'Component tests', - score: 0.96, - value: 96, - }, - ], - } satisfies PluginReport), - ).not.toThrow(); - }); - - it('should throw for a plugin report with no audit outputs', () => { - expect(() => - pluginReportSchema.parse({ - slug: 'cypress', - title: 'Cypress', - icon: 'cypress', - date: '2024-01-11T11:00:00.000Z', - duration: 123_000, - audits: [], - } satisfies PluginReport), - ).toThrow('too_small'); - }); - - it('should throw for a group reference without audits mention', () => { - expect(() => - pluginReportSchema.parse({ - slug: 'lighthouse', - title: 'Lighthouse', - icon: 'lighthouse', - date: '2024-01-12T10:00:00.000Z', - duration: 200, - audits: [ - { - slug: 'speed-index', - title: 'Speed index', - score: 0.87, - value: 600, - }, - ], - groups: [ - { - slug: 'perf', - title: 'Performance metrics', - refs: [{ slug: 'perf-lighthouse', weight: 1 }], - }, - ], - } satisfies PluginReport), - ).toThrow( - 'group references need to point to an existing audit in this plugin report: perf-lighthouse', - ); - }); -}); - -describe('reportSchema', () => { - it('should accept a valid report with all entities', () => { - expect(() => - reportSchema.parse({ - categories: [ - { - refs: [ - { - plugin: 'vitest', - slug: 'vitest-unit-test', - type: 'audit', - weight: 3, - }, - ], - slug: 'bug-prevention', - title: 'Bug prevention', - }, - ], - commit: { - hash: 'abcdef0123456789abcdef0123456789abcdef01', - message: 'Minor fixes', - author: 'John Doe', - date: new Date('2024-01-07T09:15:00.000Z'), - }, - date: '2024-01-07T09:30:00.000Z', - duration: 600, - plugins: [ - { - audits: [ - { score: 0, slug: 'vitest-unit-test', title: '', value: 0 }, - ], - date: '', - duration: 0, - icon: 'vitest', - slug: 'vitest', - title: 'Vitest', - packageName: 'cli', - version: 'v0.5.2', - }, - ], - packageName: 'cli', - version: '1.0.1', - } satisfies Report), - ).not.toThrow(); - }); - - it('should throw for a report with no plugins', () => { - expect(() => - reportSchema.parse({ - date: '2024-01-03T08:00:00.000Z', - duration: 14_500, - packageName: 'cli', - version: '1.0.1', - plugins: [], - }), - ).toThrow('too_small'); - }); - - it('should throw for a non-existent category reference', () => { - expect(() => - reportSchema.parse({ - categories: [ - { - refs: [ - { - plugin: 'vitest', - slug: 'vitest-unit-test', - type: 'audit', - weight: 3, - }, - ], - slug: 'bug-prevention', - title: 'Bug prevention', - }, - ], - commit: null, - date: '2024-01-07T09:30:00.000Z', - duration: 600, - plugins: [ - { - audits: [ - { - score: 0, - slug: 'vitest-integration-test', - title: '', - value: 0, - }, - ], - date: '2024-01-07T09:30:00.000Z', - duration: 450, - icon: 'vitest', - slug: 'vitest', - title: 'Vitest', - packageName: 'cli', - version: 'v0.5.2', - }, - ], - packageName: 'cli', - version: '1.0.1', - } satisfies Report), - ).toThrow( - 'category references need to point to an audit or group: vitest/vitest-unit-test', - ); - }); -}); diff --git a/packages/shared/models/src/lib/reports-diff.ts b/packages/shared/models/src/lib/reports-diff.ts deleted file mode 100644 index b02f612..0000000 --- a/packages/shared/models/src/lib/reports-diff.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { type ZodTypeAny, z } from 'zod'; -import { - auditDisplayValueSchema, - auditOutputSchema, - auditValueSchema, -} from './audit-output.js'; -import { commitSchema } from './commit.js'; -import { - docsUrlSchema, - executionMetaSchema, - packageVersionSchema, - scoreSchema, - slugSchema, - titleSchema, - urlSchema, -} from './implementation/schemas.js'; -import { pluginMetaSchema } from './plugin-config.js'; - -function makeComparisonSchema(schema: T) { - const sharedDescription = schema.description || 'Result'; - return z.object({ - before: schema.describe(`${sharedDescription} (source commit)`), - after: schema.describe(`${sharedDescription} (target commit)`), - }); -} - -function makeArraysComparisonSchema< - TDiff extends typeof scorableDiffSchema, - TResult extends ZodTypeAny, ->(diffSchema: TDiff, resultSchema: TResult, description: string) { - return z.object( - { - changed: z.array(diffSchema), - unchanged: z.array(resultSchema), - added: z.array(resultSchema), - removed: z.array(resultSchema), - }, - { description }, - ); -} - -const scorableMetaSchema = z.object({ - slug: slugSchema, - title: titleSchema, - docsUrl: docsUrlSchema, -}); -const scorableWithPluginMetaSchema = scorableMetaSchema.merge( - z.object({ - plugin: pluginMetaSchema - .pick({ slug: true, title: true, docsUrl: true }) - .describe('Plugin which defines it'), - }), -); - -const scorableDiffSchema = scorableMetaSchema.merge( - z.object({ - scores: makeComparisonSchema(scoreSchema) - .merge( - z.object({ - diff: z - .number() - .min(-1) - .max(1) - .describe('Score change (`scores.after - scores.before`)'), - }), - ) - .describe('Score comparison'), - }), -); -const scorableWithPluginDiffSchema = scorableDiffSchema.merge( - scorableWithPluginMetaSchema, -); - -export const categoryDiffSchema = scorableDiffSchema; -export const groupDiffSchema = scorableWithPluginDiffSchema; -export const auditDiffSchema = scorableWithPluginDiffSchema.merge( - z.object({ - values: makeComparisonSchema(auditValueSchema) - .merge( - z.object({ - diff: z - .number() - .describe('Value change (`values.after - values.before`)'), - }), - ) - .describe('Audit `value` comparison'), - displayValues: makeComparisonSchema(auditDisplayValueSchema).describe( - 'Audit `displayValue` comparison', - ), - }), -); - -export const categoryResultSchema = scorableMetaSchema.merge( - z.object({ score: scoreSchema }), -); -export const groupResultSchema = scorableWithPluginMetaSchema.merge( - z.object({ score: scoreSchema }), -); -export const auditResultSchema = scorableWithPluginMetaSchema.merge( - auditOutputSchema.pick({ score: true, value: true, displayValue: true }), -); - -export type CategoryDiff = z.infer; -export type GroupDiff = z.infer; -export type AuditDiff = z.infer; -export type CategoryResult = z.infer; -export type GroupResult = z.infer; -export type AuditResult = z.infer; - -export const reportsDiffSchema = z - .object({ - commits: makeComparisonSchema(commitSchema) - .nullable() - .describe('Commits identifying compared reports'), - portalUrl: urlSchema - .optional() - .describe('Link to comparison page in Code PushUp portal'), - label: z.string().optional().describe('Label (e.g. project name)'), - categories: makeArraysComparisonSchema( - categoryDiffSchema, - categoryResultSchema, - 'Changes affecting categories', - ), - groups: makeArraysComparisonSchema( - groupDiffSchema, - groupResultSchema, - 'Changes affecting groups', - ), - audits: makeArraysComparisonSchema( - auditDiffSchema, - auditResultSchema, - 'Changes affecting audits', - ), - }) - .merge( - packageVersionSchema({ - versionDescription: 'NPM version of the CLI (when `compare` was run)', - required: true, - }), - ) - .merge( - executionMetaSchema({ - descriptionDate: 'Start date and time of the compare run', - descriptionDuration: 'Duration of the compare run in ms', - }), - ); - -export type ReportsDiff = z.infer; diff --git a/packages/shared/models/src/lib/reports-diff.unit.test.ts b/packages/shared/models/src/lib/reports-diff.unit.test.ts deleted file mode 100644 index 6e618be..0000000 --- a/packages/shared/models/src/lib/reports-diff.unit.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { type ReportsDiff, reportsDiffSchema } from './reports-diff.js'; - -describe('reportsDiffSchema', () => { - it('should parse valid reports diff', () => { - expect(() => - reportsDiffSchema.parse({ - commits: { - before: { - hash: 'abcdef0123456789abcdef0123456789abcdef01', - message: 'Do stuff', - author: 'John Doe', - date: new Date('2023-03-07T23:00:00+01:00'), - }, - after: { - hash: '0123456789abcdef0123456789abcdef01234567', - message: 'Fix stuff', - author: 'Jane Doe', - date: new Date(), - }, - }, - portalUrl: - 'https://code-pushup.example.com/portal/example/website/comparison/abcdef0123456789abcdef0123456789abcdef01/0123456789abcdef0123456789abcdef01234567', - label: 'website', - date: new Date().toISOString(), - duration: 42, - packageName: '@push-based/core', - version: '1.2.3', - categories: { - changed: [ - { - slug: 'perf', - title: 'Performance', - scores: { before: 0.7, after: 0.66, diff: -0.04 }, - }, - ], - unchanged: [{ slug: 'a11y', title: 'Accessibility', score: 1 }], - added: [], - removed: [], - }, - groups: { - changed: [], - unchanged: [], - added: [], - removed: [ - { - slug: 'problems', - title: 'Problems', - plugin: { slug: 'eslint', title: 'ESLint' }, - score: 0.8, - }, - ], - }, - audits: { - changed: [ - { - slug: 'lcp', - title: 'Largest Contentful Paint', - plugin: { slug: 'lighthouse', title: 'Lighthouse' }, - scores: { - before: 0.9, - after: 0.7, - diff: -0.2, - }, - values: { - before: 1810, - after: 1920, - diff: 110, - }, - displayValues: { - before: '1.8 s', - after: '1.9 s', - }, - }, - ], - unchanged: [ - { - slug: 'image-alt', - title: 'Image elements have `[alt]` attributes', - plugin: { slug: 'lighthouse', title: 'Lighthouse' }, - score: 1, - value: 0, - }, - ], - added: [ - { - slug: 'document-title', - title: 'Document has a `` element', - plugin: { slug: 'lighthouse', title: 'Lighthouse' }, - score: 1, - value: 0, - }, - ], - removed: [], - }, - } satisfies ReportsDiff), - ).not.toThrow(); - }); -}); diff --git a/packages/shared/models/src/lib/runner-config.ts b/packages/shared/models/src/lib/runner-config.ts deleted file mode 100644 index 799da80..0000000 --- a/packages/shared/models/src/lib/runner-config.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { z } from 'zod'; -import { auditOutputsSchema } from './audit-output.js'; -import { filePathSchema } from './implementation/schemas.js'; - -export const outputTransformSchema = z - .function() - .args(z.unknown()) - .returns(z.union([auditOutputsSchema, z.promise(auditOutputsSchema)])); - -export type OutputTransform = z.infer<typeof outputTransformSchema>; - -export const runnerConfigSchema = z.object( - { - command: z.string({ - description: 'Shell command to execute', - }), - args: z.array(z.string({ description: 'Command arguments' })).optional(), - outputFile: filePathSchema.describe('Runner output path'), - outputTransform: outputTransformSchema.optional(), - configFile: filePathSchema.describe('Runner config path').optional(), - }, - { - description: 'How to execute runner', - }, -); - -export type RunnerConfig = z.infer<typeof runnerConfigSchema>; - -export const runnerFunctionSchema = z - .function() - .returns(z.union([auditOutputsSchema, z.promise(auditOutputsSchema)])); - -export type RunnerFunction = z.infer<typeof runnerFunctionSchema>; - -export const runnerFilesPathsSchema = z.object({ - runnerConfigPath: filePathSchema.describe('Runner config path'), - runnerOutputPath: filePathSchema.describe('Runner output path'), -}); - -export type RunnerFilesPaths = z.infer<typeof runnerFilesPathsSchema>; diff --git a/packages/shared/models/src/lib/runner-config.unit.test.ts b/packages/shared/models/src/lib/runner-config.unit.test.ts deleted file mode 100644 index 0be4b34..0000000 --- a/packages/shared/models/src/lib/runner-config.unit.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - type OutputTransform, - type RunnerConfig, - type RunnerFunction, - outputTransformSchema, - runnerConfigSchema, - runnerFunctionSchema, -} from './runner-config.js'; - -describe('runnerConfigSchema', () => { - it('should accept a valid runner configuration', () => { - expect(() => - runnerConfigSchema.parse({ - command: 'node', - args: ['-v'], - outputFile: 'output.json', - outputTransform: () => [], - } satisfies RunnerConfig), - ).not.toThrow(); - }); - - it('should accept a minimal runner configuration', () => { - expect(() => - runnerConfigSchema.parse({ - command: 'npm run test', - outputFile: 'output.json', - } satisfies RunnerConfig), - ).not.toThrow(); - }); - - it('should throw for a missing outputFile', () => { - expect(() => - runnerConfigSchema.parse({ - command: 'node', - }), - ).toThrow('invalid_type'); - }); - - it('should throw for an empty outputFile', () => { - expect(() => - runnerConfigSchema.parse({ - command: 'node', - outputFile: '', - }), - ).toThrow('path is invalid'); - }); -}); - -describe('runnerFunctionSchema', () => { - it('should accept a valid runner function', () => { - expect(() => - runnerFunctionSchema.parse((() => [ - { slug: 'npm-version', value: 6, score: 1 }, - ]) satisfies RunnerFunction), - ).not.toThrow(); - }); - - it('should accept a runner function that returns empty array', () => { - expect(() => - runnerFunctionSchema.parse((() => []) satisfies RunnerFunction), - ).not.toThrow(); - }); - - it('should throw for a non-function argument', () => { - expect(() => runnerFunctionSchema.parse({ slug: 'configuration' })).toThrow( - `Expected function,`, - ); - }); -}); - -describe('outputTransformSchema', () => { - it('should accept a valid outputTransform function', () => { - expect(() => - outputTransformSchema.parse((() => [ - { slug: 'node-version', value: 20, score: 1 }, - ]) satisfies OutputTransform), - ).not.toThrow(); - }); - - it('should accept an outputTransform function that returns empty array', () => { - expect(() => - outputTransformSchema.parse((() => []) satisfies OutputTransform), - ).not.toThrow(); - }); - - it('should throw for a non-function argument', () => { - expect(() => outputTransformSchema.parse('configuration')).toThrow( - 'Expected function', - ); - }); -}); diff --git a/packages/shared/models/src/lib/source.ts b/packages/shared/models/src/lib/source.ts deleted file mode 100644 index f1e55f2..0000000 --- a/packages/shared/models/src/lib/source.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { z } from 'zod'; -import { filePathSchema, positiveIntSchema } 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(), - }, - { description: 'Source file location' }, -); - -export type SourceFileLocation = z.infer<typeof sourceFileLocationSchema>; diff --git a/packages/shared/models/src/lib/table.ts b/packages/shared/models/src/lib/table.ts deleted file mode 100644 index 53d7cd1..0000000 --- a/packages/shared/models/src/lib/table.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { z } from 'zod'; -import { tableCellValueSchema } from './implementation/schemas.js'; - -export const tableAlignmentSchema = z.enum(['left', 'center', 'right'], { - description: 'Cell alignment', -}); -export type TableAlignment = z.infer<typeof tableAlignmentSchema>; - -export const tableColumnPrimitiveSchema = tableAlignmentSchema; -export type TableColumnPrimitive = z.infer<typeof tableColumnPrimitiveSchema>; - -export const tableColumnObjectSchema = z.object({ - key: z.string(), - label: z.string().optional(), - align: tableAlignmentSchema.optional(), -}); -export type TableColumnObject = z.infer<typeof tableColumnObjectSchema>; - -export const tableRowObjectSchema = z.record(tableCellValueSchema, { - description: 'Object row', -}); -export type TableRowObject = z.infer<typeof tableRowObjectSchema>; - -export const tableRowPrimitiveSchema = z.array(tableCellValueSchema, { - description: 'Primitive row', -}); -export type TableRowPrimitive = z.infer<typeof tableRowPrimitiveSchema>; - -const tableSharedSchema = z.object({ - title: z.string().optional().describe('Display title for table'), -}); -const tablePrimitiveSchema = tableSharedSchema.merge( - z.object( - { - columns: z.array(tableAlignmentSchema).optional(), - rows: z.array(tableRowPrimitiveSchema), - }, - { description: 'Table with primitive rows and optional alignment columns' }, - ), -); -const tableObjectSchema = tableSharedSchema.merge( - z.object( - { - columns: z - .union([ - z.array(tableAlignmentSchema), - z.array(tableColumnObjectSchema), - ]) - .optional(), - rows: z.array(tableRowObjectSchema), - }, - { - description: - 'Table with object rows and optional alignment or object columns', - }, - ), -); - -export const tableSchema = (description = 'Table information') => - z.union([tablePrimitiveSchema, tableObjectSchema], { description }); -export type Table = z.infer<ReturnType<typeof tableSchema>>; diff --git a/packages/shared/models/src/lib/table.unit.test.ts b/packages/shared/models/src/lib/table.unit.test.ts deleted file mode 100644 index 1e49a40..0000000 --- a/packages/shared/models/src/lib/table.unit.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - type Table, - type TableAlignment, - type TableColumnObject, - type TableColumnPrimitive, - type TableRowObject, - type TableRowPrimitive, - tableAlignmentSchema, - tableColumnObjectSchema, - tableColumnPrimitiveSchema, - tableRowObjectSchema, - tableRowPrimitiveSchema, - tableSchema, -} from './table.js'; - -describe('tableAlignmentSchema', () => { - it('should accept a valid enum', () => { - const alignment: TableAlignment = 'center'; - expect(() => tableAlignmentSchema.parse(alignment)).not.toThrow(); - }); - - it('should throw for a invalid enum', () => { - const alignment = 'crooked'; - expect(() => tableAlignmentSchema.parse(alignment)).toThrow( - 'invalid_enum_value', - ); - }); -}); - -describe('tableColumnPrimitiveSchema', () => { - it('should accept a valid enum', () => { - const column: TableColumnPrimitive = 'center'; - expect(() => tableColumnPrimitiveSchema.parse(column)).not.toThrow(); - }); - - it('should throw for a invalid enum', () => { - const column = 'crooked'; - expect(() => tableColumnPrimitiveSchema.parse(column)).toThrow( - 'invalid_enum_value', - ); - }); -}); - -describe('tableColumnObjectSchema', () => { - it('should accept a valid object', () => { - const column: TableColumnObject = { key: 'value' }; - expect(() => tableColumnObjectSchema.parse(column)).not.toThrow(); - }); - - it('should throw for a invalid object', () => { - const column = { test: 42 }; - expect(() => tableColumnObjectSchema.parse(column)).toThrow('invalid_type'); - }); -}); - -describe('tableRowPrimitiveSchema', () => { - it('should accept a valid array', () => { - const row: TableRowPrimitive = ['100 ms']; - expect(() => tableRowPrimitiveSchema.parse(row)).not.toThrow(); - }); - - it('should throw for a invalid array', () => { - const row = [{}]; - expect(() => tableRowPrimitiveSchema.parse(row)).toThrow( - 'Expected string, received object', - ); - }); -}); - -describe('tableRowObjectSchema', () => { - it('should accept a valid object', () => { - const row: TableRowObject = { key: 'value' }; - expect(() => tableRowObjectSchema.parse(row)).not.toThrow(); - }); - - it('should default undefined object values to null', () => { - const row = { prop: undefined }; - expect(tableRowObjectSchema.parse(row)).toStrictEqual({ - prop: null, - }); - }); - - it('should throw for a invalid object', () => { - const row = { prop: [] }; - expect(() => tableRowObjectSchema.parse(row)).toThrow('invalid_union'); - }); -}); - -describe('tableSchema', () => { - it('should accept a valid table with primitive data rows only', () => { - const table: Table = { - rows: [ - ['TTFB', '27%', '620 ms'], - ['Load Delay', '25%', '580 ms'], - ], - }; - expect(() => tableSchema().parse(table)).not.toThrow(); - }); - - it('should parse table with object data rows only', () => { - const table: Table = { rows: [{ metrics: 'TTFB' }] }; - expect(() => tableSchema().parse(table)).not.toThrow(); - }); - - it('should not throw for empty rows', () => { - const table: Table = { - rows: [], - }; - expect(() => tableSchema().parse(table)).not.toThrow(); - }); - - it('should throw for unsupported values in rows', () => { - const table: Table = { - rows: [[[] as unknown as string]], - }; - expect(() => tableSchema().parse(table)).toThrow( - 'Expected string, received array', - ); - }); - - it('should throw for mixed column types', () => { - const table = { - rows: [['1', { prop1: '2' }]], - }; - expect(() => tableSchema().parse(table)).toThrow( - 'Expected string, received object', - ); - }); - - it('should throw for unsupported combination of rows and column types', () => { - const table = { - rows: [['1']], - columns: [{ key: 'prop1' }], - }; - expect(() => tableSchema().parse(table)).toThrow('invalid_union'); - }); - - it('should parse table with rows and columns with alignment only', () => { - const table: Table = { - rows: [{ metrics: 'TTFB' }], - columns: ['left'], - }; - expect(() => tableSchema().parse(table)).not.toThrow(); - }); - - it('should parse table with rows and columns with keys only', () => { - const table: Table = { - rows: [{ metrics: 'TTFB' }], - columns: [{ key: 'metrics' }], - }; - expect(() => tableSchema().parse(table)).not.toThrow(); - }); - - it('should parse table with rows and columns', () => { - const table: Table = { - rows: [{ metrics: 'TTFB' }], - columns: [{ key: 'metrics', label: 'Metrics Name' }], - }; - expect(() => tableSchema().parse(table)).not.toThrow(); - }); - - it('should parse table with rows and columns and alignments', () => { - const table: Table = { - rows: [{ metrics: 'TTFB' }], - columns: [{ key: 'metrics', label: 'Metrics Name', align: 'left' }], - }; - expect(() => tableSchema().parse(table)).not.toThrow(); - }); - - it('should parse complete table', () => { - const fullTable: Table = { - title: 'Largest Contentful Paint element', - columns: [ - // center is often the default when rendering in MD or HTML - { key: 'phase', label: 'Phase' }, - { key: 'percentageLcp', label: '% of LCP', align: 'right' }, - { key: 'timing', label: 'Timing', align: 'left' }, - ], - rows: [ - { - phase: 'TTFB', - percentageLcp: '27%', - timing: '620 ms', - }, - { - phase: 'Load Delay', - percentageLcp: '25%', - timing: '580 ms', - }, - { - phase: 'Load Time', - percentageLcp: '41%', - timing: '940 ms', - }, - { - phase: 'Render Delay', - percentageLcp: '6%', - timing: '140 ms', - }, - ], - }; - expect(() => tableSchema().parse(fullTable)).not.toThrow(); - }); -}); diff --git a/packages/shared/models/src/lib/types.ts b/packages/shared/models/src/lib/types.ts deleted file mode 100644 index e69de29..0000000 diff --git a/packages/shared/models/src/lib/upload-config.ts b/packages/shared/models/src/lib/upload-config.ts deleted file mode 100644 index cc36ac0..0000000 --- a/packages/shared/models/src/lib/upload-config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from 'zod'; -import { slugSchema, urlSchema } from './implementation/schemas.js'; - -export const uploadConfigSchema = z.object({ - server: urlSchema.describe('URL of deployed portal API'), - apiKey: z.string({ - description: - 'API key with write access to portal (use `process.env` for security)', - }), - organization: slugSchema.describe( - 'Organization slug from Code PushUp portal', - ), - project: slugSchema.describe('Project slug from Code PushUp portal'), - timeout: z - .number({ description: 'Request timeout in minutes (default is 5)' }) - .positive() - .int() - .optional(), -}); - -export type UploadConfig = z.infer<typeof uploadConfigSchema>; diff --git a/packages/shared/models/src/lib/upload-config.unit.test.ts b/packages/shared/models/src/lib/upload-config.unit.test.ts deleted file mode 100644 index 422ea67..0000000 --- a/packages/shared/models/src/lib/upload-config.unit.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { type UploadConfig, uploadConfigSchema } from './upload-config.js'; - -describe('uploadConfigSchema', () => { - it('should accept a valid upload configuration', () => { - expect(() => - uploadConfigSchema.parse({ - apiKey: 'API-K3Y', - organization: 'code-pushup', - project: 'cli', - server: 'https://cli-server.dev:3800/', - } satisfies UploadConfig), - ).not.toThrow(); - }); - - it('should throw for an invalid server URL', () => { - expect(() => - uploadConfigSchema.parse({ - apiKey: 'API-K3Y', - organization: 'code-pushup', - project: 'cli', - server: '-invalid-/url', - } satisfies UploadConfig), - ).toThrow('Invalid url'); - }); - - it('should throw for a PascalCase organization name', () => { - expect(() => - uploadConfigSchema.parse({ - apiKey: 'API-K3Y', - organization: 'CodePushUp', - project: 'cli', - server: '-invalid-/url', - } satisfies UploadConfig), - ).toThrow('slug has to follow the pattern'); - }); - - it('should throw for a project with uppercase letters', () => { - expect(() => - uploadConfigSchema.parse({ - apiKey: 'API-K3Y', - organization: 'code-pushup', - project: 'Code-PushUp-CLI', - server: '-invalid-/url', - } satisfies UploadConfig), - ).toThrow('slug has to follow the pattern'); - }); -}); diff --git a/packages/shared/models/tsconfig.json b/packages/shared/models/tsconfig.json index 3a5af05..2582fcd 100644 --- a/packages/shared/models/tsconfig.json +++ b/packages/shared/models/tsconfig.json @@ -5,9 +5,6 @@ "references": [ { "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" } ], "nx": { diff --git a/packages/shared/models/tsconfig.spec.json b/packages/shared/models/tsconfig.spec.json deleted file mode 100644 index e1fa87f..0000000 --- a/packages/shared/models/tsconfig.spec.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./out-tsc/jest", - "types": ["jest", "node"], - "forceConsistentCasingInFileNames": true - }, - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ], - "references": [ - { - "path": "./tsconfig.lib.json" - } - ] -} diff --git a/packages/shared/models/vitest.config.mts b/packages/shared/models/vitest.config.mts deleted file mode 100644 index 2b1de1b..0000000 --- a/packages/shared/models/vitest.config.mts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - cacheDir: '../../../node_modules/.vite/models/unit', - root: __dirname, - test: { - include: ['src/**/*.spec.[jt]s?(x)'], - environment: 'node', - watch: false, - globals: true, - passWithNoTests: true, - testTimeout: 25_000, - - coverage: { - provider: 'v8', - reporter: ['text', 'lcov'], - reportsDirectory: '../../../coverage/models/unit', - exclude: ['**/mocks/**', '**/types.ts', '**/__snapshots__/**'], - }, - - reporters: ['default'], - }, -}); diff --git a/packages/shared/styles-ast-utils/jest.config.ts b/packages/shared/styles-ast-utils/jest.config.ts index edf8e72..7493468 100644 --- a/packages/shared/styles-ast-utils/jest.config.ts +++ b/packages/shared/styles-ast-utils/jest.config.ts @@ -10,7 +10,7 @@ swcJestConfig.swcrc = false; export default { displayName: '@push-based/styles-ast-utils', - preset: '../../../jest.preset.js', + preset: '../../../jest.preset.mjs', testEnvironment: 'node', transform: { '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], diff --git a/packages/shared/styles-ast-utils/package.json b/packages/shared/styles-ast-utils/package.json index bc91797..6adfc7c 100644 --- a/packages/shared/styles-ast-utils/package.json +++ b/packages/shared/styles-ast-utils/package.json @@ -2,7 +2,7 @@ "name": "@push-based/styles-ast-utils", "version": "0.0.1", "private": true, - "type": "commonjs", + "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/shared/styles-ast-utils/src/lib/utils.ts b/packages/shared/styles-ast-utils/src/lib/utils.ts index 9d0d163..73750bf 100644 --- a/packages/shared/styles-ast-utils/src/lib/utils.ts +++ b/packages/shared/styles-ast-utils/src/lib/utils.ts @@ -1,4 +1,4 @@ -import { Issue } from '@push-based/models'; +import { Issue } from '@code-pushup/models'; import { Rule } from 'postcss'; /** diff --git a/packages/shared/styles-ast-utils/tsconfig.json b/packages/shared/styles-ast-utils/tsconfig.json index 594e125..3a5af05 100644 --- a/packages/shared/styles-ast-utils/tsconfig.json +++ b/packages/shared/styles-ast-utils/tsconfig.json @@ -3,9 +3,6 @@ "files": [], "include": [], "references": [ - { - "path": "../models" - }, { "path": "./tsconfig.lib.json" }, diff --git a/packages/shared/styles-ast-utils/tsconfig.lib.json b/packages/shared/styles-ast-utils/tsconfig.lib.json index b32e9e0..3f1db1b 100644 --- a/packages/shared/styles-ast-utils/tsconfig.lib.json +++ b/packages/shared/styles-ast-utils/tsconfig.lib.json @@ -12,10 +12,6 @@ "types": ["node"] }, "include": ["src/**/*.ts"], - "references": [ - { - "path": "../models/tsconfig.lib.json" - } - ], + "references": [], "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] } diff --git a/packages/shared/typescript-ast-utils/jest.config.ts b/packages/shared/typescript-ast-utils/jest.config.ts index 5cc39ac..5333081 100644 --- a/packages/shared/typescript-ast-utils/jest.config.ts +++ b/packages/shared/typescript-ast-utils/jest.config.ts @@ -10,7 +10,7 @@ swcJestConfig.swcrc = false; export default { displayName: '@push-based/typescript-ast-utils', - preset: '../../../jest.preset.js', + preset: '../../../jest.preset.mjs', testEnvironment: 'node', transform: { '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], diff --git a/packages/shared/typescript-ast-utils/package.json b/packages/shared/typescript-ast-utils/package.json index 154002c..f945e64 100644 --- a/packages/shared/typescript-ast-utils/package.json +++ b/packages/shared/typescript-ast-utils/package.json @@ -2,7 +2,7 @@ "name": "@push-based/typescript-ast-utils", "version": "0.0.1", "private": true, - "type": "commonjs", + "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/shared/typescript-ast-utils/src/index.ts b/packages/shared/typescript-ast-utils/src/index.ts index 78809f0..6012acc 100644 --- a/packages/shared/typescript-ast-utils/src/index.ts +++ b/packages/shared/typescript-ast-utils/src/index.ts @@ -1,2 +1,5 @@ export * from './lib/constants.js'; export * from './lib/utils.js'; + +export { removeQuotes } from './lib/utils.js'; +export { QUOTE_REGEX } from './lib/constants.js'; diff --git a/packages/shared/utils/ai/API.md b/packages/shared/utils/ai/API.md index 4f30796..37ab49b 100644 --- a/packages/shared/utils/ai/API.md +++ b/packages/shared/utils/ai/API.md @@ -9,10 +9,12 @@ import { executeProcess, findFilesWithPattern, resolveFileCached, - slugify, + loadDefaultExport, objectToCliArgs, } from '@push-based/utils'; +import { slugify } from '@code-pushup/utils'; + // Execute a process with observer const result = await executeProcess({ command: 'node', @@ -28,6 +30,9 @@ const files = await findFilesWithPattern('./src', 'Component'); // Resolve file with caching const content = await resolveFileCached('./config.json'); +// Load ES module default export +const config = await loadDefaultExport('./config.mjs'); + // String utilities const slug = slugify('Hello World!'); // → 'hello-world' const args = objectToCliArgs({ name: 'test', verbose: true }); // → ['--name="test"', '--verbose'] @@ -37,6 +42,7 @@ const args = objectToCliArgs({ name: 'test', verbose: true }); // → ['--name=" - **Process Execution**: Robust child process management with observers and error handling - **File Operations**: Cached file resolution and pattern-based file searching +- **ES Module Loading**: Dynamic import of ES modules with default export extraction - **String Utilities**: Text transformation, slugification, and pluralization - **CLI Utilities**: Object-to-arguments conversion and command formatting - **Logging**: Environment-based verbose logging control @@ -46,6 +52,7 @@ const args = objectToCliArgs({ name: 'test', verbose: true }); // → ['--name=" - **Build Tools**: Execute CLI commands with real-time output monitoring - **File Processing**: Search and resolve files efficiently with caching +- **Module Loading**: Dynamic import of configuration files and plugins - **Code Generation**: Transform data into CLI arguments and formatted strings - **Development Tools**: Create development utilities with proper logging - **Static Analysis**: Find and process files based on content patterns diff --git a/packages/shared/utils/ai/EXAMPLES.md b/packages/shared/utils/ai/EXAMPLES.md index 22270c7..e4a87e5 100644 --- a/packages/shared/utils/ai/EXAMPLES.md +++ b/packages/shared/utils/ai/EXAMPLES.md @@ -95,43 +95,61 @@ if (componentFiles.length > 0) { --- -## 3 — String utilities and transformations +## 3 — Command formatting and logging -> Transform and manipulate strings for various use cases. +> Format commands with colors and context for better development experience. ```ts -import { slugify, pluralize, toUnixPath } from '@push-based/utils'; - -// Slugify text for URLs -const title = 'Hello World! This is a Test'; -const slug = slugify(title); -console.log(`Title: "${title}"`); -console.log(`Slug: "${slug}"`); -// → Title: "Hello World! This is a Test" -// → Slug: "hello-world-this-is-a-test" - -// Smart pluralization -const words = ['cat', 'dog', 'baby', 'box', 'mouse']; -words.forEach((word) => { - console.log(`${word} → ${pluralize(word)}`); +import { formatCommandLog, isVerbose, calcDuration } from '@push-based/utils'; + +// Set verbose mode for demonstration +process.env['NG_MCP_VERBOSE'] = 'true'; + +// Format commands with different contexts +const commands = [ + { cmd: 'npm', args: ['install'], cwd: undefined }, + { cmd: 'npx', args: ['eslint', '--fix', 'src/'], cwd: './packages/app' }, + { cmd: 'node', args: ['build.js', '--prod'], cwd: '../tools' }, + { + cmd: 'git', + args: ['commit', '-m', 'feat: add new feature'], + cwd: process.cwd(), + }, +]; + +console.log('Formatted commands:'); +commands.forEach(({ cmd, args, cwd }) => { + const formatted = formatCommandLog(cmd, args, cwd); + console.log(formatted); }); -// → cat → cats -// → dog → dogs -// → baby → babies -// → box → boxes -// → mouse → mouses - -// Conditional pluralization based on count -console.log(`1 ${pluralize('item', 1)}`); // → 1 item -console.log(`5 ${pluralize('item', 5)}`); // → 5 items - -// Path normalization -const windowsPath = 'C:\\Users\\John\\Documents\\file.txt'; -const unixPath = toUnixPath(windowsPath); -console.log(`Windows: ${windowsPath}`); -console.log(`Unix: ${unixPath}`); -// → Windows: C:\Users\John\Documents\file.txt -// → Unix: C:/Users/John/Documents/file.txt + +// Performance timing example +async function timedOperation() { + const start = performance.now(); + + // Simulate some work + await new Promise((resolve) => setTimeout(resolve, 150)); + + const duration = calcDuration(start); + console.log(`Operation completed in ${duration}ms`); +} + +// Verbose logging check +if (isVerbose()) { + console.log('🔍 Verbose logging is enabled'); + await timedOperation(); +} else { + console.log('🔇 Verbose logging is disabled'); +} + +// Output (with ANSI colors in terminal): +// → Formatted commands: +// → $ npm install +// → packages/app $ npx eslint --fix src/ +// → .. $ node build.js --prod +// → $ git commit -m feat: add new feature +// → 🔍 Verbose logging is enabled +// → Operation completed in 152ms ``` --- @@ -203,7 +221,105 @@ complexArgs.forEach((arg) => console.log(` ${arg}`)); --- -## 5 — Advanced file operations with generators +## 5 — Error handling and process management + +> Handle process errors gracefully with comprehensive error information. + +```ts +import { executeProcess, ProcessError } from '@push-based/utils'; + +async function robustProcessExecution() { + const commands = [ + { command: 'node', args: ['--version'] }, // ✅ Should succeed + { command: 'nonexistent-command', args: [] }, // ❌ Should fail + { command: 'node', args: ['-e', 'process.exit(1)'] }, // ❌ Should fail with exit code 1 + ]; + + for (const config of commands) { + try { + console.log( + `\n🚀 Executing: ${config.command} ${config.args?.join(' ') || ''}` + ); + + const result = await executeProcess({ + ...config, + observer: { + onStdout: (data) => console.log(` 📤 ${data.trim()}`), + onStderr: (data) => console.error(` ❌ ${data.trim()}`), + onComplete: () => console.log(' ✅ Process completed'), + }, + }); + + console.log( + ` ✅ Success! Exit code: ${result.code}, Duration: ${result.duration}ms` + ); + } catch (error) { + if (error instanceof ProcessError) { + console.error(` ❌ Process failed:`); + console.error(` Exit code: ${error.code}`); + console.error( + ` Error output: ${error.stderr.trim() || 'No stderr'}` + ); + console.error( + ` Standard output: ${error.stdout.trim() || 'No stdout'}` + ); + } else { + console.error(` ❌ Unexpected error: ${error}`); + } + } + } + + // Example with ignoreExitCode option + console.log('\n🔄 Executing command with ignoreExitCode=true:'); + try { + const result = await executeProcess({ + command: 'node', + args: ['-e', 'console.log("Hello"); process.exit(1)'], + ignoreExitCode: true, + observer: { + onStdout: (data) => console.log(` 📤 ${data.trim()}`), + onComplete: () => + console.log(' ✅ Process completed (exit code ignored)'), + }, + }); + + console.log(` ✅ Completed with exit code ${result.code} (ignored)`); + console.log(` 📝 Output: ${result.stdout.trim()}`); + } catch (error) { + console.error(` ❌ This shouldn't happen with ignoreExitCode=true`); + } +} + +await robustProcessExecution(); + +// Output: +// → 🚀 Executing: node --version +// → 📤 v18.17.0 +// → ✅ Process completed +// → ✅ Success! Exit code: 0, Duration: 42ms +// → +// → 🚀 Executing: nonexistent-command +// → ❌ Process failed: +// → Exit code: null +// → Error output: spawn nonexistent-command ENOENT +// → Standard output: No stdout +// → +// → 🚀 Executing: node -e process.exit(1) +// → ❌ Process failed: +// → Exit code: 1 +// → Error output: No stderr +// → Standard output: No stdout +// → +// → 🔄 Executing command with ignoreExitCode=true: +// → 📤 Hello +// → ✅ Process completed (exit code ignored) +// → ✅ Completed with exit code 1 (ignored) +// → 📝 Output: Hello +``` + +--- + +## 6 — Advanced file operations with generators > Use async generators for efficient file processing. @@ -310,172 +426,55 @@ if (largeFiles.length > 0) { // → Total lines: 88 // → Lines with 'export': 5 // → First few matches: -// → Line 2 (1 hits): export function toUnixPath(path: string): string { -// → Line 6 (1 hits): export function slugify(text: string): string { -// → Line 14 (1 hits): export function pluralize(text: string, amount?: number): string { +// → Line 2 (1 hits): export function calcDuration(start: number, stop?: number): number { +// → Line 6 (1 hits): export function isVerbose(): boolean { +// → Line 14 (1 hits): export function formatCommandLog(command: string, args?: string[], cwd?: string): string { ``` --- -## 6 — Command formatting and logging +## 7 — ES Module loading and dynamic imports -> Format commands with colors and context for better development experience. +> Load ES modules dynamically and extract default exports safely. ```ts -import { formatCommandLog, isVerbose, calcDuration } from '@push-based/utils'; - -// Set verbose mode for demonstration -process.env['NG_MCP_VERBOSE'] = 'true'; - -// Format commands with different contexts -const commands = [ - { cmd: 'npm', args: ['install'], cwd: undefined }, - { cmd: 'npx', args: ['eslint', '--fix', 'src/'], cwd: './packages/app' }, - { cmd: 'node', args: ['build.js', '--prod'], cwd: '../tools' }, - { - cmd: 'git', - args: ['commit', '-m', 'feat: add new feature'], - cwd: process.cwd(), - }, -]; - -console.log('Formatted commands:'); -commands.forEach(({ cmd, args, cwd }) => { - const formatted = formatCommandLog(cmd, args, cwd); - console.log(formatted); -}); - -// Performance timing example -async function timedOperation() { - const start = performance.now(); +import { loadDefaultExport } from '@push-based/utils'; - // Simulate some work - await new Promise((resolve) => setTimeout(resolve, 150)); +// Load configuration from ES module +const config = await loadDefaultExport('./config/app.config.mjs'); +console.log(`API Port: ${config.port}`); - const duration = calcDuration(start); - console.log(`Operation completed in ${duration}ms`); - - // You can also provide explicit end time - const explicitEnd = performance.now(); - const explicitDuration = calcDuration(start, explicitEnd); - console.log(`Explicit timing: ${explicitDuration}ms`); +// Load with type safety +interface AppData { + version: string; + features: string[]; } -// Verbose logging check -if (isVerbose()) { - console.log('🔍 Verbose logging is enabled'); - await timedOperation(); -} else { - console.log('🔇 Verbose logging is disabled'); +const appData = await loadDefaultExport<AppData>('./data/app.mjs'); +console.log(`App version: ${appData.version}`); +console.log(`Features: ${appData.features.join(', ')}`); + +// Handle loading errors gracefully +try { + const plugin = await loadDefaultExport('./plugins/optional.mjs'); + console.log('✅ Plugin loaded'); +} catch (error) { + if (error.message.includes('No default export found')) { + console.warn('⚠️ Module missing default export'); + } else { + console.warn('⚠️ Plugin not found, continuing without it'); + } } -// Output (with ANSI colors in terminal): -// → Formatted commands: -// → $ npm install -// → packages/app $ npx eslint --fix src/ -// → .. $ node build.js --prod -// → $ git commit -m feat: add new feature -// → 🔍 Verbose logging is enabled -// → Operation completed in 152ms -// → Explicit timing: 152ms +// Output: +// → API Port: 3000 +// → App version: 1.2.0 +// → Features: auth, logging, metrics +// → ⚠️ Plugin not found, continuing without it ``` --- -## 7 — Error handling and process management - -> Handle process errors gracefully with comprehensive error information. - -```ts -import { executeProcess, ProcessError } from '@push-based/utils'; - -async function robustProcessExecution() { - const commands = [ - { command: 'node', args: ['--version'] }, // ✅ Should succeed - { command: 'nonexistent-command', args: [] }, // ❌ Should fail - { command: 'node', args: ['-e', 'process.exit(1)'] }, // ❌ Should fail with exit code 1 - ]; - - for (const config of commands) { - try { - console.log( - `\n🚀 Executing: ${config.command} ${config.args?.join(' ') || ''}` - ); - - const result = await executeProcess({ - ...config, - observer: { - onStdout: (data) => console.log(` 📤 ${data.trim()}`), - onStderr: (data) => console.error(` ❌ ${data.trim()}`), - onComplete: () => console.log(' ✅ Process completed'), - }, - }); - - console.log( - ` ✅ Success! Exit code: ${result.code}, Duration: ${result.duration}ms` - ); - } catch (error) { - if (error instanceof ProcessError) { - console.error(` ❌ Process failed:`); - console.error(` Exit code: ${error.code}`); - console.error( - ` Error output: ${error.stderr.trim() || 'No stderr'}` - ); - console.error( - ` Standard output: ${error.stdout.trim() || 'No stdout'}` - ); - } else { - console.error(` ❌ Unexpected error: ${error}`); - } - } - } - - // Example with ignoreExitCode option - console.log('\n🔄 Executing command with ignoreExitCode=true:'); - try { - const result = await executeProcess({ - command: 'node', - args: ['-e', 'console.log("Hello"); process.exit(1)'], - ignoreExitCode: true, - observer: { - onStdout: (data) => console.log(` 📤 ${data.trim()}`), - onComplete: () => - console.log(' ✅ Process completed (exit code ignored)'), - }, - }); - - console.log(` ✅ Completed with exit code ${result.code} (ignored)`); - console.log(` 📝 Output: ${result.stdout.trim()}`); - } catch (error) { - console.error(` ❌ This shouldn't happen with ignoreExitCode=true`); - } -} - -await robustProcessExecution(); -// Output: -// → 🚀 Executing: node --version -// → 📤 v18.17.0 -// → ✅ Process completed -// → ✅ Success! Exit code: 0, Duration: 42ms -// → -// → 🚀 Executing: nonexistent-command -// → ❌ Process failed: -// → Exit code: null -// → Error output: spawn nonexistent-command ENOENT -// → Standard output: No stdout -// → -// → 🚀 Executing: node -e process.exit(1) -// → ❌ Process failed: -// → Exit code: 1 -// → Error output: No stderr -// → Standard output: No stdout -// → -// → 🔄 Executing command with ignoreExitCode=true: -// → 📤 Hello -// → ✅ Process completed (exit code ignored) -// → ✅ Completed with exit code 1 (ignored) -// → 📝 Output: Hello -``` These examples demonstrate the comprehensive capabilities of the `@push-based/utils` library for process execution, file operations, string manipulation, and development tooling in Node.js applications. diff --git a/packages/shared/utils/ai/FUNCTIONS.md b/packages/shared/utils/ai/FUNCTIONS.md index c0c7c95..405f3ae 100644 --- a/packages/shared/utils/ai/FUNCTIONS.md +++ b/packages/shared/utils/ai/FUNCTIONS.md @@ -20,12 +20,10 @@ | `getLineHits` | function | Get all pattern matches within a text line | | `isExcludedDirectory` | function | Check if a directory should be excluded from searches | | `isVerbose` | function | Check if verbose logging is enabled via environment variable | +| `loadDefaultExport` | function | Dynamically import ES modules and extract default export | | `objectToCliArgs` | function | Convert object properties to command-line arguments | -| `pluralize` | function | Convert singular words to plural form | | `resolveFile` | function | Read file content directly without caching | | `resolveFileCached` | function | Read file content with caching for performance | -| `slugify` | function | Convert text to URL-friendly slug format | -| `toUnixPath` | function | Convert Windows paths to Unix-style paths | ## Types @@ -205,37 +203,6 @@ Converts an object with different value types into command-line arguments array. **Returns:** Array of formatted CLI arguments -### `toUnixPath(path: string): string` - -Converts Windows-style paths to Unix-style paths. - -**Parameters:** - -- `path` - Path string to convert - -**Returns:** Unix-style path string - -### `slugify(text: string): string` - -Converts text to URL-friendly slug format. - -**Parameters:** - -- `text` - Text to slugify - -**Returns:** Slugified string - -### `pluralize(text: string, amount?: number): string` - -Converts singular words to plural form with smart rules. - -**Parameters:** - -- `text` - Word to pluralize -- `amount` - Optional count to determine if pluralization is needed - -**Returns:** Pluralized or original word - ### `calcDuration(start: number, stop?: number): number` Calculates duration between performance timestamps. @@ -285,6 +252,25 @@ Checks if verbose logging is enabled via the `NG_MCP_VERBOSE` environment variab **Returns:** `true` if verbose logging is enabled +### `loadDefaultExport<T = unknown>(filePath: string): Promise<T>` + +Dynamically imports an ES Module and extracts the default export. Uses proper file URL conversion for cross-platform compatibility. + +**Parameters:** + +- `filePath` - Absolute path to the ES module file to import + +**Returns:** Promise resolving to the default export from the module + +**Throws:** Error if the module cannot be loaded or has no default export + +**Example:** + +```typescript +const config = await loadDefaultExport('/path/to/config.js'); +const data = await loadDefaultExport<MyDataType>('/path/to/data.mjs'); +``` + ## Constants ### `fileResolverCache: Map<string, Promise<string>>` diff --git a/packages/shared/utils/package.json b/packages/shared/utils/package.json index 8bdedfc..3491c3c 100644 --- a/packages/shared/utils/package.json +++ b/packages/shared/utils/package.json @@ -2,7 +2,7 @@ "name": "@push-based/utils", "version": "0.0.1", "private": true, - "type": "commonjs", + "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/shared/utils/src/index.ts b/packages/shared/utils/src/index.ts index ebacbc6..4a73c32 100644 --- a/packages/shared/utils/src/index.ts +++ b/packages/shared/utils/src/index.ts @@ -1,5 +1,6 @@ export * from './lib/utils.js'; export * from './lib/execute-process.js'; -export * from './lib/logging'; +export * from './lib/logging.js'; export * from './lib/file/find-in-file.js'; export * from './lib/file/file.resolver.js'; +export * from './lib/file/default-export-loader.js'; diff --git a/packages/shared/utils/src/lib/file/default-export-loader.spec.ts b/packages/shared/utils/src/lib/file/default-export-loader.spec.ts new file mode 100644 index 0000000..9253718 --- /dev/null +++ b/packages/shared/utils/src/lib/file/default-export-loader.spec.ts @@ -0,0 +1,126 @@ +import { describe, expect, it, beforeEach, afterEach } from 'vitest'; +import { writeFileSync, rmSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { loadDefaultExport } from './default-export-loader.js'; + +describe('loadDefaultExport', () => { + let testDir: string; + + beforeEach(() => { + testDir = join( + tmpdir(), + `test-${Date.now()}-${Math.random().toString(36).slice(2)}`, + ); + mkdirSync(testDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }); + }); + + const createFile = (name: string, content: string) => { + const path = join(testDir, name); + writeFileSync(path, content, 'utf-8'); + return path; + }; + + describe('Success Cases', () => { + it.each([ + { + type: 'array', + content: '[{name: "test"}]', + expected: [{ name: 'test' }], + }, + { + type: 'object', + content: '{version: "1.0"}', + expected: { version: '1.0' }, + }, + { type: 'string', content: '"test"', expected: 'test' }, + { type: 'null', content: 'null', expected: null }, + { type: 'boolean', content: 'false', expected: false }, + { type: 'undefined', content: 'undefined', expected: undefined }, + ])('should load $type default export', async ({ content, expected }) => { + const path = createFile('test.mjs', `export default ${content};`); + expect(await loadDefaultExport(path)).toEqual(expected); + }); + }); + + describe('Error Cases - No Default Export', () => { + it.each([ + { + desc: 'named exports only', + content: 'export const a = 1; export const b = 2;', + exports: 'a, b', + }, + { desc: 'empty module', content: '', exports: 'none' }, + { desc: 'comments only', content: '// comment', exports: 'none' }, + { + desc: 'function exports', + content: 'export function fn() {}', + exports: 'fn', + }, + ])('should throw error for $desc', async ({ content, exports }) => { + const path = createFile('test.mjs', content); + await expect(loadDefaultExport(path)).rejects.toThrow( + `No default export found in module. Expected ES Module format:\nexport default [...]\n\nAvailable exports: ${exports}`, + ); + }); + }); + + describe('Error Cases - File System', () => { + it('should throw error when file does not exist', async () => { + const path = join(testDir, 'missing.mjs'); + await expect(loadDefaultExport(path)).rejects.toThrow( + `Failed to load module from ${path}`, + ); + }); + + it('should throw error when file has syntax errors', async () => { + const path = createFile( + 'syntax.mjs', + 'export default { invalid: syntax }', + ); + await expect(loadDefaultExport(path)).rejects.toThrow( + `Failed to load module from ${path}`, + ); + }); + }); + + describe('Edge Cases', () => { + it('should work with TypeScript generics', async () => { + interface Config { + name: string; + } + const path = createFile('typed.mjs', 'export default [{name: "test"}];'); + const result = await loadDefaultExport<Config[]>(path); + expect(result).toEqual([{ name: 'test' }]); + }); + + it('should handle mixed exports (prefers default)', async () => { + const path = createFile( + 'mixed.mjs', + 'export const named = "n"; export default "d";', + ); + expect(await loadDefaultExport<string>(path)).toBe('d'); + }); + + it('should handle complex nested structures', async () => { + const path = createFile( + 'complex.mjs', + ` + export default { + data: [{ name: 'test', meta: { date: new Date('2024-01-01') } }], + version: '1.0' + }; + `, + ); + const result = await loadDefaultExport(path); + expect(result).toMatchObject({ + data: [{ name: 'test', meta: { date: expect.any(Date) } }], + version: '1.0', + }); + }); + }); +}); diff --git a/packages/shared/utils/src/lib/file/default-export-loader.ts b/packages/shared/utils/src/lib/file/default-export-loader.ts new file mode 100644 index 0000000..73ec399 --- /dev/null +++ b/packages/shared/utils/src/lib/file/default-export-loader.ts @@ -0,0 +1,42 @@ +import { pathToFileURL } from 'node:url'; + +/** + * Dynamically imports an ES Module and extracts the default export. + * + * @param filePath - Absolute path to the ES module file to import + * @returns The default export from the module + * @throws Error if the module cannot be loaded or has no default export + * + * @example + * ```typescript + * const data = await loadDefaultExport('/path/to/config.js'); + * ``` + */ +export async function loadDefaultExport<T = unknown>( + filePath: string, +): Promise<T> { + try { + const fileUrl = pathToFileURL(filePath).toString(); + const module = await import(fileUrl); + + if (!('default' in module)) { + throw new Error( + `No default export found in module. Expected ES Module format:\n` + + `export default [...]\n\n` + + `Available exports: ${Object.keys(module).join(', ') || 'none'}`, + ); + } + + return module.default; + } catch (ctx) { + if ( + ctx instanceof Error && + ctx.message.includes('No default export found') + ) { + throw ctx; + } + throw new Error( + `Failed to load module from ${filePath}: ${ctx instanceof Error ? ctx.message : String(ctx)}`, + ); + } +} diff --git a/packages/shared/utils/src/lib/utils.ts b/packages/shared/utils/src/lib/utils.ts index 96ab8db..b811455 100644 --- a/packages/shared/utils/src/lib/utils.ts +++ b/packages/shared/utils/src/lib/utils.ts @@ -1,31 +1,5 @@ import { CliArgsObject, ArgumentValue } from '@push-based/models'; -export function toUnixPath(path: string): string { - return path.replace(/\\/g, '/'); -} - -export function slugify(text: string): string { - return text - .trim() - .toLowerCase() - .replace(/\s+|\//g, '-') - .replace(/[^a-z\d-]/g, ''); -} - -export function pluralize(text: string, amount?: number): string { - if (amount != null && Math.abs(amount) === 1) { - return text; - } - - if (text.endsWith('y')) { - return `${text.slice(0, -1)}ies`; - } - if (text.endsWith('s')) { - return `${text}es`; - } - return `${text}s`; -} - /** * Converts an object with different types of values into an array of command-line arguments. * diff --git a/testing/setup/package.json b/testing/setup/package.json index c902818..8f03d56 100644 --- a/testing/setup/package.json +++ b/testing/setup/package.json @@ -1,6 +1,6 @@ { "name": "@push-based/testing-setup", - "type": "commonjs", + "type": "module", "version": "0.0.1", "main": "./src/index.mjs", "types": "./src/index.d.ts", diff --git a/tools/nx-advanced-profile.postinstall.js b/tools/nx-advanced-profile.postinstall.js index 07417c0..8172324 100644 --- a/tools/nx-advanced-profile.postinstall.js +++ b/tools/nx-advanced-profile.postinstall.js @@ -1,11 +1,11 @@ import { writeFileSync } from 'node:fs'; import { readFileSync } from 'node:fs'; -// This is adding `require("./../../../../tools/perf_hooks.patch");` to your `node_modules/nx/src/utils/perf-logging.js`. +// This is adding `import("./../../../../tools/perf_hooks.patch");` to your `node_modules/nx/src/utils/perf-logging.js`. writeFileSync( './node_modules/nx/src/utils/perf-logging.js', readFileSync( './node_modules/nx/src/utils/perf-logging.js', 'utf-8', - ).toString() + 'require("./../../../../tools/perf_hooks.patch");', + ).toString() + 'import("./../../../../tools/perf_hooks.patch");', ); diff --git a/tsconfig.json b/tsconfig.json index 99432b1..beb5f3a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,9 +27,6 @@ { "path": "./packages/shared/ds-component-coverage" }, - { - "path": "./packages/shared/angular-cli-utils" - }, { "path": "./testing/vitest-setup" },