diff --git a/.gitignore b/.gitignore index b88e649ab..06761c899 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # See http://help.github.com/ignore-files/ for more about ignoring files. +.env + # compiled output dist tmp diff --git a/examples/cli-e2e/mocks/config.mock.js b/examples/cli-e2e/mocks/code-pushup.config.js similarity index 64% rename from examples/cli-e2e/mocks/config.mock.js rename to examples/cli-e2e/mocks/code-pushup.config.js index a69938b2e..35d2f93d1 100644 --- a/examples/cli-e2e/mocks/config.mock.js +++ b/examples/cli-e2e/mocks/code-pushup.config.js @@ -1,12 +1,13 @@ // TODO: import plugins using NPM package names using local registry: https://github.com/flowup/quality-metrics-cli/issues/33 -import eslintPlugin from '../../../dist/packages/plugin-eslint'; +// import eslintPlugin from '../../../dist/packages/plugin-eslint'; import lighthousePlugin from '../../../dist/packages/plugin-lighthouse'; export default { persist: { outputPath: 'tmp/cli-config-out.json' }, categories: [], plugins: [ - await eslintPlugin({ eslintrc: '.eslintrc.json', patterns: '**/*.ts' }), + // TODO: uncomment once runner is implemented + // await eslintPlugin({ eslintrc: '.eslintrc.json', patterns: '**/*.ts' }), lighthousePlugin({ config: '.lighthouserc.json' }), ], }; diff --git a/examples/cli-e2e/mocks/code-pushup.config.mjs b/examples/cli-e2e/mocks/code-pushup.config.mjs new file mode 100644 index 000000000..35d2f93d1 --- /dev/null +++ b/examples/cli-e2e/mocks/code-pushup.config.mjs @@ -0,0 +1,13 @@ +// TODO: import plugins using NPM package names using local registry: https://github.com/flowup/quality-metrics-cli/issues/33 +// import eslintPlugin from '../../../dist/packages/plugin-eslint'; +import lighthousePlugin from '../../../dist/packages/plugin-lighthouse'; + +export default { + persist: { outputPath: 'tmp/cli-config-out.json' }, + categories: [], + plugins: [ + // TODO: uncomment once runner is implemented + // await eslintPlugin({ eslintrc: '.eslintrc.json', patterns: '**/*.ts' }), + lighthousePlugin({ config: '.lighthouserc.json' }), + ], +}; diff --git a/examples/cli-e2e/mocks/code-pushup.config.ts b/examples/cli-e2e/mocks/code-pushup.config.ts new file mode 100644 index 000000000..35d2f93d1 --- /dev/null +++ b/examples/cli-e2e/mocks/code-pushup.config.ts @@ -0,0 +1,13 @@ +// TODO: import plugins using NPM package names using local registry: https://github.com/flowup/quality-metrics-cli/issues/33 +// import eslintPlugin from '../../../dist/packages/plugin-eslint'; +import lighthousePlugin from '../../../dist/packages/plugin-lighthouse'; + +export default { + persist: { outputPath: 'tmp/cli-config-out.json' }, + categories: [], + plugins: [ + // TODO: uncomment once runner is implemented + // await eslintPlugin({ eslintrc: '.eslintrc.json', patterns: '**/*.ts' }), + lighthousePlugin({ config: '.lighthouserc.json' }), + ], +}; diff --git a/examples/cli-e2e/mocks/config.mock.mjs b/examples/cli-e2e/mocks/config.mock.mjs deleted file mode 100644 index 54e3120ae..000000000 --- a/examples/cli-e2e/mocks/config.mock.mjs +++ /dev/null @@ -1,12 +0,0 @@ -// TODO: import plugins as NPM package names using local registry: https://github.com/flowup/quality-metrics-cli/issues/33 -import eslintPlugin from '../../../dist/packages/plugin-eslint'; -import lighthousePlugin from '../../../dist/packages/plugin-lighthouse'; - -export default { - persist: { outputPath: 'tmp/cli-config-out.json' }, - categories: [], - plugins: [ - await eslintPlugin({ eslintrc: '.eslintrc.json', patterns: '**/*.ts' }), - lighthousePlugin({ config: '.lighthouserc.json' }), - ], -}; diff --git a/examples/cli-e2e/mocks/config.mock.ts b/examples/cli-e2e/mocks/config.mock.ts deleted file mode 100644 index 54e3120ae..000000000 --- a/examples/cli-e2e/mocks/config.mock.ts +++ /dev/null @@ -1,12 +0,0 @@ -// TODO: import plugins as NPM package names using local registry: https://github.com/flowup/quality-metrics-cli/issues/33 -import eslintPlugin from '../../../dist/packages/plugin-eslint'; -import lighthousePlugin from '../../../dist/packages/plugin-lighthouse'; - -export default { - persist: { outputPath: 'tmp/cli-config-out.json' }, - categories: [], - plugins: [ - await eslintPlugin({ eslintrc: '.eslintrc.json', patterns: '**/*.ts' }), - lighthousePlugin({ config: '.lighthouserc.json' }), - ], -}; diff --git a/examples/cli-e2e/project.json b/examples/cli-e2e/project.json index fa0f8c784..fd54d27fb 100644 --- a/examples/cli-e2e/project.json +++ b/examples/cli-e2e/project.json @@ -13,5 +13,6 @@ } } }, + "implicitDependencies": ["cli", "plugin-eslint", "plugin-lighthouse"], "tags": ["scope:core", "scope:plugin", "type:e2e"] } diff --git a/examples/cli-e2e/tests/cli.spec.ts b/examples/cli-e2e/tests/cli.spec.ts index 89e8f8d6f..ebd593ba0 100644 --- a/examples/cli-e2e/tests/cli.spec.ts +++ b/examples/cli-e2e/tests/cli.spec.ts @@ -1,43 +1,33 @@ -import { cli } from '@quality-metrics/cli'; -import eslintPlugin from '@quality-metrics/eslint-plugin'; -import lighthousePlugin from '@quality-metrics/lighthouse-plugin'; +import { + CliArgsObject, + executeProcess, + objectToCliArgs, +} from '@code-pushup/utils'; import { join } from 'path'; -import { describe, expect, it } from 'vitest'; const configFile = (ext: 'ts' | 'js' | 'mjs') => - join(process.cwd(), `examples/cli-e2e/mocks/config.mock.${ext}`); + join(process.cwd(), `examples/cli-e2e/mocks/code-pushup.config.${ext}`); + +const execCli = (argObj: Partial) => + executeProcess({ + command: 'node', + args: objectToCliArgs({ + _: './dist/packages/cli/index.js', + verbose: true, + ...argObj, + }), + }); describe('cli', () => { it('should load .js config file', async () => { - const argv = await cli(['--configPath', configFile('js'), '--verbose']) - .argv; - expect(argv.plugins[0]).toEqual( - await eslintPlugin({ eslintrc: '.eslintrc.json', patterns: '**/*.ts' }), - ); - expect(argv.plugins[1]).toEqual( - lighthousePlugin({ config: '.lighthouserc.json' }), - ); + await execCli({ configPath: configFile('js') }); }); it('should load .mjs config file', async () => { - const argv = await cli(['--configPath', configFile('mjs'), '--verbose']) - .argv; - expect(argv.plugins[0]).toEqual( - await eslintPlugin({ eslintrc: '.eslintrc.json', patterns: '**/*.ts' }), - ); - expect(argv.plugins[1]).toEqual( - lighthousePlugin({ config: '.lighthouserc.json' }), - ); + await execCli({ configPath: configFile('mjs') }); }); it('should load .ts config file', async () => { - const argv = await cli(['--configPath', configFile('ts'), '--verbose']) - .argv; - expect(argv.plugins[0]).toEqual( - await eslintPlugin({ eslintrc: '.eslintrc.json', patterns: '**/*.ts' }), - ); - expect(argv.plugins[1]).toEqual( - lighthousePlugin({ config: '.lighthouserc.json' }), - ); + await execCli({ configPath: configFile('ts') }); }); }); diff --git a/examples/cli-e2e/tsconfig.spec.json b/examples/cli-e2e/tsconfig.spec.json index 6d3be7427..fbfa3a9da 100644 --- a/examples/cli-e2e/tsconfig.spec.json +++ b/examples/cli-e2e/tsconfig.spec.json @@ -6,14 +6,14 @@ }, "include": [ "vite.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/**/*.d.ts" + "tests/**/*.test.ts", + "tests/**/*.spec.ts", + "tests/**/*.test.tsx", + "tests/**/*.spec.tsx", + "tests/**/*.test.js", + "tests/**/*.spec.js", + "tests/**/*.test.jsx", + "tests/**/*.spec.jsx", + "tests/**/*.d.ts" ] } diff --git a/package-lock.json b/package-lock.json index 9d7703db1..48b62c4da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@quality-metrics-cli/source", + "name": "@code-pushup/cli-source", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@quality-metrics-cli/source", + "name": "@code-pushup/cli-source", "version": "0.0.0", "license": "MIT", "dependencies": { @@ -13,6 +13,7 @@ "bundle-require": "^4.0.1", "chalk": "^5.3.0", "cliui": "^8.0.1", + "simple-git": "^3.20.0", "yargs": "^17.7.2", "zod": "^3.22.4" }, @@ -36,6 +37,7 @@ "@vitest/coverage-c8": "~0.32.0", "@vitest/ui": "~0.32.0", "commitizen": "^4.3.0", + "dotenv": "^16.3.1", "esbuild": "^0.17.17", "eslint": "~8.46.0", "eslint-plugin-react": "^7.33.2", @@ -1908,8 +1910,6 @@ }, "node_modules/@code-pushup/portal-client": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@code-pushup/portal-client/-/portal-client-0.1.2.tgz", - "integrity": "sha512-BND3ss33poWuIycPCrt6m3tSJf13w4HliNc0XKqnPAqLswTMwdaearFeEoBRaMJ+eFCeqOqfAraClbks0c7dqw==", "dependencies": { "graphql-request": "^6.1.0", "graphql-tag": "^2.12.6", @@ -2388,11 +2388,10 @@ }, "node_modules/@esbuild/darwin-arm64": { "version": "0.19.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.4.tgz", - "integrity": "sha512-Lviw8EzxsVQKpbS+rSt6/6zjn9ashUZ7Tbuvc2YENgRl0yZTktGlachZ9KMJUsVjZEGFVu336kl5lBgDN6PmpA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2527,8 +2526,7 @@ }, "node_modules/@graphql-typed-document-node/core": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", - "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } @@ -3080,6 +3078,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -3833,11 +3842,10 @@ }, "node_modules/@nx/nx-darwin-arm64": { "version": "16.9.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.9.1.tgz", - "integrity": "sha512-JWGrPxxt3XjgIYzvnaNAeNmK24wyF6yEE1bV+wnnKzd7yavVps3c2TOVE/AT4sgvdVj3xFzztyixYGV58tCYrg==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -7180,7 +7188,6 @@ }, "node_modules/debug": { "version": "4.3.4", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -7718,11 +7725,10 @@ }, "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", - "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -8413,9 +8419,8 @@ }, "node_modules/fast-diff": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true, + "license": "Apache-2.0", "peer": true }, "node_modules/fast-fifo": { @@ -9148,8 +9153,7 @@ }, "node_modules/graphql": { "version": "16.8.1", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", - "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", + "license": "MIT", "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -9157,8 +9161,7 @@ }, "node_modules/graphql-request": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", - "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", + "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" @@ -9169,16 +9172,14 @@ }, "node_modules/graphql-request/node_modules/cross-fetch": { "version": "3.1.8", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", - "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "license": "MIT", "dependencies": { "node-fetch": "^2.6.12" } }, "node_modules/graphql-tag": { "version": "2.12.6", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", - "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "license": "MIT", "dependencies": { "tslib": "^2.1.0" }, @@ -9484,9 +9485,8 @@ }, "node_modules/hyperdyperid": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.18" } @@ -11470,9 +11470,8 @@ }, "node_modules/json-joy": { "version": "9.6.0", - "resolved": "https://registry.npmjs.org/json-joy/-/json-joy-9.6.0.tgz", - "integrity": "sha512-vJtJD89T0OOZFMaENe95xKCOdibMev/lELkclTdhZxLplwbBPxneWNuctUPizk2nLqtGfBxwCXVO42G9LBoFBA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "arg": "^5.0.2", "hyperdyperid": "^1.2.0" @@ -11501,9 +11500,8 @@ }, "node_modules/json-joy/node_modules/arg": { "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -11865,9 +11863,8 @@ }, "node_modules/lodash.clonedeep": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/lodash.debounce": { @@ -11889,9 +11886,8 @@ }, "node_modules/lodash.isequal": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/lodash.isfunction": { @@ -12154,9 +12150,8 @@ }, "node_modules/memfs": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.5.0.tgz", - "integrity": "sha512-8QePW5iXi/ZCySFTo39h3ujKGT0rYVnZywuSo5AzR7POAuy4uBEFZKziYkkrlGdWuxACUxKAJ0L/sry3DSG+TA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "json-joy": "^9.2.0", "thingies": "^1.11.1" @@ -12385,7 +12380,6 @@ }, "node_modules/ms": { "version": "2.1.2", - "dev": true, "license": "MIT" }, "node_modules/mute-stream": { @@ -12659,12 +12653,11 @@ }, "node_modules/nx/node_modules/@nx/nx-darwin-arm64": { "version": "16.7.4", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.7.4.tgz", - "integrity": "sha512-pRNjxn6KlcR6iGkU1j/1pzcogwXFv97pYiZaibpF7UV0vfdEUA3EETpDcs+hbNAcKMvVtn/TgN857/5LQ/lGUg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -13768,9 +13761,8 @@ }, "node_modules/quill-delta": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", - "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "fast-diff": "^1.3.0", @@ -14516,6 +14508,20 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-git": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.20.0.tgz", + "integrity": "sha512-ozK8tl2hvLts8ijTs18iFruE+RoqmC/mqZhjs/+V7gS5W68JpJ3+FCTmLVqmR59MaUQ52MfGQuWsIqfsTbbJ0Q==", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, "node_modules/sirv": { "version": "2.0.3", "dev": true, @@ -15017,9 +15023,8 @@ }, "node_modules/thingies": { "version": "1.12.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.12.0.tgz", - "integrity": "sha512-AiGqfYC1jLmJagbzQGuoZRM48JPsr9yB734a7K6wzr34NMhjUPrWSQrkF7ZBybf3yCerCL2Gcr02kMv4NmaZfA==", "dev": true, + "license": "Unlicense", "engines": { "node": ">=10.18" }, diff --git a/package.json b/package.json index 7a0e26516..d634b4462 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@quality-metrics-cli/source", + "name": "@code-pushup/cli-source", "version": "0.0.0", "license": "MIT", "scripts": { @@ -15,6 +15,7 @@ "bundle-require": "^4.0.1", "chalk": "^5.3.0", "cliui": "^8.0.1", + "simple-git": "^3.20.0", "yargs": "^17.7.2", "zod": "^3.22.4" }, @@ -38,6 +39,7 @@ "@vitest/coverage-c8": "~0.32.0", "@vitest/ui": "~0.32.0", "commitizen": "^4.3.0", + "dotenv": "^16.3.1", "esbuild": "^0.17.17", "eslint": "~8.46.0", "eslint-plugin-react": "^7.33.2", diff --git a/packages/cli/README.md b/packages/cli/README.md index ad00a9dc6..fd939bee6 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,3 +1,5 @@ -# @quality-metrics/cli +# @code-pushup/cli -TODO: docs +**TODO:** + +- if the `exec` is not working run `npx clear-npx-cache` diff --git a/packages/cli/code-pushup.config.ts b/packages/cli/code-pushup.config.ts new file mode 100644 index 000000000..c9c216aaa --- /dev/null +++ b/packages/cli/code-pushup.config.ts @@ -0,0 +1,105 @@ +const outputPath = 'tmp'; + +export default { + upload: { + organization: 'code-pushup', + project: 'cli', + apiKey: 'dummy-api-key', + server: 'https://example.com/api', + }, + persist: { outputPath }, + plugins: [ + { + slug: 'dummy-plugin', + title: 'Dummy Plugin', + icon: 'javascript', + docsUrl: 'http://www.my-docs.dev?slug=dummy-plugin', + audits: [ + { + slug: 'dummy-audit-1', + title: 'Dummy Audit 1', + description: 'A dummy audit to fill the void 1', + label: '???', + docsUrl: 'http://www.my-docs.dev?slug=dummy-audit-1', + }, + { + slug: 'dummy-audit-2', + title: 'Dummy Audit 2', + description: 'A dummy audit to fill the void 2', + label: '???', + docsUrl: 'http://www.my-docs.dev?slug=dummy-audit-2', + }, + { + slug: 'dummy-audit-3', + title: 'Dummy Audit 3', + description: 'A dummy audit to fill the void 3', + label: '???', + docsUrl: 'http://www.my-docs.dev?slug=dummy-audit-3', + }, + ], + runner: { + command: 'node', + args: [ + '-e', + `require('fs').writeFileSync('${outputPath}/dummy-plugin-output.json', '${JSON.stringify( + [ + { + title: 'Dummy Audit 1', + slug: 'dummy-audit-1', + value: 420, + score: 0.42, + }, + { + title: 'Dummy Audit 2', + slug: 'dummy-audit-2', + value: 80, + score: 0, + }, + { + title: 'Dummy Audit 3', + slug: 'dummy-audit-3', + value: 12, + score: 0.12, + }, + ], + )}')`, + ], + outputPath: `${outputPath}/dummy-plugin-output.json`, + }, + }, + ], + categories: [ + { + slug: 'dummy-category-1', + title: 'Dummy Category 1', + description: 'A dummy audit to fill the void', + refs: [ + { + plugin: 'dummy-plugin', + type: 'audit', + slug: 'dummy-audit-1', + weight: 1, + }, + { + plugin: 'dummy-plugin', + type: 'audit', + slug: 'dummy-audit-2', + weight: 6, + }, + ], + }, + { + slug: 'dummy-category-2', + title: 'Dummy Category 2', + description: 'A dummy audit to fill the void 2', + refs: [ + { + plugin: 'dummy-plugin', + type: 'audit', + slug: 'dummy-audit-3', + weight: 3, + }, + ], + }, + ], +}; diff --git a/packages/cli/package.json b/packages/cli/package.json index 204c52d65..ccb17365e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -2,14 +2,13 @@ "name": "@code-pushup/cli", "version": "0.0.1", "bin": { - "code-pushup": "src/bin.js" + "code-pushup": "index.js" }, "dependencies": { - "chalk": "^5.3.0", - "yargs": "^17.7.2", - "zod": "^3.22.1", "@code-pushup/models": "*", "@code-pushup/core": "*", - "@code-pushup/utils": "*" + "yargs": "^17.7.2", + "zod": "^3.22.1", + "chalk": "^5.3.0" } } diff --git a/packages/cli/project.json b/packages/cli/project.json index f679ad0d3..4844d3cd2 100644 --- a/packages/cli/project.json +++ b/packages/cli/project.json @@ -20,7 +20,7 @@ "dependsOn": ["build"] }, "exec": { - "command": "node dist/packages/cli/index.js", + "command": "npx dist/packages/cli --configPath=./packages/cli/code-pushup.config.ts", "dependsOn": ["build"] }, "lint": { diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts deleted file mode 100644 index 9f191acc0..000000000 --- a/packages/cli/src/bin.ts +++ /dev/null @@ -1,6 +0,0 @@ -#! /usr/bin/env node -import { cli } from './index'; -import { hideBin } from 'yargs/helpers'; - -// bootstrap yargs; format arguments -cli(hideBin(process.argv)).argv; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index fe5203fc4..70b839b4f 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,17 +1,6 @@ -import { yargsCli } from './lib/cli'; -import { options } from './lib/options'; -import { middlewares } from './lib/middlewares'; -import { commands } from './lib/commands'; +#! /usr/bin/env node +import { cli } from './lib/cli'; +import { hideBin } from 'yargs/helpers'; -export { options } from './lib/options'; -export { middlewares } from './lib/middlewares'; -export { commands } from './lib/commands'; - -export const cli = (args: string[]) => - yargsCli(args, { - usageMessage: 'Code PushUp CLI', - scriptName: 'code-pushup', - options, - middlewares, - commands, - }); +// bootstrap yargs; format arguments +cli(hideBin(process.argv)).argv; diff --git a/packages/cli/src/lib/cli.spec.ts b/packages/cli/src/lib/cli.spec.ts deleted file mode 100644 index 91e2e9aa7..000000000 --- a/packages/cli/src/lib/cli.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { CollectOptions } from '@quality-metrics/utils'; -import { join } from 'path'; -import { describe, expect, it } from 'vitest'; -import { yargsCli } from './cli'; -import { getDirname } from './implementation/helper.mock'; -import { middlewares } from './middlewares'; -import { GlobalOptions } from './model'; -import { options as defaultOptions } from './options'; - -const __dirname = getDirname(import.meta.url); -const withDirName = (path: string) => join(__dirname, path); -const validConfigPath = withDirName('implementation/mock/cli-config.mock.js'); - -const options = defaultOptions; -const demandCommand: [number, string] = [0, 'no command required']; - -describe('CLI arguments parsing', () => { - it('options should provide correct defaults', async () => { - const args: string[] = []; - const parsedArgv = yargsCli(args, { - options, - demandCommand, - }).argv as unknown as GlobalOptions; - expect(parsedArgv.configPath).toContain('code-pushup.config.js'); - expect(parsedArgv.verbose).toBe(false); - expect(parsedArgv.interactive).toBe(true); - }); - - it('options should parse correctly', async () => { - const args: string[] = [ - '--verbose', - '--no-interactive', - '--configPath', - validConfigPath, - ]; - - const parsedArgv = yargsCli(args, { - options, - demandCommand, - }).argv as unknown as GlobalOptions & CollectOptions; - expect(parsedArgv.configPath).toContain(validConfigPath); - expect(parsedArgv.verbose).toBe(true); - expect(parsedArgv.interactive).toBe(false); - }); - - it('middleware should use config correctly', async () => { - const args: string[] = ['--configPath', validConfigPath]; - const parsedArgv = (await yargsCli(args, { - demandCommand, - middlewares, - }).argv) as unknown as GlobalOptions & CollectOptions; - expect(parsedArgv.configPath).toContain(validConfigPath); - expect(parsedArgv.persist.outputPath).toContain('cli-config-out.json'); - }); -}); diff --git a/packages/cli/src/lib/cli.ts b/packages/cli/src/lib/cli.ts index cc0d34798..1a467821e 100644 --- a/packages/cli/src/lib/cli.ts +++ b/packages/cli/src/lib/cli.ts @@ -1,85 +1,17 @@ -import { CoreConfig, GlobalOptions } from '@quality-metrics/models'; -import chalk from 'chalk'; -import yargs, { - Argv, - CommandModule, - MiddlewareFunction, - Options, - ParserConfigurationOptions, -} from 'yargs'; -import { logErrorBeforeThrow } from './implementation/utils'; +import { yargsCli } from './yargs-cli'; +import { options } from './options'; +import { middlewares } from './middlewares'; +import { commands } from './commands'; -/** - * returns configurable yargs CLI for code-pushup - * - * @example - * yargsCli(hideBin(process.argv)) - * // bootstrap CLI; format arguments - * .argv; - */ -export function yargsCli( - argv: string[], - cfg: { - usageMessage?: string; - scriptName?: string; - commands?: CommandModule[]; - demandCommand?: [number, string]; - options?: { [key: string]: Options }; - middlewares?: { - middlewareFunction: MiddlewareFunction; - applyBeforeValidation?: boolean; - }[]; - }, -): Argv { - const { usageMessage, scriptName } = cfg; - let { commands, options, middlewares /*demandCommand*/ } = cfg; - // demandCommand = Array.isArray(demandCommand) ? demandCommand: [1, 'Minimum 1 command!']; @TODO implement when commands are present - commands = Array.isArray(commands) ? commands : []; - middlewares = Array.isArray(middlewares) ? middlewares : []; - options = options || {}; - const cli = yargs(argv); +export { options } from './options'; +export { middlewares } from './middlewares'; +export { commands } from './commands'; - // setup yargs - cli - .help() - .alias('h', 'help') - .parserConfiguration({ - 'strip-dashed': true, - } satisfies Partial) - .options(options); - //.demandCommand(...demandCommand); - - // usage message - if (usageMessage) { - cli.usage(chalk.bold(usageMessage)); - } - - // script name - if (scriptName) { - cli.scriptName(scriptName); - } - - // add middlewares - middlewares.forEach(({ middlewareFunction, applyBeforeValidation }) => { - cli.middleware( - logErrorBeforeThrow(middlewareFunction), - applyBeforeValidation, - ); +export const cli = (args: string[]) => + yargsCli(args, { + usageMessage: 'Code PushUp CLI', + scriptName: 'code-pushup', + options, + middlewares, + commands, }); - - // add commands - commands.forEach(commandObj => { - cli.command({ - ...commandObj, - ...(commandObj.handler && { - handler: logErrorBeforeThrow(commandObj.handler), - }), - ...(typeof commandObj.builder === 'function' && { - builder: logErrorBeforeThrow(commandObj.builder), - }), - }); - }); - - // return CLI object - return cli as unknown as Argv; -} diff --git a/packages/cli/src/lib/collect/command-object.spec.ts b/packages/cli/src/lib/collect/command-object.spec.ts index 5e714c29d..f10fb088f 100644 --- a/packages/cli/src/lib/collect/command-object.spec.ts +++ b/packages/cli/src/lib/collect/command-object.spec.ts @@ -1,56 +1,41 @@ -import { Report } from '@quality-metrics/models'; -import { dummyConfig } from '@quality-metrics/models/testing'; -import { CollectOptions } from '@quality-metrics/core'; -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { yargsCli } from '../cli'; -import { logErrorBeforeThrow } from '../implementation/utils'; +import { yargsCli } from '../yargs-cli'; import { middlewares } from '../middlewares'; -import { yargsGlobalOptionsDefinition } from '../implementation/global-options'; +import { options } from '../options'; +import { objectToCliArgs } from '@code-pushup/utils'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; import { yargsCollectCommandObject } from './command-object'; -import { getDirname } from '../implementation/helper.mock'; -const command = { - ...yargsCollectCommandObject(), - handler: logErrorBeforeThrow(yargsCollectCommandObject().handler), -}; - -const outputPath = 'tmp'; -const reportPath = (format: 'json' | 'md' = 'json') => - join(outputPath, 'report.' + format); - -const config = dummyConfig(); - -describe('collect-command-object', () => { - it('should parse arguments correctly', async () => { - const args = ['collect', '--verbose', '--configPath', '']; - const cli = yargsCli(args, { options: yargsGlobalOptionsDefinition() }) - .config(config) - .command(command); - const parsedArgv = (await cli.argv) as unknown as CollectOptions; - const { persist } = parsedArgv; - const { outputPath: outPath } = persist; - expect(outPath).toBe(outputPath); +const baseArgs = [ + ...objectToCliArgs({ + verbose: true, + configPath: join( + fileURLToPath(dirname(import.meta.url)), + '..', + '..', + '..', + 'test', + 'config.mock.ts', + ), + }), +]; +const cli = (args: string[]) => + yargsCli(['collect', ...args], { + options, + middlewares, + commands: [yargsCollectCommandObject()], }); - it('should execute middleware correctly', async () => { +describe('collect-command-object', () => { + it('should override config with CLI arguments', async () => { const args = [ - 'collect', - '--configPath', - join( - getDirname(import.meta.url), - '..', - 'implementation', - 'mock', - 'config-middleware-config.mock.mjs', - ), + ...baseArgs, + ...objectToCliArgs({ + format: 'md', + }), ]; - await yargsCli([], { middlewares }) - .config(config) - .command(command) - .parseAsync(args); - const report = JSON.parse(readFileSync(reportPath()).toString()) as Report; - expect(report.plugins[0]?.slug).toBe('plg-0'); - expect(report.plugins[0]?.audits[0]?.slug).toBe('0a'); + const parsedArgv = await cli(args).parseAsync(); + expect(parsedArgv.persist.outputPath).toBe('tmp'); + expect(parsedArgv.persist?.format).toEqual(['md']); }); }); diff --git a/packages/cli/src/lib/collect/command-object.ts b/packages/cli/src/lib/collect/command-object.ts index d35b99848..4d900e54b 100644 --- a/packages/cli/src/lib/collect/command-object.ts +++ b/packages/cli/src/lib/collect/command-object.ts @@ -1,5 +1,5 @@ import { CommandModule } from 'yargs'; -import { collectAndPersistReports } from '@quality-metrics/core'; +import { collectAndPersistReports } from '@code-pushup/core'; export function yargsCollectCommandObject() { return { diff --git a/packages/cli/src/lib/commands.ts b/packages/cli/src/lib/commands.ts index 24a3d9646..91ab66345 100644 --- a/packages/cli/src/lib/commands.ts +++ b/packages/cli/src/lib/commands.ts @@ -1,4 +1,12 @@ import { CommandModule } from 'yargs'; import { yargsCollectCommandObject } from './collect/command-object'; +import { yargsUploadCommandObject } from './upload/command-object'; -export const commands: CommandModule[] = [yargsCollectCommandObject()]; +export const commands: CommandModule[] = [ + { + ...yargsCollectCommandObject(), + command: '*', + }, + yargsCollectCommandObject(), + yargsUploadCommandObject(), +]; diff --git a/packages/cli/src/lib/implementation/config-middleware.spec.ts b/packages/cli/src/lib/implementation/config-middleware.spec.ts index a260974d0..e745977a4 100644 --- a/packages/cli/src/lib/implementation/config-middleware.spec.ts +++ b/packages/cli/src/lib/implementation/config-middleware.spec.ts @@ -1,49 +1,47 @@ import { join } from 'path'; import { expect } from 'vitest'; -import { configMiddleware, ConfigParseError } from './config-middleware'; +import { configMiddleware } from './config-middleware'; import { getDirname } from './helper.mock'; const __dirname = getDirname(import.meta.url); const withDirName = (path: string) => join(__dirname, path); const configPath = (ext: string) => - `${withDirName('mock/config-middleware-config.mock.')}${ext}`; + `${withDirName('../../../test/config.mock.')}${ext}`; describe('applyConfigMiddleware', () => { it('should load valid .mjs config', async () => { const configPathMjs = configPath('mjs'); const config = await configMiddleware({ configPath: configPathMjs }); - expect(config.configPath).toContain('.mjs'); - expect(config.persist.outputPath).toContain('mjs-'); + expect(config.upload.project).toContain('mjs'); + expect(config.persist.outputPath).toContain('tmp'); }); it('should load valid .cjs config', async () => { const configPathCjs = configPath('cjs'); const config = await configMiddleware({ configPath: configPathCjs }); - expect(config.configPath).toContain('.cjs'); - expect(config.persist.outputPath).toContain('cjs-'); + expect(config.upload.project).toContain('cjs'); + expect(config.persist.outputPath).toContain('tmp'); }); it('should load valid .js config', async () => { const configPathJs = configPath('js'); const config = await configMiddleware({ configPath: configPathJs }); - expect(config.configPath).toContain('.js'); - expect(config.persist.outputPath).toContain('js-'); + expect(config.upload.project).toContain('js'); + expect(config.persist.outputPath).toContain('tmp'); }); it('should throw with invalid configPath', async () => { const configPath = 'wrong/path/to/config'; let error: Error = new Error(); await configMiddleware({ configPath }).catch(e => (error = e)); - expect(error?.message).toContain(new ConfigParseError(configPath).message); + expect(error?.message).toContain(configPath); }); it('should provide default configPath', async () => { const defaultConfigPath = 'code-pushup.config.js'; let error: Error = new Error(); await configMiddleware({ configPath: undefined }).catch(e => (error = e)); - expect(error?.message).toContain( - new ConfigParseError(defaultConfigPath).message, - ); + expect(error?.message).toContain(defaultConfigPath); }); }); diff --git a/packages/cli/src/lib/implementation/config-middleware.ts b/packages/cli/src/lib/implementation/config-middleware.ts index 103fda1d8..f2aba4650 100644 --- a/packages/cli/src/lib/implementation/config-middleware.ts +++ b/packages/cli/src/lib/implementation/config-middleware.ts @@ -1,18 +1,50 @@ -import { GlobalOptions, globalOptionsSchema } from '@quality-metrics/models'; -import { readCodePushupConfig } from './read-code-pushup-config'; +import { GlobalOptions, globalOptionsSchema } from '@code-pushup/models'; +import { ArgsCliObj, CommandBase } from './model'; +import { readCodePushupConfig } from '@code-pushup/core'; -export class ConfigParseError extends Error { - constructor(configPath: string) { - super(`Config file ${configPath} does not exist`); - } +export async function configMiddleware(processArgs: T) { + const args = processArgs as T; + const { configPath, ...cliOptions }: GlobalOptions = + globalOptionsSchema.parse(args); + const importedRc = await readCodePushupConfig(configPath); + const cliConfigArgs = readCoreConfigFromCliArgs(processArgs); + const parsedProcessArgs: CommandBase = { + ...cliOptions, + ...(importedRc || {}), + upload: { + ...importedRc.upload, + ...cliConfigArgs.upload, + }, + persist: { + ...importedRc.persist, + ...cliConfigArgs.persist, + }, + plugins: importedRc.plugins, + categories: importedRc.categories, + }; + + return parsedProcessArgs; } -export async function configMiddleware(processArgs: T) { - const globalOptions: GlobalOptions = globalOptionsSchema.parse(processArgs); - const importedRc = await readCodePushupConfig(globalOptions.configPath); - return { - ...importedRc, - ...processArgs, - ...globalOptions, - }; +function readCoreConfigFromCliArgs(args: ArgsCliObj): CommandBase { + const parsedProcessArgs = { upload: {}, persist: {} } as CommandBase; + for (const key in args) { + const k = key as keyof ArgsCliObj; + switch (key) { + case 'organization': + case 'project': + case 'server': + case 'apiKey': + parsedProcessArgs.upload[k] = args[k]; + break; + case 'outputPath': + case 'format': + parsedProcessArgs.persist[k] = args[k]; + break; + default: + break; + } + } + + return parsedProcessArgs; } diff --git a/packages/cli/src/lib/implementation/core-config-options.ts b/packages/cli/src/lib/implementation/core-config-options.ts new file mode 100644 index 000000000..81cd11b71 --- /dev/null +++ b/packages/cli/src/lib/implementation/core-config-options.ts @@ -0,0 +1,18 @@ +import { Options } from 'yargs'; +import { ArgsCliObj } from './model'; + +export function yargsCoreConfigOptionsDefinition(): Record< + keyof ArgsCliObj, + Options +> { + return { + format: { + describe: 'Format of the report output. e.g. `md`, `json`, `stdout`', + type: 'array', + }, + apiKey: { + describe: 'apiKey for the portal', + type: 'string', + }, + } as unknown as Record; +} diff --git a/packages/cli/src/lib/implementation/mock/cli-config.mock.js b/packages/cli/src/lib/implementation/mock/cli-config.mock.js deleted file mode 100644 index 3b3d09df8..000000000 --- a/packages/cli/src/lib/implementation/mock/cli-config.mock.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - persist: { outputPath: 'cli-config-out.json' }, - categories: [], - plugins: [ - { - audits: [], - runner: { - command: 'node', - args: [ - '-e', - `require('fs').writeFileSync('tmp/cli-config-out.json', '${JSON.stringify( - { audits: [] }, - )}')`, - ], - outputPath: 'tmp/cli-config-out.json', - }, - slug: 'execute-plugin', - title: 'execute plugin', - icon: 'javascript', - }, - ], -}; diff --git a/packages/cli/src/lib/implementation/mock/config-middleware-config.mock.cjs b/packages/cli/src/lib/implementation/mock/config-middleware-config.mock.cjs deleted file mode 100644 index 819d538d3..000000000 --- a/packages/cli/src/lib/implementation/mock/config-middleware-config.mock.cjs +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - persist: { outputPath: 'tmp/cjs-out.json' }, - plugins: [], - categories: [], -}; diff --git a/packages/cli/src/lib/implementation/mock/config-middleware-config.mock.js b/packages/cli/src/lib/implementation/mock/config-middleware-config.mock.js deleted file mode 100644 index 72c44fe8a..000000000 --- a/packages/cli/src/lib/implementation/mock/config-middleware-config.mock.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - persist: { outputPath: 'tmp/js-out.json' }, - plugins: [], - categories: [], -}; diff --git a/packages/cli/src/lib/implementation/mock/config-middleware-config.mock.mjs b/packages/cli/src/lib/implementation/mock/config-middleware-config.mock.mjs deleted file mode 100644 index f8cc5b082..000000000 --- a/packages/cli/src/lib/implementation/mock/config-middleware-config.mock.mjs +++ /dev/null @@ -1,5 +0,0 @@ -export default { - persist: { outputPath: 'tmp/mjs-out.json' }, - plugins: [], - categories: [], -}; diff --git a/packages/cli/src/lib/implementation/model.ts b/packages/cli/src/lib/implementation/model.ts index 3f021cc82..e2168d40d 100644 --- a/packages/cli/src/lib/implementation/model.ts +++ b/packages/cli/src/lib/implementation/model.ts @@ -1,9 +1,13 @@ import { + Format, globalOptionsSchema as coreGlobalOptionsSchema, + PersistConfig, refineCoreConfig, unrefinedCoreConfigSchema, -} from '@quality-metrics/models'; + UploadConfig, +} from '@code-pushup/models'; import { z } from 'zod'; +import { GlobalOptions as CliOptions } from '../model'; export const globalOptionsSchema = coreGlobalOptionsSchema.merge( z.object({ @@ -18,7 +22,16 @@ export const globalOptionsSchema = coreGlobalOptionsSchema.merge( export type GlobalOptions = z.infer; +// @TODO this has any type export const commandBaseSchema = refineCoreConfig( globalOptionsSchema.merge(unrefinedCoreConfigSchema), ); export type CommandBase = z.infer; +export type ArgsCliObj = Partial< + CliOptions & + GlobalOptions & + UploadConfig & + Omit & { + format: Format | Format[]; + } +>; diff --git a/packages/cli/src/lib/implementation/read-code-pushup-config.ts b/packages/cli/src/lib/implementation/read-code-pushup-config.ts deleted file mode 100644 index abec849f9..000000000 --- a/packages/cli/src/lib/implementation/read-code-pushup-config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CoreConfig, coreConfigSchema } from '@quality-metrics/models'; -import { stat } from 'fs/promises'; -import { importModule } from '@quality-metrics/utils'; -import { ConfigParseError } from './config-middleware'; - -// @TODO [73] move into core -export async function readCodePushupConfig(filepath: string) { - try { - const stats = await stat(filepath); - if (!stats.isFile) { - throw new ConfigParseError(filepath); - } - } catch (err) { - throw new ConfigParseError(filepath); - } - - return importModule( - { - filepath, - }, - coreConfigSchema.parse, - ); -} diff --git a/packages/cli/src/lib/model.ts b/packages/cli/src/lib/model.ts index a9a03c7d4..2ff2531e7 100644 --- a/packages/cli/src/lib/model.ts +++ b/packages/cli/src/lib/model.ts @@ -1,4 +1,4 @@ -import { globalOptionsSchema as coreGlobalOptionsSchema } from '@quality-metrics/models'; +import { globalOptionsSchema as coreGlobalOptionsSchema } from '@code-pushup/models'; import { z } from 'zod'; export const globalOptionsSchema = coreGlobalOptionsSchema.merge( diff --git a/packages/cli/src/lib/options.ts b/packages/cli/src/lib/options.ts index 92370fe49..3ec28ec20 100644 --- a/packages/cli/src/lib/options.ts +++ b/packages/cli/src/lib/options.ts @@ -1,5 +1,7 @@ import { yargsGlobalOptionsDefinition } from './implementation/global-options'; +import { yargsCoreConfigOptionsDefinition } from './implementation/core-config-options'; export const options = { ...yargsGlobalOptionsDefinition(), + ...yargsCoreConfigOptionsDefinition(), }; diff --git a/packages/cli/src/lib/upload/command-object.spec.ts b/packages/cli/src/lib/upload/command-object.spec.ts new file mode 100644 index 000000000..0b931c6bd --- /dev/null +++ b/packages/cli/src/lib/upload/command-object.spec.ts @@ -0,0 +1,102 @@ +import { Report } from '@code-pushup/models'; +import { objectToCliArgs } from '@code-pushup/utils'; +import { + PortalUploadArgs, + ReportFragment, + uploadToPortal, +} from '@code-pushup/portal-client'; +import { writeFile } from 'fs/promises'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import { vi, describe, it, beforeEach } from 'vitest'; +import { yargsGlobalOptionsDefinition } from '../implementation/global-options'; +import { middlewares } from '../middlewares'; +import { yargsCli } from '../yargs-cli'; +import { yargsUploadCommandObject } from './command-object'; + +// This in needed to mock the API client used inside the upload function +vi.mock('@code-pushup/portal-client', async () => { + const module: typeof import('@code-pushup/portal-client') = + await vi.importActual('@code-pushup/portal-client'); + + return { + ...module, + uploadToPortal: vi.fn( + async () => ({ packageName: '@code-pushup/cli' } as ReportFragment), + ), + }; +}); + +const baseArgs = [ + 'upload', + '--verbose', + ...objectToCliArgs({ + configPath: join( + fileURLToPath(dirname(import.meta.url)), + '..', + '..', + '..', + 'test', + 'config.mock.ts', + ), + }), +]; +const cli = (args: string[]) => + yargsCli(args, { + options: yargsGlobalOptionsDefinition(), + middlewares, + commands: [yargsUploadCommandObject()], + }); + +const reportPath = (format: 'json' | 'md' = 'json') => + join('tmp', 'report.' + format); + +describe('upload-command-object', () => { + const dummyReport: Report = { + date: new Date().toISOString(), + duration: 1000, + categories: [], + plugins: [], + packageName: '@code-pushup/cli', + version: '0.1.0', + }; + + beforeEach(async () => { + vi.clearAllMocks(); + await writeFile(reportPath(), JSON.stringify(dummyReport)); + }); + + it('should override config with CLI arguments', async () => { + const args = [ + ...baseArgs, + ...objectToCliArgs({ + apiKey: 'some-other-api-key', + server: 'https://other-example.com/api', + }), + ]; + const parsedArgv = await cli(args).parseAsync(); + expect(parsedArgv.upload?.organization).toBe('code-pushup'); + expect(parsedArgv.upload?.project).toBe('cli'); + expect(parsedArgv.upload?.apiKey).toBe('some-other-api-key'); + expect(parsedArgv.upload?.server).toBe('https://other-example.com/api'); + }); + + it('should call portal-client function with correct parameters', async () => { + await cli(baseArgs).parseAsync(); + expect(uploadToPortal).toHaveBeenCalledWith({ + apiKey: 'dummy-api-key', + server: 'https://example.com/api', + data: { + commandStartDate: dummyReport.date, + commandDuration: 1000, + categories: [], + plugins: [], + packageName: '@code-pushup/cli', + packageVersion: '0.1.0', + organization: 'code-pushup', + project: 'cli', + commit: expect.any(String), + }, + } satisfies PortalUploadArgs); + }); +}); diff --git a/packages/cli/src/lib/upload/command-object.ts b/packages/cli/src/lib/upload/command-object.ts new file mode 100644 index 000000000..aa736cdc6 --- /dev/null +++ b/packages/cli/src/lib/upload/command-object.ts @@ -0,0 +1,10 @@ +import { CommandModule } from 'yargs'; +import { upload } from '@code-pushup/core'; + +export function yargsUploadCommandObject() { + return { + command: 'upload', + describe: 'Upload report results to the portal', + handler: upload as unknown as CommandModule['handler'], + } satisfies CommandModule; +} diff --git a/packages/cli/src/lib/yargs-cli.spec.ts b/packages/cli/src/lib/yargs-cli.spec.ts new file mode 100644 index 000000000..ec72b09e8 --- /dev/null +++ b/packages/cli/src/lib/yargs-cli.spec.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; +import { yargsCli } from './yargs-cli'; +import { objectToCliArgs } from '@code-pushup/utils'; +import { Options } from 'yargs'; + +const options: Record = { + interactive: { + describe: 'When false disables interactive input prompts for options.', + type: 'boolean', + default: true, + }, +}; +const demandCommand: [number, string] = [0, 'no command required']; +function middleware>(processArgs: T) { + return { + ...processArgs, + configPath: '42', + }; +} + +describe('yargsCli', () => { + it('global options should provide correct defaults', async () => { + const args: string[] = []; + const parsedArgv = await yargsCli(args, { + options, + }).parseAsync(); + expect(parsedArgv.interactive).toBe(true); + }); + + it('global options should parse correctly', async () => { + const args: string[] = objectToCliArgs({ + interactive: false, + }); + + const parsedArgv = await yargsCli(args, { + options, + demandCommand, + }).parseAsync(); + expect(parsedArgv.interactive).toBe(false); + }); + + it('global options and middleware handle argument overrides correctly', async () => { + const args: string[] = objectToCliArgs({ + configPath: 'validConfigPath', + }); + const parsedArgv = await yargsCli(args, { + options, + demandCommand, + middlewares: [ + { + middlewareFunction: middleware, + }, + ], + }).parseAsync(); + expect(parsedArgv.configPath).toContain(42); + }); +}); diff --git a/packages/cli/src/lib/yargs-cli.ts b/packages/cli/src/lib/yargs-cli.ts new file mode 100644 index 000000000..33fa33488 --- /dev/null +++ b/packages/cli/src/lib/yargs-cli.ts @@ -0,0 +1,86 @@ +import { CoreConfig } from '@code-pushup/models'; +import chalk from 'chalk'; +import yargs, { + Argv, + CommandModule, + MiddlewareFunction, + Options, + ParserConfigurationOptions, +} from 'yargs'; +import { logErrorBeforeThrow } from './implementation/utils'; +import { GlobalOptions } from './model'; + +/** + * returns configurable yargs CLI for code-pushup + * + * @example + * yargsCli(hideBin(process.argv)) + * // bootstrap CLI; format arguments + * .argv; + */ +export function yargsCli( + argv: string[], + cfg: { + usageMessage?: string; + scriptName?: string; + commands?: CommandModule[]; + demandCommand?: [number, string]; + options?: { [key: string]: Options }; + middlewares?: { + middlewareFunction: unknown; + applyBeforeValidation?: boolean; + }[]; + }, +): Argv { + const { usageMessage, scriptName } = cfg; + let { commands, options, middlewares /*demandCommand*/ } = cfg; + // demandCommand = Array.isArray(demandCommand) ? demandCommand: [1, 'Minimum 1 command!']; @TODO implement when commands are present + commands = Array.isArray(commands) ? commands : []; + middlewares = Array.isArray(middlewares) ? middlewares : []; + options = options || {}; + const cli = yargs(argv); + + // setup yargs + cli + .help() + .alias('h', 'help') + .parserConfiguration({ + 'strip-dashed': true, + } satisfies Partial) + .options(options); + //.demandCommand(...demandCommand); + + // usage message + if (usageMessage) { + cli.usage(chalk.bold(usageMessage)); + } + + // script name + if (scriptName) { + cli.scriptName(scriptName); + } + + // add middlewares + middlewares.forEach(({ middlewareFunction, applyBeforeValidation }) => { + cli.middleware( + logErrorBeforeThrow(middlewareFunction as MiddlewareFunction), + applyBeforeValidation, + ); + }); + + // add commands + commands.forEach(commandObj => { + cli.command({ + ...commandObj, + ...(commandObj.handler && { + handler: logErrorBeforeThrow(commandObj.handler), + }), + ...(typeof commandObj.builder === 'function' && { + builder: logErrorBeforeThrow(commandObj.builder), + }), + }); + }); + + // return CLI object + return cli as unknown as Argv; +} diff --git a/packages/cli/src/lib/collect/_command-object-config.mock.js b/packages/cli/test/config.mock.cjs similarity index 63% rename from packages/cli/src/lib/collect/_command-object-config.mock.js rename to packages/cli/test/config.mock.cjs index f0f5ea48b..30a5f57a5 100644 --- a/packages/cli/src/lib/collect/_command-object-config.mock.js +++ b/packages/cli/test/config.mock.cjs @@ -1,5 +1,12 @@ +const outputPath = 'tmp'; module.exports = { - persist: { outputPath: 'command-object-config-out.json' }, + upload: { + organization: 'code-pushup', + project: 'cli-cjs', + apiKey: 'dummy-api-key', + server: 'https://example.com/api', + }, + persist: { outputPath }, plugins: [ { audits: [ @@ -15,9 +22,10 @@ module.exports = { command: 'node', args: [ '-e', - `require('fs').writeFileSync('tmp/command-object-config-out.json', '${JSON.stringify( + `require('fs').writeFileSync('${outputPath}/out.json', '${JSON.stringify( [ { + title: 'dummy-title', slug: 'command-object-audit-slug', value: 0, score: 0, @@ -25,11 +33,12 @@ module.exports = { ], )}')`, ], - outputPath: 'tmp/command-object-config-out.json', + outputPath: `${outputPath}/out.json`, }, groups: [], slug: 'command-object-plugin', title: 'command-object plugin', + icon: 'javascript', }, ], categories: [], diff --git a/packages/cli/test/config.mock.js b/packages/cli/test/config.mock.js new file mode 100644 index 000000000..85a51e703 --- /dev/null +++ b/packages/cli/test/config.mock.js @@ -0,0 +1,45 @@ +const outputPath = 'tmp'; +export default { + upload: { + organization: 'code-pushup', + project: 'cli-js', + apiKey: 'dummy-api-key', + server: 'https://example.com/api', + }, + persist: { outputPath }, + plugins: [ + { + audits: [ + { + slug: 'command-object-audit-slug', + title: 'audit title', + description: 'audit description', + label: 'mock audit label', + docsUrl: 'http://www.my-docs.dev', + }, + ], + runner: { + command: 'node', + args: [ + '-e', + `require('fs').writeFileSync('${outputPath}/out.json', '${JSON.stringify( + [ + { + title: 'dummy-title', + slug: 'command-object-audit-slug', + value: 0, + score: 0, + }, + ], + )}')`, + ], + outputPath: `${outputPath}/out.json`, + }, + groups: [], + slug: 'command-object-plugin', + title: 'command-object plugin', + icon: 'javascript', + }, + ], + categories: [], +}; diff --git a/packages/cli/test/config.mock.mjs b/packages/cli/test/config.mock.mjs new file mode 100644 index 000000000..8528db442 --- /dev/null +++ b/packages/cli/test/config.mock.mjs @@ -0,0 +1,45 @@ +const outputPath = 'tmp'; +export default { + upload: { + organization: 'code-pushup', + project: 'cli-mjs', + apiKey: 'dummy-api-key', + server: 'https://example.com/api', + }, + persist: { outputPath }, + plugins: [ + { + audits: [ + { + slug: 'command-object-audit-slug', + title: 'audit title', + description: 'audit description', + label: 'mock audit label', + docsUrl: 'http://www.my-docs.dev', + }, + ], + runner: { + command: 'node', + args: [ + '-e', + `require('fs').writeFileSync('${outputPath}/out.json', '${JSON.stringify( + [ + { + title: 'dummy-title', + slug: 'command-object-audit-slug', + value: 0, + score: 0, + }, + ], + )}')`, + ], + outputPath: `${outputPath}/out.json`, + }, + groups: [], + slug: 'command-object-plugin', + title: 'command-object plugin', + icon: 'javascript', + }, + ], + categories: [], +}; diff --git a/packages/cli/test/config.mock.ts b/packages/cli/test/config.mock.ts new file mode 100644 index 000000000..1b2f70ecf --- /dev/null +++ b/packages/cli/test/config.mock.ts @@ -0,0 +1,45 @@ +const outputPath = 'tmp'; +export default { + upload: { + organization: 'code-pushup', + project: 'cli', + apiKey: 'dummy-api-key', + server: 'https://example.com/api', + }, + persist: { outputPath }, + plugins: [ + { + audits: [ + { + slug: 'command-object-audit-slug', + title: 'audit title', + description: 'audit description', + label: 'mock audit label', + docsUrl: 'http://www.my-docs.dev', + }, + ], + runner: { + command: 'node', + args: [ + '-e', + `require('fs').writeFileSync('${outputPath}/out.json', '${JSON.stringify( + [ + { + title: 'dummy-title', + slug: 'command-object-audit-slug', + value: 0, + score: 0, + }, + ], + )}')`, + ], + outputPath: `${outputPath}/out.json`, + }, + groups: [], + slug: 'command-object-plugin', + title: 'command-object plugin', + icon: 'javascript', + }, + ], + categories: [], +}; diff --git a/packages/cli/test/index.ts b/packages/cli/test/index.ts new file mode 100644 index 000000000..a98d4bb02 --- /dev/null +++ b/packages/cli/test/index.ts @@ -0,0 +1 @@ +export { default as configMock } from './config.mock'; diff --git a/packages/cli/tsconfig.lib.json b/packages/cli/tsconfig.lib.json index 74dff64c6..b1a2e0598 100644 --- a/packages/cli/tsconfig.lib.json +++ b/packages/cli/tsconfig.lib.json @@ -10,6 +10,7 @@ "vite.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts", - "src/**/*.mock.ts" + "src/**/*.mock.ts", + "test/**/*.ts" ] } diff --git a/packages/cli/tsconfig.spec.json b/packages/cli/tsconfig.spec.json index 6d3be7427..56845b342 100644 --- a/packages/cli/tsconfig.spec.json +++ b/packages/cli/tsconfig.spec.json @@ -6,6 +6,7 @@ }, "include": [ "vite.config.ts", + "test/**/*.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.test.tsx", diff --git a/packages/core/docs/README.md b/packages/core/docs/README.md new file mode 100644 index 000000000..ff7b7397f --- /dev/null +++ b/packages/core/docs/README.md @@ -0,0 +1,4 @@ +# Notes + +ATM the version in our reports is derived from the `core` package, closest `package.json`. +The pros and cons of this should be analysed and challenged after the rough version is out. diff --git a/packages/core/package.json b/packages/core/package.json index a29372644..60eb09728 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,9 +2,10 @@ "name": "@code-pushup/core", "version": "0.0.1", "dependencies": { - "chalk": "^5.3.0", "@code-pushup/models": "*", - "@code-pushup/utils": "*" + "@code-pushup/utils": "*", + "@code-pushup/portal-client": "^0.1.2", + "chalk": "^5.3.0" }, "type": "commonjs", "main": "./index.cjs" diff --git a/packages/core/project.json b/packages/core/project.json index e4fb2115d..f13dbc1c0 100644 --- a/packages/core/project.json +++ b/packages/core/project.json @@ -12,8 +12,7 @@ "main": "packages/core/src/index.ts", "tsConfig": "packages/core/tsconfig.lib.json", "assets": ["packages/core/*.md"], - "generatePackageJson": true, - "format": ["cjs"] + "esbuildConfig": "esbuild.config.js" } }, "lint": { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e2b73326a..14d3b5e54 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,12 @@ -export { logPersistedResults, persistReport } from './lib/persist'; -export { executePlugins } from './lib/execute-plugin'; -export { collect, CollectOptions } from './lib/collect'; +export { + logPersistedResults, + persistReport, +} from './lib/implementation/persist'; +export { executePlugins } from './lib/implementation/execute-plugin'; +export { collect, CollectOptions } from './lib/commands/collect'; +export { upload } from './lib/commands/upload'; export { collectAndPersistReports } from './lib/collect-and-persist'; +export { + readCodePushupConfig, + ConfigPathError, +} from './lib/implementation/read-code-pushup-config'; diff --git a/packages/core/src/lib/collect-and-persist.ts b/packages/core/src/lib/collect-and-persist.ts index 535de868e..db2be411a 100644 --- a/packages/core/src/lib/collect-and-persist.ts +++ b/packages/core/src/lib/collect-and-persist.ts @@ -1,8 +1,8 @@ -import { collect, CollectOptions } from './collect'; import { name, version } from '../../package.json'; -import { pluginOutputSchema, Report } from '@quality-metrics/models'; -import { logPersistedResults, persistReport } from './persist'; +import { pluginOutputSchema, Report } from '@code-pushup/models'; +import { collect, CollectOptions } from './commands/collect'; +import { logPersistedResults, persistReport } from './implementation/persist'; export async function collectAndPersistReports( config: CollectOptions, @@ -15,16 +15,11 @@ export async function collectAndPersistReports( }; const persistResults = await persistReport(report, config); - logPersistedResults(persistResults); - // validate report + // validate report and throw if invalid report.plugins.forEach(plugin => { - try { - // Running checks after persisting helps while debugging as you can check the invalid output after the error - pluginOutputSchema.parse(plugin); - } catch (e) { - throw new Error(`${plugin.slug} - ${(e as Error).message}`); - } + // Running checks after persisting helps while debugging as you can check the invalid output after the error is thrown + pluginOutputSchema.parse(plugin); }); } diff --git a/packages/core/src/lib/index.spec.ts b/packages/core/src/lib/commands/collect.spec.ts similarity index 75% rename from packages/core/src/lib/index.spec.ts rename to packages/core/src/lib/commands/collect.spec.ts index c513dafe2..d90a8c0cd 100644 --- a/packages/core/src/lib/index.spec.ts +++ b/packages/core/src/lib/commands/collect.spec.ts @@ -1,11 +1,10 @@ -import { reportSchema } from '@quality-metrics/models'; -import { mockCoreConfig } from '@quality-metrics/models/testing'; +import { reportSchema } from '@code-pushup/models'; +import { mockCoreConfig } from '@code-pushup/models/testing'; import { describe, expect, it } from 'vitest'; import { CollectOptions, collect } from './collect'; const baseOptions: CollectOptions = { ...mockCoreConfig(), - configPath: '', verbose: false, }; diff --git a/packages/core/src/lib/collect.ts b/packages/core/src/lib/commands/collect.ts similarity index 77% rename from packages/core/src/lib/collect.ts rename to packages/core/src/lib/commands/collect.ts index c2f7e885f..ab095255b 100644 --- a/packages/core/src/lib/collect.ts +++ b/packages/core/src/lib/commands/collect.ts @@ -1,6 +1,7 @@ -import { CoreConfig, GlobalOptions, Report } from '@quality-metrics/models'; -import { executePlugins } from './execute-plugin'; -import { calcDuration } from '@quality-metrics/utils'; +import { Report } from '@code-pushup/models'; +import { executePlugins } from '../implementation/execute-plugin'; +import { calcDuration } from '@code-pushup/utils'; +import { CommandBaseOptions } from '../implementation/model'; /** * Error thrown when collect output is invalid. @@ -17,7 +18,7 @@ export class CollectOutputError extends Error { } } -export type CollectOptions = GlobalOptions & CoreConfig; +export type CollectOptions = CommandBaseOptions; /** * Run audits, collect plugin output and aggregate it into a JSON object diff --git a/packages/core/src/lib/commands/upload.spec.ts b/packages/core/src/lib/commands/upload.spec.ts new file mode 100644 index 000000000..2f7e6a3d0 --- /dev/null +++ b/packages/core/src/lib/commands/upload.spec.ts @@ -0,0 +1,69 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +import { beforeEach, describe, vi } from 'vitest'; +import { + MEMFS_VOLUME, + mockPersistConfig, + mockReport, + mockUploadConfig, +} from '@code-pushup/models/testing'; +import { join } from 'path'; +import { vol } from 'memfs'; +import { upload } from './upload'; +import { ReportFragment } from '@code-pushup/portal-client/portal-client/src/lib/graphql/generated'; + +// This in needed to mock the API client used inside the upload function +vi.mock('@code-pushup/portal-client', async () => { + const module: typeof import('@code-pushup/portal-client') = + await vi.importActual('@code-pushup/portal-client'); + + return { + ...module, + uploadToPortal: vi.fn( + async () => + ({ data: { packageName: 'dummy-package' } } as { + data: ReportFragment; + }), + ), + }; +}); + +vi.mock('fs', async () => { + const memfs: typeof import('memfs') = await vi.importActual('memfs'); + return memfs.fs; +}); + +vi.mock('fs/promises', async () => { + const memfs: typeof import('memfs') = await vi.importActual('memfs'); + return memfs.fs.promises; +}); + +const outputPath = MEMFS_VOLUME; +const reportPath = (path = outputPath, format: 'json' | 'md' = 'json') => + join(path, 'report.' + format); + +describe('uploadToPortal', () => { + beforeEach(async () => { + vol.reset(); + vol.fromJSON( + { + [reportPath()]: JSON.stringify(mockReport()), + }, + MEMFS_VOLUME, + ); + }); + + test('should work', async () => { + const cfg = { + upload: mockUploadConfig({ + apiKey: 'dummy-api-key', + server: 'https://example.com/api', + }), + persist: mockPersistConfig({ + outputPath, + }), + }; + const result = (await upload(cfg)) as unknown as { data: ReportFragment }; + + expect(result.data.packageName).toBe('dummy-package'); + }); +}); diff --git a/packages/core/src/lib/commands/upload.ts b/packages/core/src/lib/commands/upload.ts new file mode 100644 index 000000000..c06c269ac --- /dev/null +++ b/packages/core/src/lib/commands/upload.ts @@ -0,0 +1,38 @@ +import { uploadToPortal } from '@code-pushup/portal-client'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { jsonToGql } from '../implementation/json-to-gql'; +import { CoreConfig, reportSchema } from '@code-pushup/models'; +import { latestHash } from '@code-pushup/utils'; + +export type UploadOptions = Pick; + +/** + * Uploads collected audits to the portal + * @param options + */ +export async function upload( + options: UploadOptions, + uploadFn: typeof uploadToPortal = uploadToPortal, +) { + if (options?.upload === undefined) { + throw new Error('upload config needs to be set'); + } + + const { apiKey, server, organization, project } = options.upload; + const { outputPath } = options.persist; + const report = reportSchema.parse( + JSON.parse(readFileSync(join(outputPath, 'report.json')).toString()), + ); + + const data = { + organization, + project, + commit: await latestHash(), + ...jsonToGql(report), + }; + + return uploadFn({ apiKey, server, data }).catch(e => { + throw new Error('upload failed. ' + e.message); + }); +} diff --git a/packages/core/src/lib/execute-plugin.spec.ts b/packages/core/src/lib/implementation/execute-plugin.spec.ts similarity index 92% rename from packages/core/src/lib/execute-plugin.spec.ts rename to packages/core/src/lib/implementation/execute-plugin.spec.ts index 1bc6de22d..b6d341735 100644 --- a/packages/core/src/lib/execute-plugin.spec.ts +++ b/packages/core/src/lib/implementation/execute-plugin.spec.ts @@ -1,8 +1,5 @@ -import { - auditOutputsSchema, - pluginConfigSchema, -} from '@quality-metrics/models'; -import { mockPluginConfig } from '@quality-metrics/models/testing'; +import { auditOutputsSchema, pluginConfigSchema } from '@code-pushup/models'; +import { mockPluginConfig } from '@code-pushup/models/testing'; import { describe, expect, it } from 'vitest'; import { executePlugin, executePlugins } from './execute-plugin'; diff --git a/packages/core/src/lib/execute-plugin.ts b/packages/core/src/lib/implementation/execute-plugin.ts similarity index 96% rename from packages/core/src/lib/execute-plugin.ts rename to packages/core/src/lib/implementation/execute-plugin.ts index 347f2c4b4..2f58d1195 100644 --- a/packages/core/src/lib/execute-plugin.ts +++ b/packages/core/src/lib/implementation/execute-plugin.ts @@ -2,8 +2,8 @@ import { PluginConfig, PluginOutput, auditOutputsSchema, -} from '@quality-metrics/models'; -import { ProcessObserver, executeProcess } from '@quality-metrics/utils'; +} from '@code-pushup/models'; +import { ProcessObserver, executeProcess } from '@code-pushup/utils'; import { readFile } from 'fs/promises'; import { join } from 'path'; diff --git a/packages/core/src/lib/implementation/json-to-gql.ts b/packages/core/src/lib/implementation/json-to-gql.ts new file mode 100644 index 000000000..540213501 --- /dev/null +++ b/packages/core/src/lib/implementation/json-to-gql.ts @@ -0,0 +1,83 @@ +import { + CategoryConfigRefType, + IssueSeverity, + IssueSourceType, + SaveReportMutationVariables, +} from '@code-pushup/portal-client'; +import { Issue, Report } from '@code-pushup/models'; + +export function jsonToGql(report: Report) { + return { + packageName: report.packageName, + packageVersion: report.version, + commandStartDate: report.date, + commandDuration: report.duration, + plugins: report.plugins.map(plugin => ({ + audits: plugin.audits.map(audit => ({ + description: audit.description, + details: { + issues: + audit.details?.issues.map(issue => ({ + message: issue.message, + severity: transformSeverity(issue.severity), + sourceEndColumn: issue.source?.position?.endColumn, + sourceEndLine: issue.source?.position?.endLine, + sourceFilePath: issue.source?.file, + sourceStartColumn: issue.source?.position?.startColumn, + sourceStartLine: issue.source?.position?.startLine, + sourceType: IssueSourceType.SourceCode, + })) || [], + }, + docsUrl: audit.docsUrl, + formattedValue: audit.displayValue, + score: audit.score, + slug: audit.slug, + title: audit.title, + value: audit.value, + })), + description: plugin.description, + docsUrl: plugin.docsUrl, + groups: plugin.groups?.map(group => ({ + slug: group.slug, + title: group.title, + description: group.description, + refs: group.refs.map(ref => ({ slug: ref.slug, weight: ref.weight })), + })), + icon: plugin.icon, + slug: plugin.slug, + title: plugin.title, + packageName: plugin.packageName, + packageVersion: plugin.version, + runnerDuration: plugin.duration, + runnerStartDate: plugin.date, + })), + categories: report.categories.map(category => ({ + slug: category.slug, + title: category.title, + description: category.description, + refs: category.refs.map(ref => ({ + plugin: ref.plugin, + type: + ref.type === 'audit' + ? CategoryConfigRefType.Audit + : CategoryConfigRefType.Group, + weight: ref.weight, + slug: ref.slug, + })), + })), + } satisfies Omit< + SaveReportMutationVariables, + 'organization' | 'project' | 'commit' + >; +} + +function transformSeverity(severity: Issue['severity']): IssueSeverity { + switch (severity) { + case 'info': + return IssueSeverity.Info; + case 'error': + return IssueSeverity.Error; + case 'warning': + return IssueSeverity.Warning; + } +} diff --git a/packages/core/src/lib/implementation/model.ts b/packages/core/src/lib/implementation/model.ts new file mode 100644 index 000000000..78a8b4bda --- /dev/null +++ b/packages/core/src/lib/implementation/model.ts @@ -0,0 +1,3 @@ +import { CoreConfig, GlobalOptions } from '@code-pushup/models'; + +export type CommandBaseOptions = Omit & CoreConfig; diff --git a/packages/core/src/lib/persist.spec.ts b/packages/core/src/lib/implementation/persist.spec.ts similarity index 97% rename from packages/core/src/lib/persist.spec.ts rename to packages/core/src/lib/implementation/persist.spec.ts index f636a5a27..d3c2c502d 100644 --- a/packages/core/src/lib/persist.spec.ts +++ b/packages/core/src/lib/implementation/persist.spec.ts @@ -1,16 +1,16 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { logPersistedResults, persistReport } from './persist'; import { readFileSync, unlinkSync } from 'fs'; -import { Report } from '@quality-metrics/models'; +import { Report } from '@code-pushup/models'; import { dummyConfig, dummyReport, MEMFS_VOLUME, mockPersistConfig, -} from '@quality-metrics/models/testing'; +} from '@code-pushup/models/testing'; import { vol } from 'memfs'; import { join } from 'path'; -import { mockConsole, unmockConsole } from '../../test/console.mock'; +import { mockConsole, unmockConsole } from '../../../test/console.mock'; vi.mock('fs', async () => { const memfs: typeof import('memfs') = await vi.importActual('memfs'); diff --git a/packages/core/src/lib/persist.ts b/packages/core/src/lib/implementation/persist.ts similarity index 94% rename from packages/core/src/lib/persist.ts rename to packages/core/src/lib/implementation/persist.ts index 9855bd422..3f84ff718 100644 --- a/packages/core/src/lib/persist.ts +++ b/packages/core/src/lib/implementation/persist.ts @@ -2,12 +2,8 @@ import { existsSync, mkdirSync } from 'fs'; import { writeFile, stat } from 'fs/promises'; import { join } from 'path'; import chalk from 'chalk'; -import { CoreConfig, Report } from '@quality-metrics/models'; -import { - formatBytes, - reportToStdout, - reportToMd, -} from '@quality-metrics/utils'; +import { CoreConfig, Report } from '@code-pushup/models'; +import { formatBytes, reportToStdout, reportToMd } from '@code-pushup/utils'; export class PersistDirError extends Error { constructor(outputPath: string) { diff --git a/packages/core/src/lib/implementation/read-code-pushup-config.ts b/packages/core/src/lib/implementation/read-code-pushup-config.ts new file mode 100644 index 000000000..76b9bc366 --- /dev/null +++ b/packages/core/src/lib/implementation/read-code-pushup-config.ts @@ -0,0 +1,24 @@ +import { CoreConfig, coreConfigSchema } from '@code-pushup/models'; +import { importModule } from '@code-pushup/utils'; +import { stat } from 'fs/promises'; + +export class ConfigPathError extends Error { + constructor(configPath: string) { + super(`Config path ${configPath} is not a file.`); + } +} + +export async function readCodePushupConfig(filepath: string) { + const isFile = (await stat(filepath)).isFile(); + + if (!isFile) { + throw new ConfigPathError(filepath); + } + + return importModule( + { + filepath, + }, + coreConfigSchema.parse, + ); +} diff --git a/packages/core/test/base.command.mock.ts b/packages/core/test/base.command.mock.ts new file mode 100644 index 000000000..b7192f5be --- /dev/null +++ b/packages/core/test/base.command.mock.ts @@ -0,0 +1,6 @@ +import { mockCoreConfig } from '@code-pushup/models/testing'; +import { CommandBaseOptions } from '../src/lib/implementation/model'; + +export function commandBaseOptionsMock(): CommandBaseOptions { + return { ...mockCoreConfig(), verbose: false }; +} diff --git a/packages/core/test/index.ts b/packages/core/test/index.ts new file mode 100644 index 000000000..fcb073fef --- /dev/null +++ b/packages/core/test/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/packages/core/test/types.ts b/packages/core/test/types.ts new file mode 100644 index 000000000..04d53f5fb --- /dev/null +++ b/packages/core/test/types.ts @@ -0,0 +1,6 @@ +export type ENV = { + API_KEY: string; + SERVER: string; + PROJECT: string; + ORGANIZATION: string; +}; diff --git a/packages/models/README.md b/packages/models/README.md index 7bab9fa4d..c60090f9b 100644 --- a/packages/models/README.md +++ b/packages/models/README.md @@ -5,7 +5,7 @@ Model definitions and validators for the CLI configuration as well as plugin typ ## Usage ```ts -import { CoreConfigSchema, pluginConfigSchema } from '@quality-metrics/models'; +import { CoreConfigSchema, pluginConfigSchema } from '@code-pushup/models'; export default { // ... diff --git a/packages/models/package.json b/packages/models/package.json index fdc400ea7..7d35eaeb2 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -2,7 +2,7 @@ "name": "@code-pushup/models", "version": "0.0.1", "dependencies": { - "@code-pushup/portal-client": "^0.1.2", - "zod": "^3.22.1" + "zod": "^3.22.1", + "@code-pushup/portal-client": "^0.1.2" } } diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index d472b7c12..d0b3cd715 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -6,7 +6,12 @@ export { refineCoreConfig, unrefinedCoreConfigSchema, } from './lib/core-config'; -export { PersistConfig, persistConfigSchema } from './lib/persist-config'; +export { + PersistConfig, + persistConfigSchema, + formatSchema, + Format, +} from './lib/persist-config'; export { UploadConfig, uploadConfigSchema } from './lib/upload-config'; export { AuditGroup, diff --git a/packages/models/src/lib/category-config.ts b/packages/models/src/lib/category-config.ts index 9f8f8210d..6ede923e9 100644 --- a/packages/models/src/lib/category-config.ts +++ b/packages/models/src/lib/category-config.ts @@ -35,7 +35,7 @@ export const categoryConfigSchema = scorableSchema( .merge( metaSchema({ titleDescription: 'Category Title', - docsUrlDescription: 'Category docs RUL', + docsUrlDescription: 'Category docs URL', descriptionDescription: 'Category description', description: 'Meta info for category', }), diff --git a/packages/models/src/lib/global-options.ts b/packages/models/src/lib/global-options.ts index 11132256b..cf0905dbf 100644 --- a/packages/models/src/lib/global-options.ts +++ b/packages/models/src/lib/global-options.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { generalFilePathSchema } from './implementation/schemas'; +import { filePathSchema } from './implementation/schemas'; export const globalOptionsSchema = z.object({ verbose: z @@ -7,7 +7,7 @@ export const globalOptionsSchema = z.object({ description: 'Outputs additional information for a run', }) .default(false), - configPath: generalFilePathSchema( + configPath: filePathSchema( "Path to config file in format `ts` or `mjs`. defaults to 'code-pushup.config.js'", ) .optional() diff --git a/packages/models/src/lib/implementation/schemas.ts b/packages/models/src/lib/implementation/schemas.ts index ed8bea5bd..92fec9b02 100644 --- a/packages/models/src/lib/implementation/schemas.ts +++ b/packages/models/src/lib/implementation/schemas.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { generalFilePathRegex, slugRegex, unixFilePathRegex } from './utils'; +import { slugRegex } from './utils'; /** * Schema for execution meta date @@ -95,14 +95,15 @@ export function metaSchema(options?: { * Schema for a generalFilePath * @param description */ -export function generalFilePathSchema(description: string) { - return z.string({ description }).regex(generalFilePathRegex, { - message: 'path is invalid', - }); +export function filePathSchema(description: string) { + return z + .string({ description }) + .trim() + .min(1, { message: 'path is invalid' }); } /** - * Schema for a unixFilePath + * Schema for a weight * @param description */ export function weightSchema( @@ -119,14 +120,6 @@ export function positiveIntSchema(description: string) { return z.number({ description }).int().nonnegative(); } -/** - * Schema for a unixFilePath - * @param description - */ -export function unixFilePathSchema(description: string) { - return z.string({ description }).regex(unixFilePathRegex); -} - export function packageVersionSchema(options?: { versionDescription?: string; optional?: boolean; diff --git a/packages/models/src/lib/implementation/utils.spec.ts b/packages/models/src/lib/implementation/utils.spec.ts index 0c95dc8ca..13ad0f2db 100644 --- a/packages/models/src/lib/implementation/utils.spec.ts +++ b/packages/models/src/lib/implementation/utils.spec.ts @@ -1,10 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { - generalFilePathRegex, - hasDuplicateStrings, - hasMissingStrings, - slugRegex, -} from './utils'; +import { hasDuplicateStrings, hasMissingStrings, slugRegex } from './utils'; describe('slugRegex', () => { // test valid and array of strings against slugRegex with it blocks @@ -37,89 +32,6 @@ describe('slugRegex', () => { }); }); -describe('generalFilePathRegex', () => { - // test valid and array of strings against slugRegex with it blocks - const validPathsUnix = [ - '/home/user/documents/file.txt', - '/var/www/html/index.html', - 'home/user/', - 'file.txt', - '/a/b/c/d/e', - 'folder/file.ext', - '/folder.with.dots/file', - ]; - const validPathsWindows = [ - 'C:\\Users\\John\\Documents\\file.docx', - 'D:/Games/Valheim/game.exe', - 'C:\\Program Files\\App\\binary.exe', - 'I:/path with spaces/', - 'E:/folder/file.ext', - 'F:\\a\\b\\c\\d.txt', - 'G:/folder.with.dots/file.exe', - ]; - it.each(validPathsUnix.concat(validPathsWindows))( - `should match valid filePath %p`, - filePath => { - expect(filePath).toMatch(generalFilePathRegex); - }, - ); - - const invalidPathsUnix = [ - '//home/user/', - ' /leading-space/path', - 'home//user/', - '/folder/../file', - '/a/b/c//d', - 'folder/name?', - '/folder<>/file', - 'folder*', - ]; - const invalidPathsWindows = [ - 'C::\\Users\\John', - ' C:\\Leading-space', - 'D:/Games//Valheim/', - 'H:\\invalid|char/file.txt', - 'C:\\path<>\\file', - 'D:/question/file?.txt', - 'E:\\star/file*.txt', - ]; - - it.each(invalidPathsUnix.concat(invalidPathsWindows))( - `should not match invalid filePath %p`, - invalidFilePath => { - expect(invalidFilePath).not.toMatch(generalFilePathRegex); - }, - ); -}); - -describe('unixFilePathRegex', () => { - // test valid and array of strings against slugRegex with it blocks - it.each([ - '/home/user/documents/file.txt', - '/var/www/html/index.html', - 'home/user/', - 'file.txt', - '/a/b/c/d/e', - 'folder/file.ext', - '/folder.with.dots/file', - ])(`should match valid filePath %p}`, validPaths => { - expect(validPaths).toMatch(generalFilePathRegex); - }); - - it.each([ - '//home/user/', - ' /leading-space/path', - 'home//user/', - '/folder/../file', - '/a/b/c//d', - 'folder/name?', - '/folder<>/file', - 'folder*', - ])(`should not match invalid filePath %p}`, validPaths => { - expect(validPaths).not.toMatch(generalFilePathRegex); - }); -}); - describe('stringUnique', () => { it('should return true for a list of unique strings', () => { expect(hasDuplicateStrings(['a', 'b'])).toBe(false); diff --git a/packages/models/src/lib/implementation/utils.ts b/packages/models/src/lib/implementation/utils.ts index 00645add1..3219f33e4 100644 --- a/packages/models/src/lib/implementation/utils.ts +++ b/packages/models/src/lib/implementation/utils.ts @@ -6,17 +6,6 @@ */ export const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; -/** - * Regular expression to validate filenames for Windows and UNIX - **/ -export const generalFilePathRegex = - /^(?:(?:[A-Za-z]:)?[\\/])?(?:\w[\w .-]*[\\/]?)*$/; - -/** - * Regular expression to validate filenames for UNIX - **/ -export const unixFilePathRegex = /^(?:(?:[A-Za-z]:)?[/])?(?:\w[\w .-]*[/]?)*$/; - /** * helper function to validate string arrays * diff --git a/packages/models/src/lib/persist-config.spec.ts b/packages/models/src/lib/persist-config.spec.ts index c3fe460ee..c3d5a3a6f 100644 --- a/packages/models/src/lib/persist-config.spec.ts +++ b/packages/models/src/lib/persist-config.spec.ts @@ -9,8 +9,7 @@ describe('persistConfigSchema', () => { }); it('should throw if outputPath is invalid', () => { - const invalidPath = '/folder/../file'; - const cfg = mockPersistConfig({ outputPath: invalidPath }); + const cfg = mockPersistConfig({ outputPath: ' ' }); expect(() => persistConfigSchema.parse(cfg)).toThrow(`path is invalid`); }); diff --git a/packages/models/src/lib/persist-config.ts b/packages/models/src/lib/persist-config.ts index 32523b5c6..194f488f9 100644 --- a/packages/models/src/lib/persist-config.ts +++ b/packages/models/src/lib/persist-config.ts @@ -1,12 +1,12 @@ import { z } from 'zod'; -import { generalFilePathSchema } from './implementation/schemas'; +import { filePathSchema } from './implementation/schemas'; + +export const formatSchema = z.enum(['json', 'stdout', 'md']); +export type Format = z.infer; export const persistConfigSchema = z.object({ - outputPath: generalFilePathSchema('Artifacts folder'), - format: z - .array(z.enum(['json', 'stdout', 'md'])) - .default(['stdout']) - .optional(), + outputPath: filePathSchema('Artifacts folder'), + format: z.array(formatSchema).default(['stdout']).optional(), }); export type PersistConfig = z.infer; diff --git a/packages/models/src/lib/plugin-config.ts b/packages/models/src/lib/plugin-config.ts index a3c2c8a41..cb10f4017 100644 --- a/packages/models/src/lib/plugin-config.ts +++ b/packages/models/src/lib/plugin-config.ts @@ -2,13 +2,12 @@ import { MATERIAL_ICONS, MaterialIcon } from '@code-pushup/portal-client'; import { z } from 'zod'; import { executionMetaSchema, - generalFilePathSchema, + filePathSchema, metaSchema, packageVersionSchema, positiveIntSchema, scorableSchema, slugSchema, - unixFilePathSchema, weightedRefSchema, } from './implementation/schemas'; import { @@ -47,7 +46,7 @@ const runnerConfigSchema = z.object( description: 'Shell command to execute', }), args: z.array(z.string({ description: 'Command arguments' })).optional(), - outputPath: generalFilePathSchema('Output path'), + outputPath: filePathSchema('Output path'), }, { description: 'How to execute runner', @@ -166,7 +165,7 @@ function getMissingRefsFromGroups(pluginCfg: _PluginCfg) { const sourceFileLocationSchema = z.object( { - file: unixFilePathSchema('Relative path to source file in Git repo'), + file: filePathSchema('Relative path to source file in Git repo'), position: z .object( { @@ -200,14 +199,13 @@ export const auditOutputSchema = auditSchema.merge( displayValue: z .string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }) .optional(), - value: positiveIntSchema('Raw numeric value').optional(), + value: positiveIntSchema('Raw numeric value'), score: z .number({ description: 'Value between 0 and 1', }) .min(0) - .max(1) - .optional(), + .max(1), details: z .object( { @@ -235,7 +233,7 @@ export const auditOutputsSchema = z export type AuditOutputs = z.infer; export const pluginOutputSchema = pluginSchema - .merge(executionMetaSchema()) // @TODO create reusable meta info for audit, plugin, category + .merge(executionMetaSchema()) .merge( z.object( { @@ -251,14 +249,14 @@ export const pluginOutputSchema = pluginSchema export type PluginOutput = z.infer; // helper for validator: audit slugs are unique -function duplicateSlugsInAuditsErrorMsg(audits: AuditOutput[]) { +function duplicateSlugsInAuditsErrorMsg(audits: { slug: string }[]) { const duplicateRefs = getDuplicateSlugsInAudits(audits); return `In plugin audits the slugs are not unique: ${errorItems( duplicateRefs, )}`; } -function getDuplicateSlugsInAudits(audits: AuditOutput[]) { +function getDuplicateSlugsInAudits(audits: { slug: string }[]) { return hasDuplicateStrings(audits.map(({ slug }) => slug)); } diff --git a/packages/models/src/lib/report.ts b/packages/models/src/lib/report.ts index 396f1e59a..756dc57a6 100644 --- a/packages/models/src/lib/report.ts +++ b/packages/models/src/lib/report.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { hasMissingStrings } from './implementation/utils'; import { + auditGroupSchema, AuditOutputs, auditOutputSchema, auditSchema, @@ -26,6 +27,7 @@ export const pluginReportSchema = pluginSchema .merge( z.object({ audits: z.array(auditReportSchema), + groups: z.array(auditGroupSchema).optional(), }), ); export type PluginReport = z.infer; diff --git a/packages/models/src/lib/upload-config.ts b/packages/models/src/lib/upload-config.ts index c088e00d8..8c56aed56 100644 --- a/packages/models/src/lib/upload-config.ts +++ b/packages/models/src/lib/upload-config.ts @@ -7,6 +7,12 @@ export const uploadConfigSchema = z.object({ description: 'API key with write access to portal (use `process.env` for security)', }), + organization: z.string({ + description: 'Organization in code versioning system', + }), + project: z.string({ + description: 'Project in code versioning system', + }), }); export type UploadConfig = z.infer; diff --git a/packages/models/test/schema.mock.ts b/packages/models/test/schema.mock.ts index 6e0eefe73..3270ea642 100644 --- a/packages/models/test/schema.mock.ts +++ b/packages/models/test/schema.mock.ts @@ -25,13 +25,16 @@ export function mockPluginConfig(opt?: { pluginSlug?: string; auditSlug?: string | string[]; groupSlug?: string | string[]; + outputPath?: string; }): PluginConfig { - const { groupSlug } = opt || {}; + const { groupSlug, outputPath } = opt || {}; let { pluginSlug, auditSlug } = opt || {}; pluginSlug = pluginSlug || __pluginSlug__; auditSlug = auditSlug || __auditSlug__; const addGroups = groupSlug !== undefined; - const pluginOutputPath = `tmp/${+new Date()}-${__outputFile__}`; + const pluginOutputPath = `${ + outputPath || 'tmp' + }/${+new Date()}-${__outputFile__}`; const audits = Array.isArray(auditSlug) ? auditSlug.map(slug => mockAuditConfig({ auditSlug: slug })) @@ -171,6 +174,7 @@ export function mockCategory(opt?: { description: `This is the category description of ${categorySlug}. Enjoy dummy text and data to the full.`, docsUrl: 'https://category.dev?' + categorySlug, refs: categoryAuditRefs.concat(categoryGroupRefs), + isBinary: false, } satisfies Required; } @@ -185,7 +189,7 @@ export function mockReport(opt?: { packageName: 'mock-package', version: '0.0.0', date: new Date().toDateString(), - duration: randDuration(), + duration: 42, categories: [mockCategory({ pluginSlug, auditSlug })], plugins: [mockPluginReport({ auditSlug, pluginSlug })], }; @@ -200,14 +204,15 @@ export function mockPluginReport(opt?: { pluginSlug = pluginSlug || __pluginSlug__; return { date: new Date().toDateString(), - duration: randDuration(), + duration: 420, slug: pluginSlug, title: 'Title of ' + pluginSlug, description: 'Plugin description of ' + pluginSlug, docsUrl: `http://plugin.io/docs/${pluginSlug}`, icon: 'nrwl', version: '0.0.1', - packageName: '@' + pluginSlug, + packageName: pluginSlug, + groups: [], audits: Array.isArray(auditSlug) ? auditSlug.map(a => mockAuditReport({ auditSlug: a })) : [mockAuditReport({ auditSlug })], @@ -251,6 +256,8 @@ export function mockUploadConfig(opt?: Partial): UploadConfig { return { apiKey: 'm0ck-API-k3y', server: 'http://test.server.io', + organization: 'code-pushup', + project: 'cli', ...opt, }; } diff --git a/packages/models/test/test-data/config-and-report-dummy.mock.ts b/packages/models/test/test-data/config-and-report-dummy.mock.ts index 384524372..f6e4fb7ea 100644 --- a/packages/models/test/test-data/config-and-report-dummy.mock.ts +++ b/packages/models/test/test-data/config-and-report-dummy.mock.ts @@ -12,9 +12,13 @@ const auditSlug1 = ['1a', '1b', '1c']; const auditSlug2 = ['2a', '2b', '2c', '2d', '2e']; const dummyPlugins = [ - mockPluginConfig({ pluginSlug: pluginSlug[0], auditSlug: auditSlug0 }), - mockPluginConfig({ pluginSlug: pluginSlug[1], auditSlug: auditSlug1 }), - mockPluginConfig({ pluginSlug: pluginSlug[2], auditSlug: auditSlug2 }), + mockPluginConfig({ + pluginSlug: pluginSlug[0], + auditSlug: auditSlug0, + outputPath: 'test', + }), + /*mockPluginConfig({ pluginSlug: pluginSlug[1], auditSlug: auditSlug1 }), + mockPluginConfig({ pluginSlug: pluginSlug[2], auditSlug: auditSlug2 }),*/ ]; const dummyCategories = [ diff --git a/packages/nx-plugin/src/generators/init/generator.spec.ts b/packages/nx-plugin/src/generators/init/generator.spec.ts index 13bf3fc51..0dc6c4be5 100644 --- a/packages/nx-plugin/src/generators/init/generator.spec.ts +++ b/packages/nx-plugin/src/generators/init/generator.spec.ts @@ -11,10 +11,10 @@ type PackageJson = { const cpuTargetName = 'code-pushup'; const devDependencyNames = [ - '@quality-metrics/cli', - '@quality-metrics/models', - '@quality-metrics/nx-plugin', - '@quality-metrics/utils', + '@code-pushup/cli', + '@code-pushup/models', + '@code-pushup/nx-plugin', + '@code-pushup/utils', ]; describe('init generator', () => { diff --git a/packages/nx-plugin/src/generators/init/generator.ts b/packages/nx-plugin/src/generators/init/generator.ts index a4d62dcaa..48733454f 100644 --- a/packages/nx-plugin/src/generators/init/generator.ts +++ b/packages/nx-plugin/src/generators/init/generator.ts @@ -27,10 +27,10 @@ function checkDependenciesInstalled(host: Tree) { packageJson.devDependencies = packageJson.devDependencies || {}; // base deps - devDependencies['@quality-metrics/nx-plugin'] = cpuNxPluginVersion; - devDependencies['@quality-metrics/models'] = cpuModelVersion; - devDependencies['@quality-metrics/utils'] = cpuUtilsVersion; - devDependencies['@quality-metrics/cli'] = cpuCliVersion; + devDependencies['@code-pushup/nx-plugin'] = cpuNxPluginVersion; + devDependencies['@code-pushup/models'] = cpuModelVersion; + devDependencies['@code-pushup/utils'] = cpuUtilsVersion; + devDependencies['@code-pushup/cli'] = cpuCliVersion; return addDependenciesToPackageJson(host, dependencies, devDependencies); } @@ -40,10 +40,10 @@ function moveToDevDependencies(tree: Tree) { packageJson.dependencies = packageJson.dependencies || {}; packageJson.devDependencies = packageJson.devDependencies || {}; - if (packageJson.dependencies['@quality-metrics/nx-plugin']) { - packageJson.devDependencies['@quality-metrics/nx-plugin'] = - packageJson.dependencies['@quality-metrics/nx-plugin']; - delete packageJson.dependencies['@quality-metrics/nx-plugin']; + if (packageJson.dependencies['@code-pushup/nx-plugin']) { + packageJson.devDependencies['@code-pushup/nx-plugin'] = + packageJson.dependencies['@code-pushup/nx-plugin']; + delete packageJson.dependencies['@code-pushup/nx-plugin']; } return packageJson; diff --git a/packages/plugin-eslint/README.md b/packages/plugin-eslint/README.md index 985755f89..b43ad5d14 100644 --- a/packages/plugin-eslint/README.md +++ b/packages/plugin-eslint/README.md @@ -1,3 +1,3 @@ -# @quality-metrics/eslint-plugin +# @code-pushup/eslint-plugin TODO: docs diff --git a/packages/plugin-eslint/package.json b/packages/plugin-eslint/package.json index c7837958a..8aa8652d9 100644 --- a/packages/plugin-eslint/package.json +++ b/packages/plugin-eslint/package.json @@ -2,9 +2,9 @@ "name": "@code-pushup/eslint-plugin", "version": "0.0.1", "dependencies": { + "@code-pushup/utils": "*", "eslint": "~8.46.0", "zod": "^3.22.4", - "@code-pushup/models": "*", - "@code-pushup/utils": "*" + "@code-pushup/models": "*" } } diff --git a/packages/plugin-eslint/src/lib/eslint-plugin.ts b/packages/plugin-eslint/src/lib/eslint-plugin.ts index 34f07a474..beb414611 100644 --- a/packages/plugin-eslint/src/lib/eslint-plugin.ts +++ b/packages/plugin-eslint/src/lib/eslint-plugin.ts @@ -1,5 +1,5 @@ -import { PluginConfig } from '@quality-metrics/models'; -import { toArray } from '@quality-metrics/utils'; +import { PluginConfig } from '@code-pushup/models'; +import { toArray } from '@code-pushup/utils'; import { ESLint } from 'eslint'; import { name, version } from '../../package.json'; import { ESLintPluginConfig, eslintPluginConfigSchema } from './config'; diff --git a/packages/plugin-eslint/src/lib/meta/audits.spec.ts b/packages/plugin-eslint/src/lib/meta/audits.spec.ts index 4e40eb9c9..6b33e6e64 100644 --- a/packages/plugin-eslint/src/lib/meta/audits.spec.ts +++ b/packages/plugin-eslint/src/lib/meta/audits.spec.ts @@ -1,4 +1,4 @@ -import type { Audit } from '@quality-metrics/models'; +import type { Audit } from '@code-pushup/models'; import { ruleToAudit } from './audits'; describe('ruleToAudit', () => { diff --git a/packages/plugin-eslint/src/lib/meta/audits.ts b/packages/plugin-eslint/src/lib/meta/audits.ts index 656cbc141..e2b592c2b 100644 --- a/packages/plugin-eslint/src/lib/meta/audits.ts +++ b/packages/plugin-eslint/src/lib/meta/audits.ts @@ -1,4 +1,4 @@ -import type { Audit } from '@quality-metrics/models'; +import type { Audit } from '@code-pushup/models'; import type { ESLint } from 'eslint'; import { ruleIdToSlug } from './hash'; import { RuleData, listRules } from './rules'; diff --git a/packages/plugin-eslint/src/lib/meta/hash.ts b/packages/plugin-eslint/src/lib/meta/hash.ts index cd8519625..a658c2042 100644 --- a/packages/plugin-eslint/src/lib/meta/hash.ts +++ b/packages/plugin-eslint/src/lib/meta/hash.ts @@ -1,4 +1,4 @@ -import { slugify } from '@quality-metrics/utils'; +import { slugify } from '@code-pushup/utils'; import { createHash } from 'crypto'; export function ruleIdToSlug( diff --git a/packages/plugin-eslint/src/lib/meta/rules.ts b/packages/plugin-eslint/src/lib/meta/rules.ts index d9f89ab79..df321385a 100644 --- a/packages/plugin-eslint/src/lib/meta/rules.ts +++ b/packages/plugin-eslint/src/lib/meta/rules.ts @@ -1,4 +1,4 @@ -import { distinct, toArray } from '@quality-metrics/utils'; +import { distinct, toArray } from '@code-pushup/utils'; import type { ESLint, Linter, Rule } from 'eslint'; import { jsonHash } from './hash'; diff --git a/packages/plugin-eslint/tsconfig.lib.json b/packages/plugin-eslint/tsconfig.lib.json index 74dff64c6..b1a2e0598 100644 --- a/packages/plugin-eslint/tsconfig.lib.json +++ b/packages/plugin-eslint/tsconfig.lib.json @@ -10,6 +10,7 @@ "vite.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts", - "src/**/*.mock.ts" + "src/**/*.mock.ts", + "test/**/*.ts" ] } diff --git a/packages/plugin-eslint/tsconfig.spec.json b/packages/plugin-eslint/tsconfig.spec.json index 6d3be7427..56845b342 100644 --- a/packages/plugin-eslint/tsconfig.spec.json +++ b/packages/plugin-eslint/tsconfig.spec.json @@ -6,6 +6,7 @@ }, "include": [ "vite.config.ts", + "test/**/*.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.test.tsx", diff --git a/packages/plugin-lighthouse/README.md b/packages/plugin-lighthouse/README.md index dd9cd8233..2e1cbda30 100644 --- a/packages/plugin-lighthouse/README.md +++ b/packages/plugin-lighthouse/README.md @@ -1,3 +1,3 @@ -# @quality-metrics/lighthouse-plugin +# @code-pushup/lighthouse-plugin TODO: docs diff --git a/packages/plugin-lighthouse/package.json b/packages/plugin-lighthouse/package.json index fcc3b759c..ebba4cd01 100644 --- a/packages/plugin-lighthouse/package.json +++ b/packages/plugin-lighthouse/package.json @@ -2,8 +2,7 @@ "name": "@code-pushup/lighthouse-plugin", "version": "0.0.1", "dependencies": { - "lighthouse": "^11.0.0", "@code-pushup/models": "*", - "@code-pushup/utils": "*" + "lighthouse": "^11.0.0" } } diff --git a/packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts b/packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts index 8e2df961b..629cc390b 100644 --- a/packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts +++ b/packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts @@ -1,5 +1,4 @@ -import { AuditOutputs, PluginConfig } from '@quality-metrics/models'; -import { objectToCliArgs } from '@quality-metrics/utils'; +import { AuditOutputs, PluginConfig } from '@code-pushup/models'; import { defaultConfig } from 'lighthouse'; type LighthousePluginConfig = { @@ -19,8 +18,9 @@ export function lighthousePlugin(_: LighthousePluginConfig): PluginConfig { ], runner: { command: 'node', - args: objectToCliArgs({ - e: `require('fs').writeFileSync('tmp/out.json', '${JSON.stringify([ + args: [ + '-e', + `require('fs').writeFileSync('tmp/out.json', '${JSON.stringify([ { slug: 'largest-contentful-paint', title: 'Largest Contentful Paint', @@ -28,7 +28,7 @@ export function lighthousePlugin(_: LighthousePluginConfig): PluginConfig { score: 0, }, ] satisfies AuditOutputs)}')`, - }), + ], outputPath: 'tmp/out.json', }, slug: 'lighthouse', diff --git a/packages/plugin-lighthouse/test/.gitkeep b/packages/plugin-lighthouse/test/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugin-lighthouse/tsconfig.lib.json b/packages/plugin-lighthouse/tsconfig.lib.json index 74dff64c6..b1a2e0598 100644 --- a/packages/plugin-lighthouse/tsconfig.lib.json +++ b/packages/plugin-lighthouse/tsconfig.lib.json @@ -10,6 +10,7 @@ "vite.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts", - "src/**/*.mock.ts" + "src/**/*.mock.ts", + "test/**/*.ts" ] } diff --git a/packages/plugin-lighthouse/tsconfig.spec.json b/packages/plugin-lighthouse/tsconfig.spec.json index 6d3be7427..56845b342 100644 --- a/packages/plugin-lighthouse/tsconfig.spec.json +++ b/packages/plugin-lighthouse/tsconfig.spec.json @@ -6,6 +6,7 @@ }, "include": [ "vite.config.ts", + "test/**/*.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.test.tsx", diff --git a/packages/utils/package.json b/packages/utils/package.json index fa9308f15..6b04fabbc 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -2,9 +2,10 @@ "name": "@code-pushup/utils", "version": "0.0.1", "dependencies": { + "@code-pushup/models": "*", + "bundle-require": "^4.0.1", "chalk": "^5.3.0", "cliui": "^8.0.1", - "bundle-require": "^4.0.1", - "@code-pushup/models": "*" + "simple-git": "^3.20.0" } } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 38ffbcd6a..e5c2f5cd3 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -5,7 +5,9 @@ export { ProcessResult, executeProcess, objectToCliArgs, + CliArgsObject, } from './lib/execute-process'; +export { latestHash, git } from './lib/git'; export { importModule } from './lib/load-file'; export { reportToMd } from './lib/report-to-md'; export { reportToStdout } from './lib/report-to-stdout'; diff --git a/packages/utils/src/lib/execute-process.spec.ts b/packages/utils/src/lib/execute-process.spec.ts index c0056c753..6faa9a9ce 100644 --- a/packages/utils/src/lib/execute-process.spec.ts +++ b/packages/utils/src/lib/execute-process.spec.ts @@ -71,7 +71,7 @@ describe('objectToCliArgs', () => { value: 0, score: 0, }, - ] satisfies AuditOutputs)}')`, + ])}')`, }; const result = objectToCliArgs(params); expect(result).toEqual([`-e="${params.e}"`]); diff --git a/packages/utils/src/lib/execute-process.ts b/packages/utils/src/lib/execute-process.ts index 21e017ea4..f8c09ed94 100644 --- a/packages/utils/src/lib/execute-process.ts +++ b/packages/utils/src/lib/execute-process.ts @@ -167,6 +167,10 @@ export function executeProcess(cfg: ProcessConfig): Promise { }); } +export type CliArgsObject = + | Record + | { _: string }; + /** * Converts an object with different types of values into an array of command-line arguments. * @@ -179,13 +183,11 @@ export function executeProcess(cfg: ProcessConfig): Promise { * formats: ['json', 'md'] // --format=json --format=md * }); */ -export function objectToCliArgs( - params: Record | { _: string }, -): string[] { +export function objectToCliArgs(params: CliArgsObject): string[] { return Object.entries(params).flatMap(([key, value]) => { // process/file/script if (key === '_') { - return [value.toString()]; + return [value + '']; } const prefix = key.length === 1 ? '-' : '--'; // "-*" arguments (shorthands) diff --git a/packages/utils/src/lib/git.spec.ts b/packages/utils/src/lib/git.spec.ts new file mode 100644 index 000000000..a1b1725e5 --- /dev/null +++ b/packages/utils/src/lib/git.spec.ts @@ -0,0 +1,10 @@ +import { latestHash } from './git'; +import { expect } from 'vitest'; + +describe('git', () => { + it('should log current hash', async () => { + const hash = await latestHash(); + expect(hash).toHaveLength(40); + expect(hash).toMatch(/^[0-9a-f]+$/); + }); +}); diff --git a/packages/utils/src/lib/git.ts b/packages/utils/src/lib/git.ts new file mode 100644 index 000000000..7bcd0eced --- /dev/null +++ b/packages/utils/src/lib/git.ts @@ -0,0 +1,12 @@ +import simpleGit from 'simple-git'; + +export const git = simpleGit(); + +export async function latestHash() { + // git log -1 --pretty=format:"%H" // logs hash e.g. 41682a2fec1d4ece81c696a26c08984baeb4bcf3 + const log = await git.log({ maxCount: 1, format: { hash: '%H' } }); + if (!log?.latest?.hash) { + throw new Error('no latest hash present in git history.'); + } + return log?.latest?.hash; +} diff --git a/packages/utils/src/lib/load-file.ts b/packages/utils/src/lib/load-file.ts index a158fea35..d9f16c539 100644 --- a/packages/utils/src/lib/load-file.ts +++ b/packages/utils/src/lib/load-file.ts @@ -9,6 +9,7 @@ export async function importModule( format: 'esm', ...options, }; + const { mod } = await bundleRequire(options); return parse(mod.default || mod); } diff --git a/packages/utils/src/lib/mock/schema-helper.mock.ts b/packages/utils/src/lib/mock/schema-helper.mock.ts index ec8909e06..38c93bdbe 100644 --- a/packages/utils/src/lib/mock/schema-helper.mock.ts +++ b/packages/utils/src/lib/mock/schema-helper.mock.ts @@ -7,7 +7,7 @@ import { PluginConfig, PluginReport, Report, -} from '@quality-metrics/models'; +} from '@code-pushup/models'; const __pluginSlug__ = 'mock-plugin-slug'; const __auditSlug__ = 'mock-audit-slug'; diff --git a/packages/utils/src/lib/report-to-md.spec.ts b/packages/utils/src/lib/report-to-md.spec.ts index b497e5ad5..3e7e06150 100644 --- a/packages/utils/src/lib/report-to-md.spec.ts +++ b/packages/utils/src/lib/report-to-md.spec.ts @@ -4,7 +4,7 @@ import { dummyReport, lighthouseReport, nxValidatorsOnlyReport, -} from '@quality-metrics/models/testing'; +} from '@code-pushup/models/testing'; describe('report-to-md', () => { it('should contain all sections when using dummy report', () => { diff --git a/packages/utils/src/lib/report-to-md.ts b/packages/utils/src/lib/report-to-md.ts index 273c35f53..f5e164d6d 100644 --- a/packages/utils/src/lib/report-to-md.ts +++ b/packages/utils/src/lib/report-to-md.ts @@ -1,4 +1,4 @@ -import { Report } from '@quality-metrics/models'; +import { Report } from '@code-pushup/models'; import { NEW_LINE, headline, style, li, table, details, link } from './md/'; import { countWeightedRefs, diff --git a/packages/utils/src/lib/report-to-stdout.spec.ts b/packages/utils/src/lib/report-to-stdout.spec.ts index 3e2a42a66..e721d5366 100644 --- a/packages/utils/src/lib/report-to-stdout.spec.ts +++ b/packages/utils/src/lib/report-to-stdout.spec.ts @@ -5,7 +5,7 @@ import { dummyReport, lighthouseReport, nxValidatorsOnlyReport, -} from '@quality-metrics/models/testing'; +} from '@code-pushup/models/testing'; let logs: string[] = []; diff --git a/packages/utils/src/lib/report-to-stdout.ts b/packages/utils/src/lib/report-to-stdout.ts index 0887aa370..2539ac587 100644 --- a/packages/utils/src/lib/report-to-stdout.ts +++ b/packages/utils/src/lib/report-to-stdout.ts @@ -1,4 +1,4 @@ -import { Report } from '@quality-metrics/models'; +import { Report } from '@code-pushup/models'; import chalk from 'chalk'; import cliui from 'cliui'; import { diff --git a/packages/utils/src/lib/utils.spec.ts b/packages/utils/src/lib/utils.spec.ts index 9b43b635f..1ece0db1f 100644 --- a/packages/utils/src/lib/utils.spec.ts +++ b/packages/utils/src/lib/utils.spec.ts @@ -1,4 +1,4 @@ -import { CategoryConfig } from '@quality-metrics/models'; +import { CategoryConfig } from '@code-pushup/models'; import { describe, expect } from 'vitest'; import { calcDuration, diff --git a/packages/utils/src/lib/utils.ts b/packages/utils/src/lib/utils.ts index 8e3139892..760f93aaf 100644 --- a/packages/utils/src/lib/utils.ts +++ b/packages/utils/src/lib/utils.ts @@ -1,4 +1,4 @@ -import { CategoryConfig } from '@quality-metrics/models'; +import { CategoryConfig } from '@code-pushup/models'; export const reportHeadlineText = 'Code Pushup Report'; export const reportOverviewTableHeaders = ['Category', 'Score', 'Audits']; diff --git a/project.json b/project.json index a120bda3b..085f54a1d 100644 --- a/project.json +++ b/project.json @@ -1,5 +1,5 @@ { - "name": "@quality-metrics-cli/source", + "name": "@code-pushup/cli-source", "$schema": "node_modules/nx/schemas/project-schema.json", "targets": { "local-registry": { diff --git a/tsconfig.base.json b/tsconfig.base.json index fa9847b3a..945466d5f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,16 +18,18 @@ "baseUrl": ".", "resolveJsonModule": true, "paths": { - "@quality-metrics/cli": ["packages/cli/src/index.ts"], - "@quality-metrics/core": ["packages/core/src/index.ts"], - "@quality-metrics/eslint-plugin": ["packages/plugin-eslint/src/index.ts"], - "@quality-metrics/lighthouse-plugin": [ + "@code-pushup/cli": ["packages/cli/src/index.ts"], + "@code-pushup/cli/testing": ["packages/cli/test/index.ts"], + "@code-pushup/core": ["packages/core/src/index.ts"], + "@code-pushup/core/testing": ["packages/core/test/index.ts"], + "@code-pushup/eslint-plugin": ["packages/plugin-eslint/src/index.ts"], + "@code-pushup/lighthouse-plugin": [ "packages/plugin-lighthouse/src/index.ts" ], - "@quality-metrics/models": ["packages/models/src/index.ts"], - "@quality-metrics/models/testing": ["packages/models/test/index.ts"], - "@quality-metrics/nx-plugin": ["packages/nx-plugin/src/index.ts"], - "@quality-metrics/utils": ["packages/utils/src/index.ts"] + "@code-pushup/models": ["packages/models/src/index.ts"], + "@code-pushup/models/testing": ["packages/models/test/index.ts"], + "@code-pushup/nx-plugin": ["packages/nx-plugin/src/index.ts"], + "@code-pushup/utils": ["packages/utils/src/index.ts"] } }, "exclude": ["node_modules", "tmp"]