From 0c1a283bf9f933beef4bdf31ffd0facbccbd38dc Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 7 May 2019 15:41:39 -0700 Subject: [PATCH] Add opt-in behavior for custom transforms to support bundles --- src/compiler/emitter.ts | 8 +-- src/compiler/factory.ts | 2 +- src/compiler/program.ts | 7 +-- src/compiler/transformer.ts | 50 +++++++++++++++++-- src/compiler/types.ts | 19 +++++-- src/testRunner/unittests/customTransforms.ts | 34 +++++++------ .../reference/api/tsserverlibrary.d.ts | 13 +++-- tests/baselines/reference/api/typescript.d.ts | 13 +++-- 8 files changed, 108 insertions(+), 38 deletions(-) diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 8f91e3d25c163..d335ceb1b6037 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -222,7 +222,7 @@ namespace ts { /*@internal*/ // targetSourceFile is when users only want one file in entire project to be emitted. This is used in compileOnSave feature - export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile | undefined, emitOnlyDtsFiles?: boolean, transformers?: TransformerFactory[], declarationTransformers?: TransformerFactory[], onlyBuildInfo?: boolean): EmitResult { + export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile | undefined, { scriptTransformers, declarationTransformers }: EmitTransformers, emitOnlyDtsFiles?: boolean, onlyBuildInfo?: boolean): EmitResult { const compilerOptions = host.getCompilerOptions(); const sourceMapDataList: SourceMapEmitResult[] | undefined = (compilerOptions.sourceMap || compilerOptions.inlineSourceMap || getAreDeclarationMapsEnabled(compilerOptions)) ? [] : undefined; const emittedFilesList: string[] | undefined = compilerOptions.listEmittedFiles ? [] : undefined; @@ -303,7 +303,7 @@ namespace ts { return; } // Transform the source files - const transform = transformNodes(resolver, host, compilerOptions, [sourceFileOrBundle], transformers!, /*allowDtsFiles*/ false); + const transform = transformNodes(resolver, host, compilerOptions, [sourceFileOrBundle], scriptTransformers, /*allowDtsFiles*/ false); const printerOptions: PrinterOptions = { removeComments: compilerOptions.removeComments, @@ -349,7 +349,7 @@ namespace ts { // Do that here when emitting only dts files nonJsFiles.forEach(collectLinkedAliases); } - const declarationTransform = transformNodes(resolver, host, compilerOptions, inputListOrBundle, concatenate([transformDeclarations], declarationTransformers), /*allowDtsFiles*/ false); + const declarationTransform = transformNodes(resolver, host, compilerOptions, inputListOrBundle, declarationTransformers, /*allowDtsFiles*/ false); if (length(declarationTransform.diagnostics)) { for (const diagnostic of declarationTransform.diagnostics!) { emitterDiagnostics.add(diagnostic); @@ -723,7 +723,7 @@ namespace ts { useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), getProgramBuildInfo: returnUndefined }; - emitFiles(notImplementedResolver, emitHost, /*targetSourceFile*/ undefined, /*emitOnlyDtsFiles*/ false, getTransformers(config.options)); + emitFiles(notImplementedResolver, emitHost, /*targetSourceFile*/ undefined, getTransformers(config.options), /*emitOnlyDtsFiles*/ false); return outputFiles; } diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 002e0e8f07f66..517ebacb89dfb 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -2949,7 +2949,7 @@ namespace ts { return node; } - export function updateBundle(node: Bundle, sourceFiles: ReadonlyArray, prepends: ReadonlyArray = emptyArray) { + export function updateBundle(node: Bundle, sourceFiles: ReadonlyArray, prepends: ReadonlyArray = emptyArray) { if (node.sourceFiles !== sourceFiles || node.prepends !== prepends) { return createBundle(sourceFiles, prepends); } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 6f331bb49ca1b..2fda6ea49a55f 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1453,9 +1453,8 @@ namespace ts { notImplementedResolver, getEmitHost(writeFileCallback), /*targetSourceFile*/ undefined, + /*transformers*/ noTransformers, /*emitOnlyDtsFiles*/ false, - /*transformers*/ undefined, - /*declaraitonTransformers*/ undefined, /*onlyBuildInfo*/ true ); @@ -1574,14 +1573,12 @@ namespace ts { performance.mark("beforeEmit"); - const transformers = emitOnlyDtsFiles ? [] : getTransformers(options, customTransformers); const emitResult = emitFiles( emitResolver, getEmitHost(writeFileCallback), sourceFile, + getTransformers(options, customTransformers, emitOnlyDtsFiles), emitOnlyDtsFiles, - transformers, - customTransformers && customTransformers.afterDeclarations ); performance.mark("afterEmit"); diff --git a/src/compiler/transformer.ts b/src/compiler/transformer.ts index ba47d5b8106ff..1c1df9bb0c210 100644 --- a/src/compiler/transformer.ts +++ b/src/compiler/transformer.ts @@ -24,13 +24,24 @@ namespace ts { EmitNotifications = 1 << 1, } - export function getTransformers(compilerOptions: CompilerOptions, customTransformers?: CustomTransformers) { + export const noTransformers: EmitTransformers = { scriptTransformers: emptyArray, declarationTransformers: emptyArray }; + + export function getTransformers(compilerOptions: CompilerOptions, customTransformers?: CustomTransformers, emitOnlyDtsFiles?: boolean): EmitTransformers { + return { + scriptTransformers: getScriptTransformers(compilerOptions, customTransformers, emitOnlyDtsFiles), + declarationTransformers: getDeclarationTransformers(customTransformers), + }; + } + + function getScriptTransformers(compilerOptions: CompilerOptions, customTransformers?: CustomTransformers, emitOnlyDtsFiles?: boolean) { + if (emitOnlyDtsFiles) return emptyArray; + const jsx = compilerOptions.jsx; const languageVersion = getEmitScriptTarget(compilerOptions); const moduleKind = getEmitModuleKind(compilerOptions); const transformers: TransformerFactory[] = []; - addRange(transformers, customTransformers && customTransformers.before); + addRange(transformers, customTransformers && map(customTransformers.before, wrapScriptTransformerFactory)); transformers.push(transformTypeScript); @@ -71,11 +82,44 @@ namespace ts { transformers.push(transformES5); } - addRange(transformers, customTransformers && customTransformers.after); + addRange(transformers, customTransformers && map(customTransformers.after, wrapScriptTransformerFactory)); + return transformers; + } + function getDeclarationTransformers(customTransformers?: CustomTransformers) { + const transformers: TransformerFactory[] = []; + transformers.push(transformDeclarations); + addRange(transformers, customTransformers && map(customTransformers.afterDeclarations, wrapDeclarationTransformerFactory)); return transformers; } + /** + * Wrap a custom script or declaration transformer object in a `Transformer` callback with fallback support for transforming bundles. + */ + function wrapCustomTransformer(transformer: CustomTransformer): Transformer { + return node => isBundle(node) ? transformer.transformBundle(node) : transformer.transformSourceFile(node); + } + + /** + * Wrap a transformer factory that may return a custom script or declaration transformer object. + */ + function wrapCustomTransformerFactory(transformer: TransformerFactory | CustomTransformerFactory, handleDefault: (node: Transformer) => Transformer): TransformerFactory { + return context => { + const customTransformer = transformer(context); + return typeof customTransformer === "function" + ? handleDefault(customTransformer) + : wrapCustomTransformer(customTransformer); + }; + } + + function wrapScriptTransformerFactory(transformer: TransformerFactory | CustomTransformerFactory): TransformerFactory { + return wrapCustomTransformerFactory(transformer, chainBundle); + } + + function wrapDeclarationTransformerFactory(transformer: TransformerFactory | CustomTransformerFactory): TransformerFactory { + return wrapCustomTransformerFactory(transformer, identity); + } + export function noEmitSubstitution(_hint: EmitHint, node: Node) { return node; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 205ad19b95e5f..0e3ab5bec24a1 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3011,13 +3011,26 @@ namespace ts { Completely = 1 << 1, } + export type CustomTransformerFactory = (context: TransformationContext) => CustomTransformer; + + export interface CustomTransformer { + transformSourceFile(node: SourceFile): SourceFile; + transformBundle(node: Bundle): Bundle; + } + export interface CustomTransformers { /** Custom transformers to evaluate before built-in .js transformations. */ - before?: TransformerFactory[]; + before?: (TransformerFactory | CustomTransformerFactory)[]; /** Custom transformers to evaluate after built-in .js transformations. */ - after?: TransformerFactory[]; + after?: (TransformerFactory | CustomTransformerFactory)[]; /** Custom transformers to evaluate after built-in .d.ts transformations. */ - afterDeclarations?: TransformerFactory[]; + afterDeclarations?: (TransformerFactory | CustomTransformerFactory)[]; + } + + /*@internal*/ + export interface EmitTransformers { + scriptTransformers: readonly TransformerFactory[]; + declarationTransformers: readonly TransformerFactory[]; } export interface SourceMapSpan { diff --git a/src/testRunner/unittests/customTransforms.ts b/src/testRunner/unittests/customTransforms.ts index 0ac1434134231..2a2309d4b8786 100644 --- a/src/testRunner/unittests/customTransforms.ts +++ b/src/testRunner/unittests/customTransforms.ts @@ -140,22 +140,28 @@ namespace ts { ], { before: [ - context => node => visitNode(node, function visitor(node: Node): Node { - if (isIdentifier(node) && node.text === "original") { - const newNode = createIdentifier("changed"); - setSourceMapRange(newNode, { - pos: 0, - end: 7, - // Do not provide a custom skipTrivia function for `source`. - source: createSourceMapSource("another.html", "changed;") - }); - return newNode; - } - return visitEachChild(node, visitor, context); - }) + context => { + const transformSourceFile: Transformer = node => visitNode(node, function visitor(node: Node): Node { + if (isIdentifier(node) && node.text === "original") { + const newNode = createIdentifier("changed"); + setSourceMapRange(newNode, { + pos: 0, + end: 7, + // Do not provide a custom skipTrivia function for `source`. + source: createSourceMapSource("another.html", "changed;") + }); + return newNode; + } + return visitEachChild(node, visitor, context); + }); + return { + transformSourceFile, + transformBundle: node => createBundle(map(node.sourceFiles, transformSourceFile), node.prepends), + }; + } ] }, - { sourceMap: true } + { sourceMap: true, outFile: "source.js" } ); }); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 08e25fed188c8..ae473420de5ba 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1879,13 +1879,18 @@ declare namespace ts { sourceFile: SourceFile; references?: ReadonlyArray; } + type CustomTransformerFactory = (context: TransformationContext) => CustomTransformer; + interface CustomTransformer { + transformSourceFile(node: SourceFile): SourceFile; + transformBundle(node: Bundle): Bundle; + } interface CustomTransformers { /** Custom transformers to evaluate before built-in .js transformations. */ - before?: TransformerFactory[]; + before?: (TransformerFactory | CustomTransformerFactory)[]; /** Custom transformers to evaluate after built-in .js transformations. */ - after?: TransformerFactory[]; + after?: (TransformerFactory | CustomTransformerFactory)[]; /** Custom transformers to evaluate after built-in .d.ts transformations. */ - afterDeclarations?: TransformerFactory[]; + afterDeclarations?: (TransformerFactory | CustomTransformerFactory)[]; } interface SourceMapSpan { /** Line number in the .js file. */ @@ -4071,7 +4076,7 @@ declare namespace ts { function createInputFiles(javascriptText: string, declarationText: string): InputFiles; function createInputFiles(readFileText: (path: string) => string | undefined, javascriptPath: string, javascriptMapPath: string | undefined, declarationPath: string, declarationMapPath: string | undefined, buildInfoPath: string | undefined): InputFiles; function createInputFiles(javascriptText: string, declarationText: string, javascriptMapPath: string | undefined, javascriptMapText: string | undefined, declarationMapPath: string | undefined, declarationMapText: string | undefined): InputFiles; - function updateBundle(node: Bundle, sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; + function updateBundle(node: Bundle, sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray): CallExpression; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray, param: ParameterDeclaration, paramValue: Expression): CallExpression; function createImmediatelyInvokedArrowFunction(statements: ReadonlyArray): CallExpression; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index a7c3fc07971bb..038a953d08188 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1879,13 +1879,18 @@ declare namespace ts { sourceFile: SourceFile; references?: ReadonlyArray; } + type CustomTransformerFactory = (context: TransformationContext) => CustomTransformer; + interface CustomTransformer { + transformSourceFile(node: SourceFile): SourceFile; + transformBundle(node: Bundle): Bundle; + } interface CustomTransformers { /** Custom transformers to evaluate before built-in .js transformations. */ - before?: TransformerFactory[]; + before?: (TransformerFactory | CustomTransformerFactory)[]; /** Custom transformers to evaluate after built-in .js transformations. */ - after?: TransformerFactory[]; + after?: (TransformerFactory | CustomTransformerFactory)[]; /** Custom transformers to evaluate after built-in .d.ts transformations. */ - afterDeclarations?: TransformerFactory[]; + afterDeclarations?: (TransformerFactory | CustomTransformerFactory)[]; } interface SourceMapSpan { /** Line number in the .js file. */ @@ -4071,7 +4076,7 @@ declare namespace ts { function createInputFiles(javascriptText: string, declarationText: string): InputFiles; function createInputFiles(readFileText: (path: string) => string | undefined, javascriptPath: string, javascriptMapPath: string | undefined, declarationPath: string, declarationMapPath: string | undefined, buildInfoPath: string | undefined): InputFiles; function createInputFiles(javascriptText: string, declarationText: string, javascriptMapPath: string | undefined, javascriptMapText: string | undefined, declarationMapPath: string | undefined, declarationMapText: string | undefined): InputFiles; - function updateBundle(node: Bundle, sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; + function updateBundle(node: Bundle, sourceFiles: ReadonlyArray, prepends?: ReadonlyArray): Bundle; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray): CallExpression; function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray, param: ParameterDeclaration, paramValue: Expression): CallExpression; function createImmediatelyInvokedArrowFunction(statements: ReadonlyArray): CallExpression;