Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/create-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ Each plugin exposes its own configuration keys that can be passed as CLI argumen
| **`--js-packages.dependencyGroups`** | `('prod'` \| `'dev'` \| `'optional')[]` | `prod`, `dev` | Dependency groups |
| **`--js-packages.categories`** | `boolean` | `true` | Add JS packages categories |

#### TypeScript

| Option | Type | Default | Description |
| ----------------------------- | --------- | ------------- | ------------------------- |
| **`--typescript.tsconfig`** | `string` | auto-detected | TypeScript config file |
| **`--typescript.categories`** | `boolean` | `true` | Add TypeScript categories |

### Examples

Run interactively (default):
Expand Down
1 change: 1 addition & 0 deletions packages/create-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@code-pushup/eslint-plugin": "0.122.0",
"@code-pushup/js-packages-plugin": "0.122.0",
"@code-pushup/models": "0.122.0",
"@code-pushup/typescript-plugin": "0.122.0",
"@code-pushup/utils": "0.122.0",
"@inquirer/prompts": "^8.0.0",
"yaml": "^2.5.1",
Expand Down
4 changes: 3 additions & 1 deletion packages/create-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { hideBin } from 'yargs/helpers';
import { coverageSetupBinding } from '@code-pushup/coverage-plugin';
import { eslintSetupBinding } from '@code-pushup/eslint-plugin';
import { jsPackagesSetupBinding } from '@code-pushup/js-packages-plugin';
import { typescriptSetupBinding } from '@code-pushup/typescript-plugin';
import { parsePluginSlugs, validatePluginSlugs } from './lib/setup/plugins.js';
import {
CI_PROVIDERS,
Expand All @@ -13,11 +14,12 @@ import {
} from './lib/setup/types.js';
import { runSetupWizard } from './lib/setup/wizard.js';

// TODO: create, import and pass remaining plugin bindings (lighthouse, typescript, jsdocs, axe)
// TODO: create, import and pass remaining plugin bindings (lighthouse, jsdocs, axe)
const bindings: PluginSetupBinding[] = [
eslintSetupBinding,
coverageSetupBinding,
jsPackagesSetupBinding,
typescriptSetupBinding,
];

const argv = await yargs(hideBin(process.argv))
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { typescriptPlugin } from './lib/typescript-plugin.js';

export default typescriptPlugin;

export { typescriptSetupBinding } from './lib/binding.js';
export { TYPESCRIPT_PLUGIN_SLUG } from './lib/constants.js';
export {
typescriptPluginConfigSchema,
Expand Down
107 changes: 107 additions & 0 deletions packages/plugin-typescript/src/lib/binding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { readdir } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import type {
CategoryConfig,
PluginAnswer,
PluginSetupBinding,
} from '@code-pushup/models';
import {
answerBoolean,
answerString,
fileExists,
singleQuote,
} from '@code-pushup/utils';
import {
DEFAULT_TS_CONFIG,
TSCONFIG_PATTERN,
TYPESCRIPT_PLUGIN_SLUG,
TYPESCRIPT_PLUGIN_TITLE,
} from './constants.js';

const { name: PACKAGE_NAME } = createRequire(import.meta.url)(
'../../package.json',
) as typeof import('../../package.json');

const TYPESCRIPT_CATEGORIES: CategoryConfig[] = [
{
slug: 'bug-prevention',
title: 'Bug prevention',
description: 'Type checks that find **potential bugs** in your code.',
refs: [
{
type: 'group',
plugin: TYPESCRIPT_PLUGIN_SLUG,
slug: 'problems',
weight: 1,
},
],
},
];

type TypescriptOptions = {
tsconfig: string;
categories: boolean;
};

export const typescriptSetupBinding = {
slug: TYPESCRIPT_PLUGIN_SLUG,
title: TYPESCRIPT_PLUGIN_TITLE,
packageName: PACKAGE_NAME,
isRecommended,
prompts: async (targetDir: string) => {
const tsconfig = await detectTsconfig(targetDir);
return [
{
key: 'typescript.tsconfig',
message: 'TypeScript config file',
type: 'input',
default: tsconfig,
},
{
key: 'typescript.categories',
message: 'Add TypeScript categories?',
type: 'confirm',
default: true,
},
];
},
generateConfig: (answers: Record<string, PluginAnswer>) => {
const options = parseAnswers(answers);
return {
imports: [
{ moduleSpecifier: PACKAGE_NAME, defaultImport: 'typescriptPlugin' },
],
pluginInit: formatPluginInit(options),
...(options.categories ? { categories: TYPESCRIPT_CATEGORIES } : {}),
};
},
} satisfies PluginSetupBinding;

function parseAnswers(
answers: Record<string, PluginAnswer>,
): TypescriptOptions {
return {
tsconfig: answerString(answers, 'typescript.tsconfig') || DEFAULT_TS_CONFIG,
categories: answerBoolean(answers, 'typescript.categories'),
};
}

function formatPluginInit({ tsconfig }: TypescriptOptions): string[] {
return tsconfig === DEFAULT_TS_CONFIG
? ['typescriptPlugin(),']
: ['typescriptPlugin({', ` tsconfig: ${singleQuote(tsconfig)},`, '}),'];
}

async function isRecommended(targetDir: string): Promise<boolean> {
return (
(await fileExists(path.join(targetDir, 'tsconfig.json'))) ||
(await fileExists(path.join(targetDir, 'tsconfig.base.json')))
);
}

async function detectTsconfig(targetDir: string): Promise<string> {
const files = await readdir(targetDir, { encoding: 'utf8' });
const match = files.find(file => TSCONFIG_PATTERN.test(file));
return match ?? DEFAULT_TS_CONFIG;
}
135 changes: 135 additions & 0 deletions packages/plugin-typescript/src/lib/binding.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { vol } from 'memfs';
import type { PluginAnswer } from '@code-pushup/models';
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
import { typescriptSetupBinding as binding } from './binding.js';

const defaultAnswers: Record<string, PluginAnswer> = {
'typescript.tsconfig': 'tsconfig.json',
'typescript.categories': true,
};

describe('typescriptSetupBinding', () => {
beforeEach(() => {
vol.fromJSON({ '.gitkeep': '' }, MEMFS_VOLUME);
});

describe('isRecommended', () => {
it('should recommend when tsconfig.json exists', async () => {
vol.fromJSON({ 'tsconfig.json': '{}' }, MEMFS_VOLUME);

await expect(binding.isRecommended(MEMFS_VOLUME)).resolves.toBeTrue();
});

it('should recommend when tsconfig.base.json exists', async () => {
vol.fromJSON({ 'tsconfig.base.json': '{}' }, MEMFS_VOLUME);

await expect(binding.isRecommended(MEMFS_VOLUME)).resolves.toBeTrue();
});

it('should not recommend when no tsconfig found', async () => {
await expect(binding.isRecommended(MEMFS_VOLUME)).resolves.toBeFalse();
});
});

describe('prompts', () => {
it('should detect tsconfig.json as default', async () => {
vol.fromJSON({ 'tsconfig.json': '{}' }, MEMFS_VOLUME);

await expect(
binding.prompts(MEMFS_VOLUME),
).resolves.toIncludeAllPartialMembers([
{ key: 'typescript.tsconfig', default: 'tsconfig.json' },
]);
});

it('should detect tsconfig.base.json when present', async () => {
vol.fromJSON({ 'tsconfig.base.json': '{}' }, MEMFS_VOLUME);

await expect(
binding.prompts(MEMFS_VOLUME),
).resolves.toIncludeAllPartialMembers([
{ key: 'typescript.tsconfig', default: 'tsconfig.base.json' },
]);
});

it('should detect tsconfig.app.json when present', async () => {
vol.fromJSON({ 'tsconfig.app.json': '{}' }, MEMFS_VOLUME);

await expect(
binding.prompts(MEMFS_VOLUME),
).resolves.toIncludeAllPartialMembers([
{ key: 'typescript.tsconfig', default: 'tsconfig.app.json' },
]);
});

it('should default to tsconfig.json when no tsconfig found', async () => {
await expect(
binding.prompts(MEMFS_VOLUME),
).resolves.toIncludeAllPartialMembers([
{ key: 'typescript.tsconfig', default: 'tsconfig.json' },
]);
});

it('should default categories confirmation to true', async () => {
await expect(
binding.prompts(MEMFS_VOLUME),
).resolves.toIncludeAllPartialMembers([
{ key: 'typescript.categories', type: 'confirm', default: true },
]);
});
});

describe('generateConfig', () => {
it('should omit tsconfig option when using default tsconfig.json', () => {
expect(binding.generateConfig(defaultAnswers).pluginInit).toEqual([
'typescriptPlugin(),',
]);
});

it('should include tsconfig when non-default path provided', () => {
expect(
binding.generateConfig({
...defaultAnswers,
'typescript.tsconfig': 'tsconfig.base.json',
}).pluginInit,
).toEqual([
'typescriptPlugin({',
" tsconfig: 'tsconfig.base.json',",
'}),',
]);
});

it('should generate bug-prevention category from problems group when confirmed', () => {
expect(binding.generateConfig(defaultAnswers).categories).toEqual([
expect.objectContaining({
slug: 'bug-prevention',
refs: [
expect.objectContaining({
type: 'group',
plugin: 'typescript',
slug: 'problems',
}),
],
}),
]);
});

it('should omit categories when declined', () => {
expect(
binding.generateConfig({
...defaultAnswers,
'typescript.categories': false,
}).categories,
).toBeUndefined();
});

it('should import from @code-pushup/typescript-plugin', () => {
expect(binding.generateConfig(defaultAnswers).imports).toEqual([
{
moduleSpecifier: '@code-pushup/typescript-plugin',
defaultImport: 'typescriptPlugin',
},
]);
});
});
});
2 changes: 2 additions & 0 deletions packages/plugin-typescript/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const TYPESCRIPT_PLUGIN_TITLE = 'TypeScript';

export const DEFAULT_TS_CONFIG = 'tsconfig.json';

export const TSCONFIG_PATTERN = /^tsconfig(\..+)?\.json$/;

const AUDIT_DESCRIPTIONS: Record<AuditSlug, string> = {
'semantic-errors':
'Errors that occur during type checking and type inference',
Expand Down
3 changes: 1 addition & 2 deletions packages/plugin-typescript/src/lib/nx/tsconfig-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { readdir } from 'node:fs/promises';
import path from 'node:path';
import { readConfigFile, sys } from 'typescript';
import { loadNxProjectGraph, logger, pluralizeToken } from '@code-pushup/utils';
import { TSCONFIG_PATTERN } from '../constants.js';
import { formatMetaLog } from '../format.js';

const TSCONFIG_PATTERN = /^tsconfig(\..+)?\.json$/;

/**
* Returns true only if config explicitly defines files or include with values.
*/
Expand Down
Loading