Skip to content

Commit

Permalink
fix(tsconfig): rewrite "include" patterns (#3293)
Browse files Browse the repository at this point in the history
Rewrite "include" patterns in tsconfig files that fall outside the sandbox.

For example:

```json
{
  include: ['./**/*', '../../node_modules/self-service-server/lib/main/shared/@types/**/*.d.ts'],
}
```

Becomes:

```json
{
  include: ['./**/*', '../../../../node_modules/self-service-server/lib/main/shared/@types/**/*.d.ts'],
}
```

Same is true for `files` and `exclude`.

Fixes #3281
  • Loading branch information
nicojs committed Dec 2, 2021
1 parent dc89215 commit 37ead22
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 23 deletions.
59 changes: 43 additions & 16 deletions packages/core/src/sandbox/ts-config-preprocessor.ts
Expand Up @@ -6,9 +6,13 @@ import { Logger } from '@stryker-mutator/api/logging';

import { FilePreprocessor } from './file-preprocessor';

interface TSConfigReferences {
export interface TSConfig {
references?: Array<{ path: string }>;
extends?: string;
files?: string[];
exclude?: string[];
include?: string[];
compilerOptions?: Record<string, unknown>;
}
/**
* A helper class that rewrites `references` and `extends` file paths if they end up falling outside of the sandbox.
Expand Down Expand Up @@ -59,47 +63,70 @@ export class TSConfigPreprocessor implements FilePreprocessor {
if (tsconfigFile) {
this.log.debug('Rewriting file %s', tsconfigFile);
const ts = await import('typescript');
const { config }: { config?: TSConfigReferences } = ts.parseConfigFileTextToJson(tsconfigFile.name, tsconfigFile.textContent);
const { config }: { config?: TSConfig } = ts.parseConfigFileTextToJson(tsconfigFile.name, tsconfigFile.textContent);
if (config) {
await this.rewriteExtends(config, tsconfigFileName);
await this.rewriteProjectReferences(config, tsconfigFileName);
this.rewriteFileArrayProperty(config, tsconfigFileName, 'include');
this.rewriteFileArrayProperty(config, tsconfigFileName, 'exclude');
this.rewriteFileArrayProperty(config, tsconfigFileName, 'files');
this.fs.set(tsconfigFileName, new File(tsconfigFileName, JSON.stringify(config, null, 2)));
}
}
}
}

private async rewriteExtends(config: TSConfigReferences, tsconfigFileName: string): Promise<boolean> {
private async rewriteExtends(config: TSConfig, tsconfigFileName: string): Promise<void> {
const extend = config.extends;
if (typeof extend === 'string') {
const extendsFileName = path.resolve(path.dirname(tsconfigFileName), extend);
const relativeToSandbox = path.relative(process.cwd(), extendsFileName);
if (relativeToSandbox.startsWith('..')) {
config.extends = this.join('..', '..', extend);
return true;
const rewritten = this.tryRewriteReference(extend, tsconfigFileName);
if (rewritten) {
config.extends = rewritten;
} else {
await this.rewriteTSConfigFile(extendsFileName);
await this.rewriteTSConfigFile(path.resolve(path.dirname(tsconfigFileName), extend));
}
}
return false;
}

private async rewriteProjectReferences(config: TSConfigReferences, originTSConfigFileName: string): Promise<void> {
private rewriteFileArrayProperty(config: TSConfig, tsconfigFileName: string, prop: 'exclude' | 'files' | 'include'): void {
const fileArray = config[prop];
if (Array.isArray(fileArray)) {
config[prop] = fileArray.map((pattern) => {
const rewritten = this.tryRewriteReference(pattern, tsconfigFileName);
if (rewritten) {
return rewritten;
} else {
return pattern;
}
});
}
}

private async rewriteProjectReferences(config: TSConfig, originTSConfigFileName: string): Promise<void> {
const ts = await import('typescript');
if (Array.isArray(config.references)) {
for (const reference of config.references) {
const referencePath = ts.resolveProjectReferencePath(reference);
const referencedProjectFileName = path.resolve(path.dirname(originTSConfigFileName), referencePath);
const relativeToProject = path.relative(process.cwd(), referencedProjectFileName);
if (relativeToProject.startsWith('..')) {
reference.path = this.join('..', '..', referencePath);
const rewritten = this.tryRewriteReference(referencePath, originTSConfigFileName);
if (rewritten) {
reference.path = rewritten;
} else {
await this.rewriteTSConfigFile(referencedProjectFileName);
await this.rewriteTSConfigFile(path.resolve(path.dirname(originTSConfigFileName), referencePath));
}
}
}
}

private tryRewriteReference(reference: string, originTSConfigFileName: string): string | false {
const dirName = path.dirname(originTSConfigFileName);
const fileName = path.resolve(dirName, reference);
const relativeToSandbox = path.relative(process.cwd(), fileName);
if (relativeToSandbox.startsWith('..')) {
return this.join('..', '..', reference);
}
return false;
}

private join(...pathSegments: string[]) {
return pathSegments.map((segment) => segment.replace(/\\/g, '/')).join('/');
}
Expand Down
52 changes: 45 additions & 7 deletions packages/core/test/unit/sandbox/ts-config-preprocessor.it.spec.ts
Expand Up @@ -4,7 +4,7 @@ import { expect } from 'chai';
import { File } from '@stryker-mutator/api/core';
import { assertions, testInjector } from '@stryker-mutator/test-helpers';

import { TSConfigPreprocessor } from '../../../src/sandbox/ts-config-preprocessor';
import { TSConfig, TSConfigPreprocessor } from '../../../src/sandbox/ts-config-preprocessor';

describe(TSConfigPreprocessor.name, () => {
let files: File[];
Expand Down Expand Up @@ -83,6 +83,50 @@ describe(TSConfigPreprocessor.name, () => {
]);
});

it('should rewrite "include" array items located outside of the sandbox', async () => {
// See https://github.com/stryker-mutator/stryker-js/issues/3281
files.push(
tsconfigFile('tsconfig.json', {
include: ['./**/*', '../../../node_modules/self-service-server/lib/main/shared/@types/**/*.d.ts'],
})
);
const output = await sut.preprocess(files);
assertions.expectTextFilesEqual(output, [
tsconfigFile('tsconfig.json', {
include: ['./**/*', '../../../../../node_modules/self-service-server/lib/main/shared/@types/**/*.d.ts'],
}),
]);
});

it('should rewrite "exclude" array items located outside of the sandbox', async () => {
files.push(
tsconfigFile('tsconfig.json', {
exclude: ['./**/*', '../foo.ts'],
})
);
const output = await sut.preprocess(files);
assertions.expectTextFilesEqual(output, [
tsconfigFile('tsconfig.json', {
exclude: ['./**/*', '../../../foo.ts'],
}),
]);
});

it('should rewrite "files" array items located outside of the sandbox', async () => {
// See https://github.com/stryker-mutator/stryker-js/issues/3281
files.push(
tsconfigFile('tsconfig.json', {
files: ['foo/bar.ts', '../global.d.ts'],
})
);
const output = await sut.preprocess(files);
assertions.expectTextFilesEqual(output, [
tsconfigFile('tsconfig.json', {
files: ['foo/bar.ts', '../../../global.d.ts'],
}),
]);
});

it('should be able to rewrite a monorepo style project', async () => {
// Arrange
files.push(
Expand Down Expand Up @@ -116,10 +160,4 @@ describe(TSConfigPreprocessor.name, () => {
function tsconfigFile(fileName: string, content: TSConfig) {
return new File(path.resolve(fileName), JSON.stringify(content, null, 2));
}

interface TSConfig {
extends?: string;
references?: Array<{ path: string }>;
[key: string]: unknown;
}
});

0 comments on commit 37ead22

Please sign in to comment.