Skip to content

Commit 1b4d4bb

Browse files
committed
feat: add ts config default generator
1 parent e229455 commit 1b4d4bb

12 files changed

+396
-2
lines changed

code-pushup.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,6 @@ export default mergeConfigs(
4242
await eslintCoreConfigNx(),
4343
await typescriptPluginConfigNx({
4444
tsConfigPath: 'packages/plugin-typescript/tsconfig.lib.json',
45+
// onlyAudits: ['verbatim-module-syntax-typescript']
4546
}),
4647
);
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
{
2+
"compilerOptions": {
3+
/* Visit https://aka.ms/tsconfig to read more about this file */
4+
5+
/* Projects */
6+
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7+
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8+
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9+
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10+
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11+
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12+
13+
/* Language and Environment */
14+
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15+
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16+
// "jsx": "preserve", /* Specify what JSX code is generated. */
17+
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18+
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19+
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20+
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21+
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22+
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23+
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24+
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25+
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26+
27+
/* Modules */
28+
"module": "commonjs", /* Specify what module code is generated. */
29+
// "rootDir": "./", /* Specify the root folder within your source files. */
30+
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
31+
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32+
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33+
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34+
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35+
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
36+
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37+
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38+
// "resolveJsonModule": true, /* Enable importing .json files. */
39+
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
40+
41+
/* JavaScript Support */
42+
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
43+
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44+
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
45+
46+
/* Emit */
47+
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48+
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
49+
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50+
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51+
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
52+
// "outDir": "./", /* Specify an output folder for all emitted files. */
53+
// "removeComments": true, /* Disable emitting comments. */
54+
// "noEmit": true, /* Disable emitting files from a compilation. */
55+
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56+
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
57+
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58+
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59+
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60+
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
61+
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62+
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63+
// "newLine": "crlf", /* Set the newline character for emitting files. */
64+
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
65+
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
66+
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67+
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
68+
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69+
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70+
71+
/* Interop Constraints */
72+
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73+
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74+
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
75+
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76+
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
77+
78+
/* Type Checking */
79+
"strict": true, /* Enable all strict type-checking options. */
80+
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
81+
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
82+
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83+
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
84+
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85+
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
86+
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
87+
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88+
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
89+
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90+
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91+
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92+
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93+
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
94+
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95+
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
96+
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97+
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98+
99+
/* Completeness */
100+
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101+
"skipLibCheck": true /* Skip type checking all .d.ts files. */
102+
}
103+
}

packages/plugin-typescript/project.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
"options": {
3636
"configFile": "packages/plugin-typescript/vite.config.integration.ts"
3737
}
38+
},
39+
"generate-ts-defaults": {
40+
"command": "npx tsx --tsconfig=packages/plugin-typescript/tsconfig.tools.json packages/plugin-typescript/tools/bin.ts"
3841
}
3942
},
4043
"tags": ["scope:plugin", "type:feature", "publishable"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { updateKnownConfigMap } from './generate-ts-config';
2+
3+
(async () => await updateKnownConfigMap())();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { join } from 'node:path';
2+
import { describe, expect, it } from 'vitest';
3+
import { readTextFile } from '@code-pushup/utils';
4+
import { prepareTsConfigFileContent } from './generate-ts-config.js';
5+
6+
describe('prepareTsConfigFileContent', () => {
7+
it('should parse tsconfig.json created from init command', async () => {
8+
const testContent = await readTextFile(
9+
join('./packages/plugin-typescript/mocks/fixtures', 'tsconfig.init.json'),
10+
);
11+
expect(prepareTsConfigFileContent(testContent)).toMatchSnapshot();
12+
});
13+
});
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Run typescript init (with a version specified) to generate a tsconfig.json that will have all defaults listed.
2+
// store this json per ts version in src/default-configs.ts
3+
// get a list of TS version, maybe from npm and somehow filter only versions
4+
5+
/*
6+
0. In a chron job on GitHub
7+
1. Load known config defaults for TS versions from the `src/lib/ts-config` folder. The files are named after the TS version e.g. `1.4.2.ts`.
8+
2. Load all existing ts versions from NPM. `npm view typescript versions --json`
9+
2.1. filter for relevant releases `relevantVersions` (only 3 version segments e.g. `3.4.1`):
10+
- start from `1.6.2` as before that there was no init
11+
- skip all pre-release versions: like `3.5.0-dev.20190404`, `3.6.3-insiders.20190909`, and release candidates like `3.6.1-rc`
12+
3. Iterate over `version` in `relevantVersions`
13+
3.1. If the `version` is present in `knownConfigMap` continue
14+
3.2 Else, run `npx -y -p=typescript@<version> tsc --init`
15+
3.3 If the config is identical to the previous version stored in `knownConfigMap` continue
16+
3.4 Else, extract the json form the generated `tsconfig.json` and store data under `version` in `knownConfigMap`
17+
4. Optional cleanup run `npm uninstall typescript@5.4.2 -g`
18+
5. Save new known configs into `src/lib/ts-config-per-version.ts`
19+
*/
20+
import { executeProcess } from '@push-based/nx-verdaccio/src/internal/execute-process';
21+
import { ensureDirectoryExists } from '@push-based/nx-verdaccio/src/internal/file-system';
22+
import { readdir, writeFile } from 'node:fs/promises';
23+
import { basename, join } from 'node:path';
24+
import * as process from 'node:process';
25+
import type { CompilerOptions } from 'typescript';
26+
import { readJsonFile, readTextFile } from '@code-pushup/utils';
27+
28+
export type SemVerString = `${number}.${number}.${number}`;
29+
30+
export const TS_CONFIG_DIR = join(
31+
'packages',
32+
'plugin-typescript',
33+
'src',
34+
'lib',
35+
'default-ts-configs',
36+
);
37+
export const TMP_TS_CONFIG_DIR = join('tmp', 'plugin-typescript-ts-config');
38+
39+
/**
40+
* As typescript does not expose a way to get the default config, we need to maintain them programmatically.
41+
* To save memory and have a cleaner git diff we store the configs per version in separate files.
42+
*
43+
* Folder structure
44+
*
45+
* src/lib/ts-config
46+
* ├── 1.4.2.ts
47+
* ├── 1.4.3.ts
48+
* ├── ....ts
49+
*
50+
* @example
51+
* // src/lib/ts-config/1.4.2.ts
52+
*
53+
* export default {
54+
* "compilerOptions": {
55+
* "target": "es5",
56+
* "module": "commonjs",
57+
* "outDir": "./dist",
58+
* "rootDir": "./src",
59+
* "strict": true,
60+
* "esModuleInterop": true,
61+
* "skipLibCheck": true,
62+
* }
63+
* }
64+
*/
65+
66+
/**
67+
* Iterate over `version` in `relevantVersions`
68+
* If the `version` is present in `knownConfigMap` continue
69+
* Else, run `npx -y -p=typescript@<version> tsc --init`
70+
* If the config is identical to the previous version stored in `knownConfigMap` continue
71+
* Else, extract the json form the generated `tsconfig.json` and store data under `version` in `knownConfigMap`
72+
* Optional cleanup run `npm uninstall typescript@5.4.2 -g`
73+
*
74+
* @param version
75+
* @param config
76+
*/
77+
export async function updateKnownConfigMap() {
78+
const knownVersions = await loadKnownVersions();
79+
const relevantVersions = await getRelevantVersions();
80+
const versionsToGenerate = relevantVersions.filter(
81+
version => !knownVersions.includes(version),
82+
);
83+
84+
console.log(
85+
`generate TS config defaults for ${versionsToGenerate.length} versions`,
86+
);
87+
88+
await Promise.all(versionsToGenerate.map(saveDefaultTsConfig));
89+
}
90+
91+
export async function saveDefaultTsConfig(version: SemVerString) {
92+
await generateTsConfigFile(version);
93+
const config = await extractTsConfig(version);
94+
await cleanupNpmCache(version);
95+
return writeFile(
96+
join(TS_CONFIG_DIR, `${version}.ts`),
97+
`export default ${JSON.stringify(config, null, 2)}`,
98+
);
99+
}
100+
101+
export async function generateTsConfigFile(version: SemVerString) {
102+
const dir = join(TMP_TS_CONFIG_DIR, version);
103+
await ensureDirectoryExists(dir);
104+
await executeProcess({
105+
command: 'npx',
106+
args: ['-y', `-p=typescript@${version}`, 'tsc', '--init'],
107+
cwd: dir,
108+
});
109+
}
110+
111+
/**
112+
* Extract the json form the generated `tsconfig.json` and store data under `version` in `knownConfigMap`
113+
* @param version
114+
*/
115+
export async function extractTsConfig(
116+
version: SemVerString,
117+
): Promise<CompilerOptions> {
118+
const dir = join(TMP_TS_CONFIG_DIR, version);
119+
await ensureDirectoryExists(dir);
120+
try {
121+
const fileContent = await readTextFile(join(dir, 'tsconfig.json'));
122+
123+
await writeFile(
124+
join(TMP_TS_CONFIG_DIR, version, `tsconfig.clean.json`),
125+
prepareTsConfigFileContent(fileContent),
126+
);
127+
const stConfigJson = JSON.parse(prepareTsConfigFileContent(fileContent));
128+
129+
return stConfigJson;
130+
} catch (e) {
131+
throw new Error(
132+
`Failed to extract tsconfig.json for version ${version}. \n ${(e as Error).message}`,
133+
);
134+
}
135+
}
136+
137+
/**
138+
* Cleanup run `npm uninstall typescript@5.4.2 -g`
139+
* @param version
140+
*/
141+
export async function cleanupNpmCache(version: SemVerString) {
142+
await executeProcess({
143+
command: 'npm',
144+
args: ['uninstall', `typescript@${version}`, '-g'],
145+
});
146+
}
147+
148+
/**
149+
* Load known config defaults for TS versions from the `src / lib / ts - config` folder. The files are named after the TS version e.g. `1.4.2.ts`.
150+
*/
151+
export async function loadKnownVersions() {
152+
await ensureDirectoryExists(TS_CONFIG_DIR);
153+
// load known config defaults for TS versions from the `src / lib / ts - config` folder. The files are named after the TS version e.g. `1.4.2.ts`.
154+
const dirContent = await readdir(join(process.cwd(), TS_CONFIG_DIR));
155+
return dirContent.map(
156+
file => basename(file).replace('.ts', '') as SemVerString,
157+
);
158+
}
159+
160+
/**
161+
* Loads all existing TS versions from NPM via `npm view typescript versions--json`.
162+
* Filter for relevant releases `relevantVersions` (only 3 version segments e.g. `3.4.1`):
163+
* - start from `1.6.2` as before that there was no init
164+
* - skip all pre-release versions: like `3.5.0 - dev.20190404`, `3.6.3 - insiders.20190909`, and release candidates like `3.6.1 - rc`
165+
*/
166+
export async function getRelevantVersions() {
167+
const { stdout } = await executeProcess({
168+
command: 'npm',
169+
args: ['view', 'typescript', 'versions', '--json'],
170+
});
171+
const allVersions: SemVerString[] = JSON.parse(stdout);
172+
return allVersions.filter(version => {
173+
const [major, minor, patch] = version.split('.').map(Number);
174+
return (
175+
major >= 1 &&
176+
minor >= 6 &&
177+
patch >= 2 &&
178+
!version.includes('rc') &&
179+
!version.includes('dev') &&
180+
!version.includes('insiders')
181+
);
182+
});
183+
}
184+
185+
export function prepareTsConfigFileContent(fileContent: string) {
186+
const parsedFileContent = fileContent
187+
.split('\n')
188+
.map(line =>
189+
line
190+
// replace all /**/ comments with empty string
191+
.replace(/\r/g, '')
192+
.replace(/\/\*.*\*\//g, '')
193+
// replace all // strings with empty string
194+
.replace(/\/\//g, '')
195+
.replace(/:\s*([^,\n\r]*)\s*\/\/.*$/gm, ': $1')
196+
.replace(/,(\s*[}\]])/gm, '$1')
197+
.trim(),
198+
)
199+
.filter(s => s !== '')
200+
.map(s => {
201+
// if it is a property with a value, check if there is a comma at the end
202+
if (s.match(/:\s*[^,\n\r]*$/)) {
203+
return s.replace(/:\s*([^,]*)$/, ': $1,');
204+
}
205+
return s;
206+
})
207+
.join('')
208+
// last camma is not allowed
209+
.replace(/,\s*}/gm, '}');
210+
211+
return parsedFileContent;
212+
}

0 commit comments

Comments
 (0)