-
Notifications
You must be signed in to change notification settings - Fork 247
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(tsconfig): rewrite tsconfig references (#2292)
Add a new `tsconfigFile` option (reused from the old `@stryker-mutator/typescript` package). Use this new option in the `@stryker-mutator/typescript-checker` instead of a specific typescript-checker specific one From stryker core, use this setting to rewrite `tsconfig.json` file's `references` and `extends` when they refer config files that fall outside of the sandbox. For example: ```json { "extends": "../../tsconfig.settings.json", "references": { "path": "../model" } } ``` becomes: ```json { "extends": "../../../../tsconfig.settings.json", "references": { "path": "../../../model" } } ``` The implementation relies on typescript being available, but will only import it when it found a tsconfig.json file. Once a tsconfig file is found, referenced files within the sandbox also get rewritten. Closes #2276
- Loading branch information
Showing
92 changed files
with
295 additions
and
2,734 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './sandbox'; | ||
export * from './sandbox-tsconfig-rewriter'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import path = require('path'); | ||
|
||
import { StrykerOptions, File } from '@stryker-mutator/api/core'; | ||
import { tokens, commonTokens } from '@stryker-mutator/api/plugin'; | ||
import { Logger } from '@stryker-mutator/api/logging'; | ||
|
||
/** | ||
* A helper class that rewrites `references` and `extends` file paths if they end up falling outside of the sandbox. | ||
* @example | ||
* { | ||
* "extends": "../../tsconfig.settings.json", | ||
* "references": { | ||
* "path": "../model" | ||
* } | ||
* } | ||
* becomes: | ||
* { | ||
* "extends": "../../../../tsconfig.settings.json", | ||
* "references": { | ||
* "path": "../../../model" | ||
* } | ||
* } | ||
*/ | ||
export class SandboxTSConfigRewriter { | ||
private readonly touched: string[] = []; | ||
private readonly fs = new Map<string, File>(); | ||
public static readonly inject = tokens(commonTokens.logger, commonTokens.options); | ||
constructor(private readonly log: Logger, private readonly options: StrykerOptions) {} | ||
|
||
public async rewrite(input: File[]): Promise<readonly File[]> { | ||
const tsconfigFile = path.resolve(this.options.tsconfigFile); | ||
if (input.find((file) => file.name === tsconfigFile)) { | ||
this.fs.clear(); | ||
input.forEach((file) => { | ||
this.fs.set(file.name, file); | ||
}); | ||
await this.rewriteTSConfigFile(tsconfigFile); | ||
return [...this.fs.values()]; | ||
} else { | ||
return input; | ||
} | ||
} | ||
|
||
private async rewriteTSConfigFile(tsconfigFileName: string): Promise<void> { | ||
if (!this.touched.includes(tsconfigFileName)) { | ||
this.touched.push(tsconfigFileName); | ||
const tsconfigFile = this.fs.get(tsconfigFileName); | ||
if (tsconfigFile) { | ||
this.log.debug('Rewriting file %s', tsconfigFile); | ||
const ts = await import('typescript'); | ||
const { config } = ts.parseConfigFileTextToJson(tsconfigFile.name, tsconfigFile.textContent); | ||
if (config) { | ||
await this.rewriteExtends(config, tsconfigFileName); | ||
await this.rewriteProjectReferences(config, tsconfigFileName); | ||
this.fs.set(tsconfigFileName, new File(tsconfigFileName, JSON.stringify(config, null, 2))); | ||
} | ||
} | ||
} | ||
} | ||
|
||
private async rewriteExtends(config: any, tsconfigFileName: string): Promise<boolean> { | ||
if (typeof config.extends === 'string') { | ||
const extendsFileName = path.resolve(path.dirname(tsconfigFileName), config.extends); | ||
const relativeToSandbox = path.relative(process.cwd(), extendsFileName); | ||
if (relativeToSandbox.startsWith('..')) { | ||
config.extends = this.join('..', '..', config.extends); | ||
return true; | ||
} else { | ||
await this.rewriteTSConfigFile(extendsFileName); | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
private async rewriteProjectReferences(config: { references?: Array<{ path: string }> }, 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); | ||
} else { | ||
await this.rewriteTSConfigFile(referencedProjectFileName); | ||
} | ||
} | ||
} | ||
} | ||
|
||
private join(...pathSegments: string[]) { | ||
return pathSegments.map((segment) => segment.replace(/\\/g, '/')).join('/'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
116 changes: 116 additions & 0 deletions
116
packages/core/test/unit/sandbox/sandbox-tsconfig-rewriter.it.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import path = require('path'); | ||
|
||
import { expect } from 'chai'; | ||
import { File } from '@stryker-mutator/api/core'; | ||
import { testInjector } from '@stryker-mutator/test-helpers'; | ||
|
||
import { SandboxTSConfigRewriter } from '../../../src/sandbox'; | ||
|
||
describe(SandboxTSConfigRewriter.name, () => { | ||
let files: File[]; | ||
let sut: SandboxTSConfigRewriter; | ||
|
||
beforeEach(() => { | ||
files = []; | ||
sut = testInjector.injector.injectClass(SandboxTSConfigRewriter); | ||
}); | ||
|
||
it('should not do anything if the tsconfig file does not exist', async () => { | ||
files.push(new File('foo.js', 'console.log("foo");')); | ||
const output = await sut.rewrite(files); | ||
expect(output).eq(files); | ||
}); | ||
|
||
it('should ignore missing "extends"', async () => { | ||
files.push(tsconfigFile('tsconfig.json', { references: [{ path: './tsconfig.src.json' }] })); | ||
const output = await sut.rewrite(files); | ||
expect(output).deep.eq(files); | ||
}); | ||
|
||
it('should ignore missing "references"', async () => { | ||
files.push(tsconfigFile('tsconfig.json', { extends: './tsconfig.settings.json' })); | ||
const output = await sut.rewrite(files); | ||
expect(output).deep.eq(files); | ||
}); | ||
|
||
it('should rewrite "extends" if it falls outside of sandbox', async () => { | ||
files.push(tsconfigFile('tsconfig.json', { extends: '../tsconfig.settings.json' })); | ||
const output = await sut.rewrite(files); | ||
expect(output).deep.eq([tsconfigFile('tsconfig.json', { extends: '../../../tsconfig.settings.json' })]); | ||
}); | ||
|
||
it('should support comments and other settings', async () => { | ||
files.push( | ||
new File( | ||
path.resolve(process.cwd(), 'tsconfig.json'), | ||
`{ | ||
"extends": "../tsconfig.settings.json", | ||
"compilerOptions": { | ||
// Here are the options | ||
"target": "es5", // and a trailing comma | ||
} | ||
}` | ||
) | ||
); | ||
const output = await sut.rewrite(files); | ||
expect(output).deep.eq([tsconfigFile('tsconfig.json', { extends: '../../../tsconfig.settings.json', compilerOptions: { target: 'es5' } })]); | ||
}); | ||
|
||
it('should rewrite "references" if it falls outside of sandbox', async () => { | ||
files.push(tsconfigFile('tsconfig.json', { references: [{ path: '../model' }] })); | ||
const output = await sut.rewrite(files); | ||
expect(output).deep.eq([tsconfigFile('tsconfig.json', { references: [{ path: '../../../model/tsconfig.json' }] })]); | ||
}); | ||
|
||
it('should rewrite referenced tsconfig files that are also located in the sandbox', async () => { | ||
files.push(tsconfigFile('tsconfig.json', { extends: './tsconfig.settings.json', references: [{ path: './src' }] })); | ||
files.push(tsconfigFile('tsconfig.settings.json', { extends: '../../tsconfig.root-settings.json' })); | ||
files.push(tsconfigFile('src/tsconfig.json', { references: [{ path: '../../model' }] })); | ||
const output = await sut.rewrite(files); | ||
expect(output).deep.eq([ | ||
tsconfigFile('tsconfig.json', { extends: './tsconfig.settings.json', references: [{ path: './src' }] }), | ||
tsconfigFile('tsconfig.settings.json', { extends: '../../../../tsconfig.root-settings.json' }), | ||
tsconfigFile('src/tsconfig.json', { references: [{ path: '../../../../model/tsconfig.json' }] }), | ||
]); | ||
}); | ||
|
||
it('should be able to rewrite a monorepo style project', async () => { | ||
// Arrange | ||
files.push( | ||
tsconfigFile('tsconfig.root.json', { | ||
extends: '../../tsconfig.settings.json', | ||
references: [{ path: 'src' }, { path: 'test/tsconfig.test.json' }], | ||
}) | ||
); | ||
files.push(tsconfigFile('src/tsconfig.json', { extends: '../../../tsconfig.settings.json', references: [{ path: '../../model' }] })); | ||
files.push(tsconfigFile('test/tsconfig.test.json', { extends: '../tsconfig.root.json', references: [{ path: '../src' }] })); | ||
testInjector.options.tsconfigFile = 'tsconfig.root.json'; | ||
|
||
// Act | ||
const actual = await sut.rewrite(files); | ||
|
||
// Assert | ||
const expected = [ | ||
tsconfigFile('tsconfig.root.json', { | ||
extends: '../../../../tsconfig.settings.json', | ||
references: [{ path: 'src' }, { path: 'test/tsconfig.test.json' }], | ||
}), | ||
tsconfigFile('src/tsconfig.json', { | ||
extends: '../../../../../tsconfig.settings.json', | ||
references: [{ path: '../../../../model/tsconfig.json' }], | ||
}), | ||
tsconfigFile('test/tsconfig.test.json', { extends: '../tsconfig.root.json', references: [{ path: '../src' }] }), | ||
]; | ||
expect(actual).deep.eq(expected); | ||
}); | ||
|
||
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; | ||
} | ||
}); |
8 changes: 8 additions & 0 deletions
8
packages/core/testResources/sandbox-tsconfig-rewriter/mono-repo/src/tsconfig.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"extends": "../../../tsconfig.settings.json", | ||
"references": [ | ||
{ | ||
"path": "../../model" | ||
} | ||
] | ||
} |
8 changes: 8 additions & 0 deletions
8
packages/core/testResources/sandbox-tsconfig-rewriter/mono-repo/test/tsconfig.test.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"extends": "../tsconfig.root.json", | ||
"references": [ | ||
{ | ||
"path": "../src" | ||
} | ||
] | ||
} |
12 changes: 12 additions & 0 deletions
12
packages/core/testResources/sandbox-tsconfig-rewriter/mono-repo/tsconfig.root.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"extends": "../../tsconfig.settings.json", | ||
"files": [], | ||
"references": [ | ||
{ | ||
"path": "src" | ||
}, | ||
{ | ||
"path": "test/tsconfig.test.json" | ||
} | ||
] | ||
} |
Oops, something went wrong.