1namespace ts { 2 describe("unittests:: customTransforms", () => { 3 function emitsCorrectly(name: string, sources: { file: string, text: string }[], customTransformers: CustomTransformers, options: CompilerOptions = {}) { 4 it(name, () => { 5 const roots = sources.map(source => createSourceFile(source.file, source.text, ScriptTarget.ES2015)); 6 const fileMap = arrayToMap(roots, file => file.fileName); 7 const outputs = new Map<string, string>(); 8 const host: CompilerHost = { 9 getSourceFile: (fileName) => fileMap.get(fileName), 10 getDefaultLibFileName: () => "lib.d.ts", 11 getCurrentDirectory: () => "", 12 getDirectories: () => [], 13 getCanonicalFileName: (fileName) => fileName, 14 useCaseSensitiveFileNames: () => true, 15 getNewLine: () => "\n", 16 fileExists: (fileName) => fileMap.has(fileName), 17 readFile: (fileName) => fileMap.has(fileName) ? fileMap.get(fileName)!.text : undefined, 18 writeFile: (fileName, text) => outputs.set(fileName, text), 19 }; 20 21 const program = createProgram(arrayFrom(fileMap.keys()), { newLine: NewLineKind.LineFeed, ...options }, host); 22 program.emit(/*targetSourceFile*/ undefined, host.writeFile, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ false, customTransformers); 23 let content = ""; 24 for (const [file, text] of arrayFrom(outputs.entries())) { 25 if (content) content += "\n\n"; 26 content += `// [${file}]\n`; 27 content += text; 28 } 29 Harness.Baseline.runBaseline(`customTransforms/${name}.js`, content); 30 }); 31 } 32 33 const sources = [{ 34 file: "source.ts", 35 text: ` 36 function f1() { } 37 class c() { } 38 enum e { } 39 // leading 40 function f2() { } // trailing 41 ` 42 }]; 43 44 const before: TransformerFactory<SourceFile> = context => { 45 return file => visitEachChild(file, visit, context); 46 function visit(node: Node): VisitResult<Node> { 47 switch (node.kind) { 48 case SyntaxKind.FunctionDeclaration: 49 return visitFunction(node as FunctionDeclaration); 50 default: 51 return visitEachChild(node, visit, context); 52 } 53 } 54 function visitFunction(node: FunctionDeclaration) { 55 addSyntheticLeadingComment(node, SyntaxKind.MultiLineCommentTrivia, "@before", /*hasTrailingNewLine*/ true); 56 return node; 57 } 58 }; 59 60 const after: TransformerFactory<SourceFile> = context => { 61 return file => visitEachChild(file, visit, context); 62 function visit(node: Node): VisitResult<Node> { 63 switch (node.kind) { 64 case SyntaxKind.VariableStatement: 65 return visitVariableStatement(node as VariableStatement); 66 default: 67 return visitEachChild(node, visit, context); 68 } 69 } 70 function visitVariableStatement(node: VariableStatement) { 71 addSyntheticLeadingComment(node, SyntaxKind.SingleLineCommentTrivia, "@after"); 72 return node; 73 } 74 }; 75 76 emitsCorrectly("before", sources, { before: [before] }); 77 emitsCorrectly("after", sources, { after: [after] }); 78 emitsCorrectly("both", sources, { before: [before], after: [after] }); 79 80 emitsCorrectly("before+decorators", [{ 81 file: "source.ts", 82 text: ` 83 declare const dec: any; 84 class B {} 85 @dec export class C { constructor(b: B) { } } 86 'change' 87 ` 88 }], {before: [ 89 context => node => visitNode(node, function visitor(node: Node): Node { 90 if (isStringLiteral(node) && node.text === "change") return factory.createStringLiteral("changed"); 91 return visitEachChild(node, visitor, context); 92 }) 93 ]}, { 94 target: ScriptTarget.ES5, 95 module: ModuleKind.ES2015, 96 emitDecoratorMetadata: true, 97 experimentalDecorators: true 98 }); 99 100 emitsCorrectly("sourceMapExternalSourceFiles", 101 [ 102 { 103 file: "source.ts", 104 // The text of length 'changed' is made to be on two lines so we know the line map change 105 text: `\`multi 106 line\` 107'change'` 108 }, 109 ], 110 { 111 before: [ 112 context => node => visitNode(node, function visitor(node: Node): Node { 113 if (isStringLiteral(node) && node.text === "change") { 114 const text = "'changed'"; 115 const lineMap = computeLineStarts(text); 116 setSourceMapRange(node, { 117 pos: 0, end: text.length, source: { 118 text, 119 fileName: "another.html", 120 lineMap, 121 getLineAndCharacterOfPosition: pos => computeLineAndCharacterOfPosition(lineMap, pos) 122 } 123 }); 124 return node; 125 } 126 return visitEachChild(node, visitor, context); 127 }) 128 ] 129 }, 130 { sourceMap: true } 131 ); 132 133 emitsCorrectly("skipTriviaExternalSourceFiles", 134 [ 135 { 136 file: "source.ts", 137 // The source file contains preceding trivia (e.g. whitespace) to try to confuse the `skipSourceTrivia` function. 138 text: " original;" 139 }, 140 ], 141 { 142 before: [ 143 context => { 144 const transformSourceFile: Transformer<SourceFile> = node => visitNode(node, function visitor(node: Node): Node { 145 if (isIdentifier(node) && node.text === "original") { 146 const newNode = factory.createIdentifier("changed"); 147 setSourceMapRange(newNode, { 148 pos: 0, 149 end: 7, 150 // Do not provide a custom skipTrivia function for `source`. 151 source: createSourceMapSource("another.html", "changed;") 152 }); 153 return newNode; 154 } 155 return visitEachChild(node, visitor, context); 156 }); 157 return { 158 transformSourceFile, 159 transformBundle: node => factory.createBundle(map(node.sourceFiles, transformSourceFile), node.prepends), 160 }; 161 } 162 ] 163 }, 164 { sourceMap: true, outFile: "source.js" } 165 ); 166 167 }); 168} 169