Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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