UNPKG

4.66 kBPlain TextView Raw
1import path from 'path';
2
3import { StrykerOptions } from '@stryker-mutator/api/core';
4import { tokens, commonTokens } from '@stryker-mutator/api/plugin';
5import { Logger } from '@stryker-mutator/api/logging';
6
7import { Project } from '../fs/project.js';
8
9import { FilePreprocessor } from './file-preprocessor.js';
10
11export 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 * A helper class that rewrites `references` and `extends` file paths if they end up falling outside of the sandbox.
21 * @example
22 * {
23 * "extends": "../../tsconfig.settings.json",
24 * "references": {
25 * "path": "../model"
26 * }
27 * }
28 * becomes:
29 * {
30 * "extends": "../../../../tsconfig.settings.json",
31 * "references": {
32 * "path": "../../../model"
33 * }
34 * }
35 */
36export 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 // If stryker is running 'inPlace', we don't have to change the tsconfig file
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}