Skip to content

Commit 32f3e51

Browse files
authored
feat(plugin-typescript): add setup wizard binding
1 parent 54501b3 commit 32f3e51

8 files changed

Lines changed: 257 additions & 3 deletions

File tree

‎packages/create-cli/README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ Each plugin exposes its own configuration keys that can be passed as CLI argumen
5858
|**`--js-packages.dependencyGroups`**|`('prod'`\|`'dev'`\|`'optional')[]`|`prod`, `dev`| Dependency groups |
5959
|**`--js-packages.categories`**|`boolean`|`true`| Add JS packages categories |
6060

61+
#### TypeScript
62+
63+
| Option | Type | Default | Description |
64+
| ----------------------------- | --------- | ------------- | ------------------------- |
65+
|**`--typescript.tsconfig`**|`string`| auto-detected | TypeScript config file |
66+
|**`--typescript.categories`**|`boolean`|`true`| Add TypeScript categories |
67+
6168
### Examples
6269

6370
Run interactively (default):

‎packages/create-cli/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@code-pushup/eslint-plugin": "0.122.1",
3131
"@code-pushup/js-packages-plugin": "0.122.1",
3232
"@code-pushup/models": "0.122.1",
33+
"@code-pushup/typescript-plugin": "0.122.1",
3334
"@code-pushup/utils": "0.122.1",
3435
"@inquirer/prompts": "^8.0.0",
3536
"yaml": "^2.5.1",

‎packages/create-cli/src/index.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { hideBin } from 'yargs/helpers';
44
import{coverageSetupBinding}from'@code-pushup/coverage-plugin';
55
import{eslintSetupBinding}from'@code-pushup/eslint-plugin';
66
import{jsPackagesSetupBinding}from'@code-pushup/js-packages-plugin';
7+
import{typescriptSetupBinding}from'@code-pushup/typescript-plugin';
78
import{parsePluginSlugs,validatePluginSlugs}from'./lib/setup/plugins.js';
89
import{
910
CI_PROVIDERS,
@@ -13,11 +14,12 @@ import {
1314
}from'./lib/setup/types.js';
1415
import{runSetupWizard}from'./lib/setup/wizard.js';
1516

16-
// TODO: create, import and pass remaining plugin bindings (lighthouse, typescript, jsdocs, axe)
17+
// TODO: create, import and pass remaining plugin bindings (lighthouse, jsdocs, axe)
1718
constbindings: PluginSetupBinding[]=[
1819
eslintSetupBinding,
1920
coverageSetupBinding,
2021
jsPackagesSetupBinding,
22+
typescriptSetupBinding,
2123
];
2224

2325
constargv=awaityargs(hideBin(process.argv))

‎packages/plugin-typescript/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { typescriptPlugin } from './lib/typescript-plugin.js';
22

33
exportdefaulttypescriptPlugin;
44

5+
export{typescriptSetupBinding}from'./lib/binding.js';
56
export{TYPESCRIPT_PLUGIN_SLUG}from'./lib/constants.js';
67
export{
78
typescriptPluginConfigSchema,
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import{readdir}from'node:fs/promises';
2+
import{createRequire}from'node:module';
3+
importpathfrom'node:path';
4+
importtype{
5+
CategoryConfig,
6+
PluginAnswer,
7+
PluginSetupBinding,
8+
}from'@code-pushup/models';
9+
import{
10+
answerBoolean,
11+
answerString,
12+
fileExists,
13+
singleQuote,
14+
}from'@code-pushup/utils';
15+
import{
16+
DEFAULT_TS_CONFIG,
17+
TSCONFIG_PATTERN,
18+
TYPESCRIPT_PLUGIN_SLUG,
19+
TYPESCRIPT_PLUGIN_TITLE,
20+
}from'./constants.js';
21+
22+
const{name: PACKAGE_NAME}=createRequire(import.meta.url)(
23+
'../../package.json',
24+
)astypeofimport('../../package.json');
25+
26+
constTYPESCRIPT_CATEGORIES: CategoryConfig[]=[
27+
{
28+
slug: 'bug-prevention',
29+
title: 'Bug prevention',
30+
description: 'Type checks that find **potential bugs** in your code.',
31+
refs: [
32+
{
33+
type: 'group',
34+
plugin: TYPESCRIPT_PLUGIN_SLUG,
35+
slug: 'problems',
36+
weight: 1,
37+
},
38+
],
39+
},
40+
];
41+
42+
typeTypescriptOptions={
43+
tsconfig: string;
44+
categories: boolean;
45+
};
46+
47+
exportconsttypescriptSetupBinding={
48+
slug: TYPESCRIPT_PLUGIN_SLUG,
49+
title: TYPESCRIPT_PLUGIN_TITLE,
50+
packageName: PACKAGE_NAME,
51+
isRecommended,
52+
prompts: async(targetDir: string)=>{
53+
consttsconfig=awaitdetectTsconfig(targetDir);
54+
return[
55+
{
56+
key: 'typescript.tsconfig',
57+
message: 'TypeScript config file',
58+
type: 'input',
59+
default: tsconfig,
60+
},
61+
{
62+
key: 'typescript.categories',
63+
message: 'Add TypeScript categories?',
64+
type: 'confirm',
65+
default: true,
66+
},
67+
];
68+
},
69+
generateConfig: (answers: Record<string,PluginAnswer>)=>{
70+
constoptions=parseAnswers(answers);
71+
return{
72+
imports: [
73+
{moduleSpecifier: PACKAGE_NAME,defaultImport: 'typescriptPlugin'},
74+
],
75+
pluginInit: formatPluginInit(options),
76+
...(options.categories ? {categories: TYPESCRIPT_CATEGORIES} : {}),
77+
};
78+
},
79+
}satisfiesPluginSetupBinding;
80+
81+
functionparseAnswers(
82+
answers: Record<string,PluginAnswer>,
83+
): TypescriptOptions{
84+
return{
85+
tsconfig: answerString(answers,'typescript.tsconfig')||DEFAULT_TS_CONFIG,
86+
categories: answerBoolean(answers,'typescript.categories'),
87+
};
88+
}
89+
90+
functionformatPluginInit({ tsconfig }: TypescriptOptions): string[]{
91+
returntsconfig===DEFAULT_TS_CONFIG
92+
? ['typescriptPlugin(),']
93+
: ['typescriptPlugin({',` tsconfig: ${singleQuote(tsconfig)},`,'}),'];
94+
}
95+
96+
asyncfunctionisRecommended(targetDir: string): Promise<boolean>{
97+
return(
98+
(awaitfileExists(path.join(targetDir,'tsconfig.json')))||
99+
(awaitfileExists(path.join(targetDir,'tsconfig.base.json')))
100+
);
101+
}
102+
103+
asyncfunctiondetectTsconfig(targetDir: string): Promise<string>{
104+
constfiles=awaitreaddir(targetDir,{encoding: 'utf8'});
105+
constmatch=files.find(file=>TSCONFIG_PATTERN.test(file));
106+
returnmatch??DEFAULT_TS_CONFIG;
107+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import{vol}from'memfs';
2+
importtype{PluginAnswer}from'@code-pushup/models';
3+
import{MEMFS_VOLUME}from'@code-pushup/test-utils';
4+
import{typescriptSetupBindingasbinding}from'./binding.js';
5+
6+
constdefaultAnswers: Record<string,PluginAnswer>={
7+
'typescript.tsconfig': 'tsconfig.json',
8+
'typescript.categories': true,
9+
};
10+
11+
describe('typescriptSetupBinding',()=>{
12+
beforeEach(()=>{
13+
vol.fromJSON({'.gitkeep': ''},MEMFS_VOLUME);
14+
});
15+
16+
describe('isRecommended',()=>{
17+
it('should recommend when tsconfig.json exists',async()=>{
18+
vol.fromJSON({'tsconfig.json': '{}'},MEMFS_VOLUME);
19+
20+
awaitexpect(binding.isRecommended(MEMFS_VOLUME)).resolves.toBeTrue();
21+
});
22+
23+
it('should recommend when tsconfig.base.json exists',async()=>{
24+
vol.fromJSON({'tsconfig.base.json': '{}'},MEMFS_VOLUME);
25+
26+
awaitexpect(binding.isRecommended(MEMFS_VOLUME)).resolves.toBeTrue();
27+
});
28+
29+
it('should not recommend when no tsconfig found',async()=>{
30+
awaitexpect(binding.isRecommended(MEMFS_VOLUME)).resolves.toBeFalse();
31+
});
32+
});
33+
34+
describe('prompts',()=>{
35+
it('should detect tsconfig.json as default',async()=>{
36+
vol.fromJSON({'tsconfig.json': '{}'},MEMFS_VOLUME);
37+
38+
awaitexpect(
39+
binding.prompts(MEMFS_VOLUME),
40+
).resolves.toIncludeAllPartialMembers([
41+
{key: 'typescript.tsconfig',default: 'tsconfig.json'},
42+
]);
43+
});
44+
45+
it('should detect tsconfig.base.json when present',async()=>{
46+
vol.fromJSON({'tsconfig.base.json': '{}'},MEMFS_VOLUME);
47+
48+
awaitexpect(
49+
binding.prompts(MEMFS_VOLUME),
50+
).resolves.toIncludeAllPartialMembers([
51+
{key: 'typescript.tsconfig',default: 'tsconfig.base.json'},
52+
]);
53+
});
54+
55+
it('should detect tsconfig.app.json when present',async()=>{
56+
vol.fromJSON({'tsconfig.app.json': '{}'},MEMFS_VOLUME);
57+
58+
awaitexpect(
59+
binding.prompts(MEMFS_VOLUME),
60+
).resolves.toIncludeAllPartialMembers([
61+
{key: 'typescript.tsconfig',default: 'tsconfig.app.json'},
62+
]);
63+
});
64+
65+
it('should default to tsconfig.json when no tsconfig found',async()=>{
66+
awaitexpect(
67+
binding.prompts(MEMFS_VOLUME),
68+
).resolves.toIncludeAllPartialMembers([
69+
{key: 'typescript.tsconfig',default: 'tsconfig.json'},
70+
]);
71+
});
72+
73+
it('should default categories confirmation to true',async()=>{
74+
awaitexpect(
75+
binding.prompts(MEMFS_VOLUME),
76+
).resolves.toIncludeAllPartialMembers([
77+
{key: 'typescript.categories',type: 'confirm',default: true},
78+
]);
79+
});
80+
});
81+
82+
describe('generateConfig',()=>{
83+
it('should omit tsconfig option when using default tsconfig.json',()=>{
84+
expect(binding.generateConfig(defaultAnswers).pluginInit).toEqual([
85+
'typescriptPlugin(),',
86+
]);
87+
});
88+
89+
it('should include tsconfig when non-default path provided',()=>{
90+
expect(
91+
binding.generateConfig({
92+
...defaultAnswers,
93+
'typescript.tsconfig': 'tsconfig.base.json',
94+
}).pluginInit,
95+
).toEqual([
96+
'typescriptPlugin({',
97+
" tsconfig: 'tsconfig.base.json',",
98+
'}),',
99+
]);
100+
});
101+
102+
it('should generate bug-prevention category from problems group when confirmed',()=>{
103+
expect(binding.generateConfig(defaultAnswers).categories).toEqual([
104+
expect.objectContaining({
105+
slug: 'bug-prevention',
106+
refs: [
107+
expect.objectContaining({
108+
type: 'group',
109+
plugin: 'typescript',
110+
slug: 'problems',
111+
}),
112+
],
113+
}),
114+
]);
115+
});
116+
117+
it('should omit categories when declined',()=>{
118+
expect(
119+
binding.generateConfig({
120+
...defaultAnswers,
121+
'typescript.categories': false,
122+
}).categories,
123+
).toBeUndefined();
124+
});
125+
126+
it('should import from @code-pushup/typescript-plugin',()=>{
127+
expect(binding.generateConfig(defaultAnswers).imports).toEqual([
128+
{
129+
moduleSpecifier: '@code-pushup/typescript-plugin',
130+
defaultImport: 'typescriptPlugin',
131+
},
132+
]);
133+
});
134+
});
135+
});

‎packages/plugin-typescript/src/lib/constants.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const TYPESCRIPT_PLUGIN_TITLE = 'TypeScript';
88

99
exportconstDEFAULT_TS_CONFIG='tsconfig.json';
1010

11+
exportconstTSCONFIG_PATTERN=/^tsconfig(\..+)?\.json$/;
12+
1113
constAUDIT_DESCRIPTIONS: Record<AuditSlug,string>={
1214
'semantic-errors':
1315
'Errors that occur during type checking and type inference',

‎packages/plugin-typescript/src/lib/nx/tsconfig-paths.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import { readdir } from 'node:fs/promises';
33
importpathfrom'node:path';
44
import{readConfigFile,sys}from'typescript';
55
import{loadNxProjectGraph,logger,pluralizeToken}from'@code-pushup/utils';
6+
import{TSCONFIG_PATTERN}from'../constants.js';
67
import{formatMetaLog}from'../format.js';
78

8-
constTSCONFIG_PATTERN=/^tsconfig(\..+)?\.json$/;
9-
109
/**
1110
* Returns true only if config explicitly defines files or include with values.
1211
*/

0 commit comments

Comments
 (0)