diff --git a/projects/patch/src/ts/create-program.ts b/projects/patch/src/ts/create-program.ts index 0cf9b91..b7e99e2 100644 --- a/projects/patch/src/ts/create-program.ts +++ b/projects/patch/src/ts/create-program.ts @@ -74,7 +74,7 @@ namespace tsp { rootNames = createOpts.rootNames; options = createOpts.options; host = createOpts.host; - oldProgram = createOpts.oldProgram; + oldProgram = (createOpts.oldProgram); configFileParsingDiagnostics = createOpts.configFileParsingDiagnostics; } else { options = options!; @@ -98,9 +98,12 @@ namespace tsp { } /* Invoke TS createProgram */ - let program: tsShim.Program & { originalEmit?: tsShim.Program['emit'] } = createOpts ? - tsShim.originalCreateProgram(createOpts) : - tsShim.originalCreateProgram(rootNames, options, host, oldProgram, configFileParsingDiagnostics); + let program: tsShim.Program & { + originalEmit?: tsShim.Program['emit']; + originalGetSemanticDiagnostics?: tsShim.Program['getSemanticDiagnostics']; + } = createOpts ? + (tsShim.originalCreateProgram(createOpts)): + (tsShim.originalCreateProgram(rootNames, options, host, oldProgram, configFileParsingDiagnostics)); /* Prevent recursion in Program transformers */ const programTransformers = pluginCreator.createProgramTransformers(); @@ -151,6 +154,20 @@ namespace tsp { return result; } + /* Hook getSemanticDiagnostics method for ts-loader */ + if (!program.originalGetSemanticDiagnostics) { + program.originalGetSemanticDiagnostics = program.getSemanticDiagnostics; + program.getSemanticDiagnostics = newGetSemanticDiagnostics; + } + + function newGetSemanticDiagnostics(sourceFile: tsShim.SourceFile, cancellationToken?: tsShim.CancellationToken) { + const originalDiagnostics = program.originalGetSemanticDiagnostics!(sourceFile, cancellationToken); + const addedDiagnostics = tsp.diagnosticMap.get(program) || []; + const diagnosticsByFile = addedDiagnostics.filter(it => it.file === sourceFile); + + return originalDiagnostics.concat(diagnosticsByFile); + } + return program; } } diff --git a/test/assets/projects/transformer-extras/package.json b/test/assets/projects/transformer-extras/package.json new file mode 100644 index 0000000..6572670 --- /dev/null +++ b/test/assets/projects/transformer-extras/package.json @@ -0,0 +1,7 @@ +{ + "name": "transformer-extras-test", + "main": "src/index.ts", + "dependencies": { + "ts-node" : "^10.9.1" + } +} diff --git a/test/assets/projects/transformer-extras/src/compiler.ts b/test/assets/projects/transformer-extras/src/compiler.ts new file mode 100644 index 0000000..ea5f4af --- /dev/null +++ b/test/assets/projects/transformer-extras/src/compiler.ts @@ -0,0 +1,34 @@ +const path = require('path'); + +(() => { + const tsInstance = require('ts-patch/compiler'); + + const configPath = path.join(process.cwd(), `tsconfig.json`); + const configText = tsInstance.sys.readFile(configPath); + const configParseResult = tsInstance.parseConfigFileTextToJson(configPath, configText); + const config = configParseResult.config; + + config.compilerOptions.noEmit = false; + config.compilerOptions.skipLibCheck = true; + config.compilerOptions.outDir = 'dist'; + + const sourceFilePath = path.join(__dirname, 'index.ts'); + const program = tsInstance.createProgram({ + rootNames: [ sourceFilePath ], + options: config.compilerOptions, + }); + + const emitResult = program.emit(); + const sourceFile = program.getSourceFile(sourceFilePath); + const semanticDiagnostics = program.getSemanticDiagnostics(sourceFile); + + process.stdout.write(`emitResultDiagnostics:${diagnosticsToJsonString(emitResult.diagnostics)}\n`); + process.stdout.write(`semanticDiagnostics:${diagnosticsToJsonString(semanticDiagnostics)}\n`); +})(); + +function diagnosticsToJsonString(diagnostics): string { + return JSON.stringify(diagnostics.map(diagnostic => { + const { file, start, length, messageText, category, code } = diagnostic; + return { file: file.fileName, start, length, messageText, category, code }; + })); +} diff --git a/test/assets/projects/transformer-extras/src/index.ts b/test/assets/projects/transformer-extras/src/index.ts new file mode 100644 index 0000000..c139e58 --- /dev/null +++ b/test/assets/projects/transformer-extras/src/index.ts @@ -0,0 +1 @@ +export const a: string = 42; diff --git a/test/assets/projects/transformer-extras/src/transformer.ts b/test/assets/projects/transformer-extras/src/transformer.ts new file mode 100644 index 0000000..543a0f7 --- /dev/null +++ b/test/assets/projects/transformer-extras/src/transformer.ts @@ -0,0 +1,44 @@ +// @ts-nocheck +import type * as ts from 'typescript' +import type { TransformerExtras } from 'ts-patch' + +export default function(program: ts.Program, pluginOptions: unknown, transformerExtras?: TransformerExtras) { + return (ctx: ts.TransformationContext) => { + return (sourceFile: ts.SourceFile) => { + transformerExtras?.addDiagnostic({ + file: sourceFile, + code: 42, + messageText: 'It\'s a warning message!', + category: 0, + start: 0, + length: 1, + }); + transformerExtras?.addDiagnostic({ + file: sourceFile, + code: 42, + messageText: 'It\'s an error message!', + category: 1, + start: 1, + length: 2, + }); + transformerExtras?.addDiagnostic({ + file: sourceFile, + code: 42, + messageText: 'It\'s a suggestion message!', + category: 2, + start: 2, + length: 3, + }); + transformerExtras?.addDiagnostic({ + file: sourceFile, + code: 42, + messageText: 'It\'s a message!', + category: 3, + start: 3, + length: 4, + }); + + return sourceFile; + }; + }; +} diff --git a/test/assets/projects/transformer-extras/tsconfig.json b/test/assets/projects/transformer-extras/tsconfig.json new file mode 100644 index 0000000..3bcadc0 --- /dev/null +++ b/test/assets/projects/transformer-extras/tsconfig.json @@ -0,0 +1,15 @@ +{ + "include": [ + "src" + ], + "compilerOptions": { + "outDir": "dist", + "module": "commonjs", + "target": "esnext", + "plugins" : [ + { + "transform": "./src/transformer.ts" + } + ] + } +} diff --git a/test/tests/transformer-extras.test.ts b/test/tests/transformer-extras.test.ts new file mode 100644 index 0000000..3efb45b --- /dev/null +++ b/test/tests/transformer-extras.test.ts @@ -0,0 +1,89 @@ +import { prepareTestProject } from '../src/project'; +import { execSync } from 'child_process'; +import path from 'path'; + +/* ****************************************************************************************************************** * + * Tests + * ****************************************************************************************************************** */ + +describe('Transformer Extras addDiagnostics', () => { + let projectPath: string; + let output: string[]; + + beforeAll(() => { + const prepRes = prepareTestProject({ projectName: 'transformer-extras' }); + projectPath = prepRes.tmpProjectPath; + + let commandOutput: string; + try { + commandOutput = execSync('ts-node src/compiler.ts', { + cwd: projectPath, + env: { + ...process.env, + PATH: `${projectPath}/node_modules/.bin${path.delimiter}${process.env.PATH}` + } + }).toString(); + } + catch (e) { + const err = new Error(e.stdout.toString() + '\n' + e.stderr.toString()); + console.error(err); + throw e; + } + + output = commandOutput.trim().split('\n'); + }); + + test('Provide emit result diagnostics and semantic diagnostics and merge it with original diagnostics', () => { + const [ emitResultDiagnosticsText, semanticDiagnosticsText ] = output; + + const emitResultDiagnostics = JSON.parse(emitResultDiagnosticsText.split('emitResultDiagnostics:')[1]); + const semanticDiagnostics = JSON.parse(semanticDiagnosticsText.split('semanticDiagnostics:')[1]); + + const filePath = path.join(projectPath, 'src/index.ts'); + const expectedEmitResultDiagnostics = [ + { + file: expect.stringContaining(filePath), + code: 42, + start: 0, + length: 1, + messageText: 'It\'s a warning message!', + category: 0 + }, { + file: expect.stringContaining(filePath), + code: 42, + start: 1, + length: 2, + messageText: 'It\'s an error message!', + category: 1 + }, { + file: expect.stringContaining(filePath), + code: 42, + start: 2, + length: 3, + messageText: 'It\'s a suggestion message!', + category: 2 + }, { + file: expect.stringContaining(filePath), + code: 42, + start: 3, + length: 4, + messageText: 'It\'s a message!', + category: 3 + } + ]; + const expectedSemanticDiagnostics = [ + { + file: expect.stringContaining(filePath), + code: 2322, + category: 1, + length: 1, + messageText: 'Type \'number\' is not assignable to type \'string\'.', + start: 13, + }, + ...expectedEmitResultDiagnostics, + ] + + expect(emitResultDiagnostics).toEqual(expectedEmitResultDiagnostics); + expect(semanticDiagnostics).toEqual(expectedSemanticDiagnostics); + }); +});