1 | import path from 'path';
|
2 |
|
3 | import { StrykerOptions } from '@stryker-mutator/api/core';
|
4 | import { tokens, commonTokens } from '@stryker-mutator/api/plugin';
|
5 | import { Logger } from '@stryker-mutator/api/logging';
|
6 |
|
7 | import { Project } from '../fs/project.js';
|
8 |
|
9 | import { FilePreprocessor } from './file-preprocessor.js';
|
10 |
|
11 | export interface TSConfig {
|
12 | references?: Array<{ path: string }>;
|
13 | extends?: string;
|
14 | files?: string[];
|
15 | exclude?: string[];
|
16 | include?: string[];
|
17 | compilerOptions?: Record<string, unknown>;
|
18 | }
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | export class TSConfigPreprocessor implements FilePreprocessor {
|
37 | private readonly touched = new Set<string>();
|
38 | public static readonly inject = tokens(commonTokens.logger, commonTokens.options);
|
39 | constructor(private readonly log: Logger, private readonly options: StrykerOptions) {}
|
40 |
|
41 | public async preprocess(project: Project): Promise<void> {
|
42 | if (this.options.inPlace) {
|
43 |
|
44 | return;
|
45 | } else {
|
46 | this.touched.clear();
|
47 | await this.rewriteTSConfigFile(project, path.resolve(this.options.tsconfigFile));
|
48 | }
|
49 | }
|
50 |
|
51 | private async rewriteTSConfigFile(project: Project, tsconfigFileName: string): Promise<void> {
|
52 | if (!this.touched.has(tsconfigFileName)) {
|
53 | this.touched.add(tsconfigFileName);
|
54 | const tsconfigFile = project.files.get(tsconfigFileName);
|
55 | if (tsconfigFile) {
|
56 | this.log.debug('Rewriting file %s', tsconfigFile);
|
57 | const { default: ts } = await import('typescript');
|
58 | const { config }: { config?: TSConfig } = ts.parseConfigFileTextToJson(tsconfigFileName, await tsconfigFile.readContent());
|
59 | if (config) {
|
60 | await this.rewriteExtends(project, config, tsconfigFileName);
|
61 | await this.rewriteProjectReferences(project, config, tsconfigFileName);
|
62 | this.rewriteFileArrayProperty(config, tsconfigFileName, 'include');
|
63 | this.rewriteFileArrayProperty(config, tsconfigFileName, 'exclude');
|
64 | this.rewriteFileArrayProperty(config, tsconfigFileName, 'files');
|
65 | tsconfigFile.setContent(JSON.stringify(config, null, 2));
|
66 | }
|
67 | }
|
68 | }
|
69 | }
|
70 |
|
71 | private async rewriteExtends(project: Project, config: TSConfig, tsconfigFileName: string): Promise<void> {
|
72 | const extend = config.extends;
|
73 | if (typeof extend === 'string') {
|
74 | const rewritten = this.tryRewriteReference(extend, tsconfigFileName);
|
75 | if (rewritten) {
|
76 | config.extends = rewritten;
|
77 | } else {
|
78 | await this.rewriteTSConfigFile(project, path.resolve(path.dirname(tsconfigFileName), extend));
|
79 | }
|
80 | }
|
81 | }
|
82 |
|
83 | private rewriteFileArrayProperty(config: TSConfig, tsconfigFileName: string, prop: 'exclude' | 'files' | 'include'): void {
|
84 | const fileArray = config[prop];
|
85 | if (Array.isArray(fileArray)) {
|
86 | config[prop] = fileArray.map((pattern) => {
|
87 | const rewritten = this.tryRewriteReference(pattern, tsconfigFileName);
|
88 | if (rewritten) {
|
89 | return rewritten;
|
90 | } else {
|
91 | return pattern;
|
92 | }
|
93 | });
|
94 | }
|
95 | }
|
96 |
|
97 | private async rewriteProjectReferences(project: Project, config: TSConfig, originTSConfigFileName: string): Promise<void> {
|
98 | const { default: ts } = await import('typescript');
|
99 | if (Array.isArray(config.references)) {
|
100 | for (const reference of config.references) {
|
101 | const referencePath = ts.resolveProjectReferencePath(reference);
|
102 | const rewritten = this.tryRewriteReference(referencePath, originTSConfigFileName);
|
103 | if (rewritten) {
|
104 | reference.path = rewritten;
|
105 | } else {
|
106 | await this.rewriteTSConfigFile(project, path.resolve(path.dirname(originTSConfigFileName), referencePath));
|
107 | }
|
108 | }
|
109 | }
|
110 | }
|
111 |
|
112 | private tryRewriteReference(reference: string, originTSConfigFileName: string): string | false {
|
113 | const dirName = path.dirname(originTSConfigFileName);
|
114 | const fileName = path.resolve(dirName, reference);
|
115 | const relativeToSandbox = path.relative(process.cwd(), fileName);
|
116 | if (relativeToSandbox.startsWith('..')) {
|
117 | return this.join('..', '..', reference);
|
118 | }
|
119 | return false;
|
120 | }
|
121 |
|
122 | private join(...pathSegments: string[]) {
|
123 | return pathSegments.map((segment) => segment.replace(/\\/g, '/')).join('/');
|
124 | }
|
125 | }
|