diff --git a/e2e/plugin-typescript-e2e/eslint.config.js b/e2e/plugin-typescript-e2e/eslint.config.js new file mode 100644 index 000000000..2656b27cb --- /dev/null +++ b/e2e/plugin-typescript-e2e/eslint.config.js @@ -0,0 +1,12 @@ +import tseslint from 'typescript-eslint'; +import baseConfig from '../../eslint.config.js'; + +export default tseslint.config(...baseConfig, { + files: ['**/*.ts'], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, +}); diff --git a/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/code-pushup.config.ts b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/code-pushup.config.ts new file mode 100644 index 000000000..0d6856059 --- /dev/null +++ b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/code-pushup.config.ts @@ -0,0 +1,22 @@ +import type { CoreConfig } from '@code-pushup/models'; +import { + getCategoryRefsFromGroups, + typescriptPlugin, +} from '@code-pushup/typescript-plugin'; + +export default { + plugins: [ + await typescriptPlugin({ + tsConfigPath: 'tsconfig.json', + }), + ], + categories: [ + { + slug: 'typescript-quality', + title: 'Typescript', + refs: await getCategoryRefsFromGroups({ + tsConfigPath: 'tsconfig.json', + }), + }, + ], +} satisfies CoreConfig; diff --git a/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/exclude/utils.ts b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/exclude/utils.ts new file mode 100644 index 000000000..20f47ca5d --- /dev/null +++ b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/exclude/utils.ts @@ -0,0 +1,3 @@ +export function test() { + return 'test'; +} diff --git a/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/src/1-syntax-errors.ts b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/src/1-syntax-errors.ts new file mode 100644 index 000000000..5ba0acd92 --- /dev/null +++ b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/src/1-syntax-errors.ts @@ -0,0 +1 @@ +const a = { ; // Error: TS1136: Property assignment expected diff --git a/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/src/2-semantic-errors.ts b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/src/2-semantic-errors.ts new file mode 100644 index 000000000..f29009cea --- /dev/null +++ b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/src/2-semantic-errors.ts @@ -0,0 +1,7 @@ +// 2683 - NoImplicitThis: 'this' implicitly has type 'any'. +function noImplicitThisTS2683() { + console.log(this.value); // Error 2683 +} + +// 2531 - StrictNullChecks: Object is possibly 'null'. +const strictNullChecksTS2531: string = null; diff --git a/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/src/4-languale-service.ts b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/src/4-languale-service.ts new file mode 100644 index 000000000..444811dfb --- /dev/null +++ b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/src/4-languale-service.ts @@ -0,0 +1,6 @@ +class Standalone { + override method() { // Error: TS4114 - 'override' modifier can only be used in a class derived from a base class. + console.log("Standalone method"); + } +} +const s = Standalone; diff --git a/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/src/6-configuration-errors.ts b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/src/6-configuration-errors.ts new file mode 100644 index 000000000..c5388fe05 --- /dev/null +++ b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/src/6-configuration-errors.ts @@ -0,0 +1,3 @@ +import { test } from '../exclude/utils'; + +// TS6059:: File 'exclude/utils.ts' is not under 'rootDir' '.../configuration-errors'. 'rootDir' is expected to contain all source files. diff --git a/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/tsconfig.json b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/tsconfig.json new file mode 100644 index 000000000..b9dd367e9 --- /dev/null +++ b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "target": "ES6", + "module": "CommonJS", + "strict": true, + "verbatimModuleSyntax": false + }, + "include": ["src/**/*.ts"] +} diff --git a/e2e/plugin-typescript-e2e/project.json b/e2e/plugin-typescript-e2e/project.json new file mode 100644 index 000000000..4ec3f7c8a --- /dev/null +++ b/e2e/plugin-typescript-e2e/project.json @@ -0,0 +1,23 @@ +{ + "name": "plugin-typescript-e2e", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "e2e/plugin-typescript-e2e/src", + "projectType": "application", + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["e2e/plugin-typescript-e2e/**/*.ts"] + } + }, + "e2e": { + "executor": "@nx/vite:test", + "options": { + "configFile": "e2e/plugin-typescript-e2e/vite.config.e2e.ts" + } + } + }, + "implicitDependencies": ["cli", "plugin-typescript"], + "tags": ["scope:plugin", "type:e2e"] +} diff --git a/e2e/plugin-typescript-e2e/tests/__snapshots__/typescript-plugin-json-report.json b/e2e/plugin-typescript-e2e/tests/__snapshots__/typescript-plugin-json-report.json new file mode 100644 index 000000000..0ec3a50da --- /dev/null +++ b/e2e/plugin-typescript-e2e/tests/__snapshots__/typescript-plugin-json-report.json @@ -0,0 +1,127 @@ +{ + "categories": [ + { + "refs": [ + { + "plugin": "typescript", + "slug": "problems", + "type": "group", + "weight": 1, + }, + { + "plugin": "typescript", + "slug": "ts-configuration", + "type": "group", + "weight": 1, + }, + { + "plugin": "typescript", + "slug": "miscellaneous", + "type": "group", + "weight": 1, + }, + ], + "slug": "typescript-quality", + "title": "Typescript", + }, + ], + "packageName": "@code-pushup/core", + "plugins": [ + { + "audits": [ + { + "description": "Errors that occur during parsing and lexing of TypeScript source code", + "slug": "syntax-errors", + "title": "Syntax errors", + }, + { + "description": "Errors that occur during type checking and type inference", + "slug": "semantic-errors", + "title": "Semantic errors", + }, + { + "description": "Errors that occur during TypeScript language service operations", + "slug": "declaration-and-language-service-errors", + "title": "Declaration and language service errors", + }, + { + "description": "Errors that occur during TypeScript internal operations", + "slug": "internal-errors", + "title": "Internal errors", + }, + { + "description": "Errors that occur when parsing TypeScript configuration files", + "slug": "configuration-errors", + "title": "Configuration errors", + }, + { + "description": "Errors related to no implicit any compiler option", + "slug": "no-implicit-any-errors", + "title": "No implicit any errors", + }, + { + "description": "Errors that do not match any known TypeScript error code", + "slug": "unknown-codes", + "title": "Unknown codes", + }, + ], + "description": "Official Code PushUp Typescript plugin.", + "docsUrl": "https://www.npmjs.com/package/@code-pushup/typescript-plugin/", + "groups": [ + { + "description": "Syntax, semantic, and internal compiler errors are critical for identifying and preventing bugs.", + "refs": [ + { + "slug": "syntax-errors", + "weight": 1, + }, + { + "slug": "semantic-errors", + "weight": 1, + }, + { + "slug": "no-implicit-any-errors", + "weight": 1, + }, + ], + "slug": "problems", + "title": "Problems", + }, + { + "description": "TypeScript configuration and options errors ensure correct project setup, reducing risks from misconfiguration.", + "refs": [ + { + "slug": "configuration-errors", + "weight": 1, + }, + ], + "slug": "ts-configuration", + "title": "Configuration", + }, + { + "description": "Errors that do not bring any specific value to the developer, but are still useful to know.", + "refs": [ + { + "slug": "unknown-codes", + "weight": 1, + }, + { + "slug": "internal-errors", + "weight": 1, + }, + { + "slug": "declaration-and-language-service-errors", + "weight": 1, + }, + ], + "slug": "miscellaneous", + "title": "Miscellaneous", + }, + ], + "icon": "typescript", + "packageName": "@code-pushup/typescript-plugin", + "slug": "typescript", + "title": "Typescript", + }, + ], +} \ No newline at end of file diff --git a/e2e/plugin-typescript-e2e/tests/collect.e2e.test.ts b/e2e/plugin-typescript-e2e/tests/collect.e2e.test.ts new file mode 100644 index 000000000..5706d2580 --- /dev/null +++ b/e2e/plugin-typescript-e2e/tests/collect.e2e.test.ts @@ -0,0 +1,75 @@ +import { cp } from 'node:fs/promises'; +import path from 'node:path'; +import { afterAll, beforeAll, expect } from 'vitest'; +import { type Report, reportSchema } from '@code-pushup/models'; +import { nxTargetProject } from '@code-pushup/test-nx-utils'; +import { + E2E_ENVIRONMENTS_DIR, + TEST_OUTPUT_DIR, + omitVariableReportData, + removeColorCodes, + teardownTestFolder, +} from '@code-pushup/test-utils'; +import { executeProcess, readJsonFile } from '@code-pushup/utils'; + +describe('PLUGIN collect report with typescript-plugin NPM package', () => { + const envRoot = join(E2E_ENVIRONMENTS_DIR, nxTargetProject()); + const distRoot = join(envRoot, TEST_OUTPUT_DIR); + + const fixturesDir = join( + 'e2e', + nxTargetProject(), + 'mocks', + 'fixtures', + 'default-setup', + ); + + beforeAll(async () => { + await cp(fixturesDir, envRoot, { recursive: true }); + }); + + afterAll(async () => { + await teardownTestFolder(distRoot); + }); + + it('should run plugin over CLI and creates report.json', async () => { + const outputDir = join( + path.relative(envRoot, distRoot), + 'create-report', + '.code-pushup', + ); + + const { code, stdout } = await executeProcess({ + command: 'npx', + // verbose exposes audits with perfect scores that are hidden in the default stdout + args: [ + '@code-pushup/cli', + 'collect', + '--no-progress', + '--verbose', + `--persist.outputDir=${outputDir}`, + ], + cwd: envRoot, + }); + + expect(code).toBe(0); + const cleanStdout = removeColorCodes(stdout); + + expect(cleanStdout).toMatch(/● Semantic errors\s+\d+ issue/); + expect(cleanStdout).toMatch(/● Configuration errors\s+\d+ issue/); + expect(cleanStdout).toMatch( + /● Declaration and language service errors\s+\d+ issue/, + ); + expect(cleanStdout).toMatch(/● Syntax errors\s+\d+ issue/); + expect(cleanStdout).toMatch(/● Internal errors\s+\d+ issue/); + expect(cleanStdout).toMatch(/● No implicit any errors\s+\d+ issue/); + + const reportJson = await readJsonFile( + join(envRoot, outputDir, 'report.json'), + ); + expect(() => reportSchema.parse(reportJson)).not.toThrow(); + expect( + omitVariableReportData(reportJson, { omitAuditData: true }), + ).toMatchFileSnapshot('__snapshots__/typescript-plugin-json-report.json'); + }); +}); diff --git a/e2e/plugin-typescript-e2e/tsconfig.json b/e2e/plugin-typescript-e2e/tsconfig.json new file mode 100644 index 000000000..f5a2f890a --- /dev/null +++ b/e2e/plugin-typescript-e2e/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["vitest"] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.test.json" + } + ] +} diff --git a/e2e/plugin-typescript-e2e/tsconfig.test.json b/e2e/plugin-typescript-e2e/tsconfig.test.json new file mode 100644 index 000000000..10c7f79de --- /dev/null +++ b/e2e/plugin-typescript-e2e/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"], + "target": "ES2020" + }, + "include": [ + "vite.config.e2e.ts", + "tests/**/*.e2e.test.ts", + "tests/**/*.d.ts", + "mocks/**/*.ts" + ] +} diff --git a/e2e/plugin-typescript-e2e/vite.config.e2e.ts b/e2e/plugin-typescript-e2e/vite.config.e2e.ts new file mode 100644 index 000000000..3035c644b --- /dev/null +++ b/e2e/plugin-typescript-e2e/vite.config.e2e.ts @@ -0,0 +1,33 @@ +/// +import { defineConfig } from 'vite'; +import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases.js'; + +export default defineConfig({ + cacheDir: '../../node_modules/.vite/plugin-typescript-e2e', + resolve: { + alias: { + '@code-pushup/test-nx-utils': '../../test-nx-utils/index.js', + '@code-pushup/test-setup': '../../test-setup/index.js', + '@code-pushup/test-utils': '../../test-utils/index.js', + }, + }, + test: { + reporters: ['basic'], + testTimeout: 120_000, + globals: true, + alias: tsconfigPathAliases(), + pool: 'threads', + poolOptions: { threads: { singleThread: true } }, + coverage: { + reporter: ['text', 'lcov'], + reportsDirectory: '../../coverage/plugin-typescript-e2e/e2e-tests', + exclude: ['mocks/**', '**/types.ts'], + }, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'node', + include: ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + setupFiles: ['../../testing/test-setup/src/lib/reset.mocks.ts'], + }, +}); diff --git a/packages/plugin-typescript/src/index.ts b/packages/plugin-typescript/src/index.ts index de384e130..7dbcf52bb 100644 --- a/packages/plugin-typescript/src/index.ts +++ b/packages/plugin-typescript/src/index.ts @@ -1,6 +1,6 @@ export { TYPESCRIPT_PLUGIN_SLUG } from './lib/constants.js'; export { typescriptPlugin } from './lib/typescript-plugin.js'; -export { getCategories } from './lib/utils.js'; +export { getCategories, getCategoryRefsFromGroups } from './lib/utils.js'; export { type TypescriptPluginConfig, type TypescriptPluginOptions,