Skip to content
This repository was archived by the owner on May 22, 2025. It is now read-only.

Commit 296e760

Browse files
committed
Introduce transformer support for tsickle
API (see `transformer.ts`): The method `emitWithTsickle` allows to emit any program with tsickle. I.e. this is a replacement for using the `TsickleCompilerHost`. Implementation using existing `DecoratorClassVisitor` and `Annotator`: (see `transformer_sourcemap.ts`): Parses the produced sources back into a `ts.SourceFile` and establishes the links to the original nodes via the information given to the `SourceMapper` interface. Tests via goldens: The transformer version of Tsickle emits slightly different JavaScript (see the previous commit). Therefore, some goldens are slightly different. We use patch files to assert the differences to the regular goldens. Tests for SourceMaps: TypeScript automatically produces source maps for all changes that the transformer does. However, some applications (e.g. Angular) rely on the feature of tsickle to merge source maps that were already present in the input TypeScript code. Therefore, the transformer version still has to implement this merging.
1 parent 6ff41ce commit 296e760

18 files changed

+912
-54
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
},
2020
"devDependencies": {
2121
"@types/chai": "^3.4.32",
22+
"@types/diff": "^3.2.0",
2223
"@types/glob": "^5.0.29",
2324
"@types/google-closure-compiler": "0.0.18",
2425
"@types/minimatch": "^2.0.28",
@@ -30,6 +31,7 @@
3031
"@types/source-map-support": "^0.2.27",
3132
"chai": "^3.5.0",
3233
"clang-format": "^1.0.51",
34+
"diff": "^3.2.0",
3335
"glob": "^7.0.0",
3436
"google-closure-compiler": "^20161024.1.0",
3537
"gulp": "^3.8.11",

src/modules_manifest.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ export class ModulesManifest {
1515
/** Map of file name to arrays of imported googmodule module names */
1616
private referencedModules: FileMap<string[]> = {};
1717

18+
addManifest(other: ModulesManifest) {
19+
Object.assign(this.moduleToFileName, other.moduleToFileName);
20+
Object.assign(this.referencedModules, other.referencedModules);
21+
}
22+
1823
addModule(fileName: string, module: string): void {
1924
this.moduleToFileName[module] = fileName;
2025
this.referencedModules[fileName] = [];

src/source_map_utils.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {SourceMapConsumer, SourceMapGenerator} from 'source-map';
9+
import {RawSourceMap, SourceMapConsumer, SourceMapGenerator} from 'source-map';
1010
import * as ts from 'typescript';
1111

1212
/**
@@ -62,6 +62,17 @@ export function setInlineSourceMap(source: string, sourceMap: string): string {
6262
}
6363
}
6464

65+
export function parseSourceMap(text: string, fileName?: string, sourceName?: string): RawSourceMap {
66+
const rawSourceMap = JSON.parse(text) as RawSourceMap;
67+
if (sourceName) {
68+
rawSourceMap.sources = [sourceName];
69+
}
70+
if (fileName) {
71+
rawSourceMap.file = fileName;
72+
}
73+
return rawSourceMap;
74+
}
75+
6576
export function sourceMapConsumerToGenerator(sourceMapConsumer: SourceMapConsumer):
6677
SourceMapGenerator {
6778
return SourceMapGenerator.fromSourceMap(sourceMapConsumer);

src/transformer.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as path from 'path';
10+
import {RawSourceMap, SourceMapConsumer, SourceMapGenerator} from 'source-map';
11+
import * as ts from 'typescript';
12+
13+
import * as decorator from './decorator-annotator';
14+
import * as es5processor from './es5processor';
15+
import {ModulesManifest} from './modules_manifest';
16+
import {containsInlineSourceMap, extractInlineSourceMap, parseSourceMap, removeInlineSourceMap, setInlineSourceMap, SourceMapper, SourcePosition} from './source_map_utils';
17+
import {createTransformerFromSourceMap} from './transformer_sourcemap';
18+
import {createCustomTransformers} from './transformer_util';
19+
import * as tsickle from './tsickle';
20+
21+
export interface TransformerOptions extends es5processor.Es5ProcessorOptions, tsickle.Options {
22+
/**
23+
* Whether to downlevel decorators
24+
*/
25+
transformDecorators?: boolean;
26+
/**
27+
* Whether to convers types to closure
28+
*/
29+
transformTypesToClosure?: boolean;
30+
}
31+
32+
export interface TransformerHost extends es5processor.Es5ProcessorHost, tsickle.AnnotatorHost {
33+
/**
34+
* If true, tsickle and decorator downlevel processing will be skipped for
35+
* that file.
36+
*/
37+
shouldSkipTsickleProcessing(fileName: string): boolean;
38+
/**
39+
* Tsickle treats warnings as errors, if true, ignore warnings. This might be
40+
* useful for e.g. third party code.
41+
*/
42+
shouldIgnoreWarningsForPath(filePath: string): boolean;
43+
}
44+
45+
export function mergeEmitResults(emitResults: EmitResult[]): EmitResult {
46+
const diagnostics: ts.Diagnostic[] = [];
47+
let emitSkipped = true;
48+
const emittedFiles: string[] = [];
49+
const externs: {[fileName: string]: string} = {};
50+
const modulesManifest = new ModulesManifest();
51+
emitResults.forEach(er => {
52+
diagnostics.push(...er.diagnostics);
53+
emitSkipped = emitSkipped || er.emitSkipped;
54+
emittedFiles.push(...er.emittedFiles);
55+
Object.assign(externs, er.externs);
56+
modulesManifest.addManifest(er.modulesManifest);
57+
});
58+
return {diagnostics, emitSkipped, emittedFiles, externs, modulesManifest};
59+
}
60+
61+
export interface EmitResult extends ts.EmitResult {
62+
// The manifest of JS modules output by the compiler.
63+
modulesManifest: ModulesManifest;
64+
/** externs.js files produced by tsickle, if any. */
65+
externs: {[fileName: string]: string};
66+
}
67+
68+
export interface EmitTransformers {
69+
beforeTsickle?: Array<ts.TransformerFactory<ts.SourceFile>>;
70+
beforeTs?: Array<ts.TransformerFactory<ts.SourceFile>>;
71+
afterTs?: Array<ts.TransformerFactory<ts.SourceFile>>;
72+
}
73+
74+
export function emitWithTsickle(
75+
program: ts.Program, host: TransformerHost, options: TransformerOptions,
76+
tsHost: ts.CompilerHost, tsOptions: ts.CompilerOptions, targetSourceFile?: ts.SourceFile,
77+
writeFile?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken,
78+
emitOnlyDtsFiles?: boolean, customTransformers?: EmitTransformers): EmitResult {
79+
let tsickleDiagnostics: ts.Diagnostic[] = [];
80+
const typeChecker = program.getTypeChecker();
81+
const beforeTsTransformers: Array<ts.TransformerFactory<ts.SourceFile>> = [];
82+
// add tsickle transformers
83+
if (options.transformTypesToClosure) {
84+
// Note: tsickle.annotate can also lower decorators in the same run.
85+
beforeTsTransformers.push(createTransformerFromSourceMap((sourceFile, sourceMapper) => {
86+
const tisckleOptions: tsickle.Options = {...options, filterTypesForExport: true};
87+
const {output, diagnostics} = tsickle.annotate(
88+
typeChecker, sourceFile, host, tisckleOptions, tsHost, tsOptions, sourceMapper,
89+
tsickle.AnnotatorFeatures.Transformer);
90+
tsickleDiagnostics.push(...diagnostics);
91+
return output;
92+
}));
93+
} else if (options.transformDecorators) {
94+
beforeTsTransformers.push(createTransformerFromSourceMap((sourceFile, sourceMapper) => {
95+
const {output, diagnostics} =
96+
decorator.convertDecorators(typeChecker, sourceFile, sourceMapper);
97+
tsickleDiagnostics.push(...diagnostics);
98+
return output;
99+
}));
100+
}
101+
// // For debugging: transformer that just emits the same text.
102+
// beforeTsTransformers.push(createTransformer(host, typeChecker, (sourceFile, sourceMapper) => {
103+
// sourceMapper.addMapping(sourceFile, {position: 0, line: 0, column: 0}, {position: 0, line: 0,
104+
// column: 0}, sourceFile.text.length); return sourceFile.text;
105+
// }));
106+
// add user supplied transformers
107+
const afterTsTransformers: Array<ts.TransformerFactory<ts.SourceFile>> = [];
108+
if (customTransformers) {
109+
if (customTransformers.beforeTsickle) {
110+
beforeTsTransformers.unshift(...customTransformers.beforeTsickle);
111+
}
112+
113+
if (customTransformers.beforeTs) {
114+
beforeTsTransformers.push(...customTransformers.beforeTs);
115+
}
116+
if (customTransformers.afterTs) {
117+
afterTsTransformers.push(...customTransformers.afterTs);
118+
}
119+
}
120+
customTransformers = createCustomTransformers({
121+
before: beforeTsTransformers.map(tf => skipTransformForSourceFileIfNeeded(host, tf)),
122+
after: afterTsTransformers.map(tf => skipTransformForSourceFileIfNeeded(host, tf))
123+
});
124+
125+
const writeFileDelegate = writeFile || tsHost.writeFile.bind(tsHost);
126+
const modulesManifest = new ModulesManifest();
127+
const writeFileImpl =
128+
(fileName: string, content: string, writeByteOrderMark: boolean,
129+
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
130+
if (path.extname(fileName) !== '.map') {
131+
if (tsOptions.inlineSourceMap) {
132+
content = combineInlineSourceMaps(program, fileName, content);
133+
} else {
134+
content = removeInlineSourceMap(content);
135+
}
136+
content = es5processor.convertCommonJsToGoogModuleIfNeeded(
137+
host, options, modulesManifest, fileName, content);
138+
} else {
139+
content = combineSourceMaps(program, fileName, content);
140+
}
141+
writeFileDelegate(fileName, content, writeByteOrderMark, onError, sourceFiles);
142+
};
143+
144+
const {diagnostics: tsDiagnostics, emitSkipped, emittedFiles} = program.emit(
145+
targetSourceFile, writeFileImpl, cancellationToken, emitOnlyDtsFiles, customTransformers);
146+
147+
const externs: {[fileName: string]: string} = {};
148+
if (options.transformTypesToClosure) {
149+
const sourceFiles = targetSourceFile ? [targetSourceFile] : program.getSourceFiles();
150+
sourceFiles.forEach(sf => {
151+
if (tsickle.isDtsFileName(sf.fileName) && host.shouldSkipTsickleProcessing(sf.fileName)) {
152+
return;
153+
}
154+
const {output, diagnostics} = tsickle.writeExterns(typeChecker, sf, host, options);
155+
if (output) {
156+
externs[sf.fileName] = output;
157+
}
158+
if (diagnostics) {
159+
tsickleDiagnostics.push(...diagnostics);
160+
}
161+
});
162+
}
163+
// All diagnostics (including warnings) are treated as errors.
164+
// If we've decided to ignore them, just discard them.
165+
// Warnings include stuff like "don't use @type in your jsdoc"; tsickle
166+
// warns and then fixes up the code to be Closure-compatible anyway.
167+
tsickleDiagnostics = tsickleDiagnostics.filter(
168+
d => d.category === ts.DiagnosticCategory.Error ||
169+
!host.shouldIgnoreWarningsForPath(d.file.fileName));
170+
171+
return {
172+
modulesManifest,
173+
emitSkipped,
174+
emittedFiles: emittedFiles || [],
175+
diagnostics: [...tsDiagnostics, ...tsickleDiagnostics],
176+
externs
177+
};
178+
}
179+
180+
function skipTransformForSourceFileIfNeeded(
181+
host: TransformerHost,
182+
delegateFactory: ts.TransformerFactory<ts.SourceFile>): ts.TransformerFactory<ts.SourceFile> {
183+
return (context: ts.TransformationContext) => {
184+
const delegate = delegateFactory(context);
185+
return (sourceFile: ts.SourceFile) => {
186+
if (host.shouldSkipTsickleProcessing(sourceFile.fileName)) {
187+
return sourceFile;
188+
}
189+
return delegate(sourceFile);
190+
};
191+
};
192+
}
193+
194+
function combineInlineSourceMaps(
195+
program: ts.Program, filePath: string, compiledJsWithInlineSourceMap: string): string {
196+
if (tsickle.isDtsFileName(filePath)) {
197+
return compiledJsWithInlineSourceMap;
198+
}
199+
const sourceMapJson = extractInlineSourceMap(compiledJsWithInlineSourceMap);
200+
compiledJsWithInlineSourceMap = removeInlineSourceMap(compiledJsWithInlineSourceMap);
201+
const composedSourceMap = combineSourceMaps(program, filePath, sourceMapJson);
202+
return setInlineSourceMap(compiledJsWithInlineSourceMap, composedSourceMap);
203+
}
204+
205+
function combineSourceMaps(
206+
program: ts.Program, filePath: string, tscSourceMapText: string): string {
207+
const tscSourceMap = parseSourceMap(tscSourceMapText);
208+
let tscSourceMapGenerator: SourceMapGenerator|undefined;
209+
for (const sourceFileName of tscSourceMap.sources) {
210+
const sourceFile = program.getSourceFile(sourceFileName);
211+
if (!sourceFile || !containsInlineSourceMap(sourceFile.text)) {
212+
continue;
213+
}
214+
const preexistingSourceMapText = extractInlineSourceMap(sourceFile.text);
215+
if (!tscSourceMapGenerator) {
216+
tscSourceMapGenerator = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(tscSourceMap));
217+
}
218+
tscSourceMapGenerator.applySourceMap(
219+
new SourceMapConsumer(parseSourceMap(preexistingSourceMapText, sourceFileName)));
220+
}
221+
return tscSourceMapGenerator ? tscSourceMapGenerator.toString() : tscSourceMapText;
222+
}

0 commit comments

Comments
 (0)