Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add opt-in behavior for custom transforms to support bundles #31301

Merged
merged 1 commit into from May 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/compiler/emitter.ts
Expand Up @@ -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<Bundle | SourceFile>[], declarationTransformers?: TransformerFactory<Bundle | SourceFile>[], 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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/compiler/factory.ts
Expand Up @@ -2949,7 +2949,7 @@ namespace ts {
return node;
}

export function updateBundle(node: Bundle, sourceFiles: ReadonlyArray<SourceFile>, prepends: ReadonlyArray<UnparsedSource> = emptyArray) {
export function updateBundle(node: Bundle, sourceFiles: ReadonlyArray<SourceFile>, prepends: ReadonlyArray<UnparsedSource | InputFiles> = emptyArray) {
if (node.sourceFiles !== sourceFiles || node.prepends !== prepends) {
return createBundle(sourceFiles, prepends);
}
Expand Down
7 changes: 2 additions & 5 deletions src/compiler/program.ts
Expand Up @@ -1453,9 +1453,8 @@ namespace ts {
notImplementedResolver,
getEmitHost(writeFileCallback),
/*targetSourceFile*/ undefined,
/*transformers*/ noTransformers,
/*emitOnlyDtsFiles*/ false,
/*transformers*/ undefined,
/*declaraitonTransformers*/ undefined,
/*onlyBuildInfo*/ true
);

Expand Down Expand Up @@ -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");
Expand Down
50 changes: 47 additions & 3 deletions src/compiler/transformer.ts
Expand Up @@ -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<SourceFile | Bundle>[] = [];

addRange(transformers, customTransformers && customTransformers.before);
addRange(transformers, customTransformers && map(customTransformers.before, wrapScriptTransformerFactory));

transformers.push(transformTypeScript);

Expand Down Expand Up @@ -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<SourceFile | Bundle>[] = [];
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<Bundle | SourceFile> {
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<T extends SourceFile | Bundle>(transformer: TransformerFactory<T> | CustomTransformerFactory, handleDefault: (node: Transformer<T>) => Transformer<Bundle | SourceFile>): TransformerFactory<Bundle | SourceFile> {
return context => {
const customTransformer = transformer(context);
return typeof customTransformer === "function"
? handleDefault(customTransformer)
: wrapCustomTransformer(customTransformer);
};
}

function wrapScriptTransformerFactory(transformer: TransformerFactory<SourceFile> | CustomTransformerFactory): TransformerFactory<Bundle | SourceFile> {
return wrapCustomTransformerFactory(transformer, chainBundle);
}

function wrapDeclarationTransformerFactory(transformer: TransformerFactory<Bundle | SourceFile> | CustomTransformerFactory): TransformerFactory<Bundle | SourceFile> {
return wrapCustomTransformerFactory(transformer, identity);
}

export function noEmitSubstitution(_hint: EmitHint, node: Node) {
return node;
}
Expand Down
19 changes: 16 additions & 3 deletions src/compiler/types.ts
Expand Up @@ -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<SourceFile>[];
before?: (TransformerFactory<SourceFile> | CustomTransformerFactory)[];
/** Custom transformers to evaluate after built-in .js transformations. */
after?: TransformerFactory<SourceFile>[];
after?: (TransformerFactory<SourceFile> | CustomTransformerFactory)[];
/** Custom transformers to evaluate after built-in .d.ts transformations. */
afterDeclarations?: TransformerFactory<Bundle | SourceFile>[];
afterDeclarations?: (TransformerFactory<Bundle | SourceFile> | CustomTransformerFactory)[];
}

/*@internal*/
export interface EmitTransformers {
scriptTransformers: readonly TransformerFactory<SourceFile | Bundle>[];
declarationTransformers: readonly TransformerFactory<SourceFile | Bundle>[];
}

export interface SourceMapSpan {
Expand Down
34 changes: 20 additions & 14 deletions src/testRunner/unittests/customTransforms.ts
Expand Up @@ -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<SourceFile> = 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" }
);

});
Expand Down
13 changes: 9 additions & 4 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Expand Up @@ -1879,13 +1879,18 @@ declare namespace ts {
sourceFile: SourceFile;
references?: ReadonlyArray<ResolvedProjectReference | undefined>;
}
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<SourceFile>[];
before?: (TransformerFactory<SourceFile> | CustomTransformerFactory)[];
/** Custom transformers to evaluate after built-in .js transformations. */
after?: TransformerFactory<SourceFile>[];
after?: (TransformerFactory<SourceFile> | CustomTransformerFactory)[];
/** Custom transformers to evaluate after built-in .d.ts transformations. */
afterDeclarations?: TransformerFactory<Bundle | SourceFile>[];
afterDeclarations?: (TransformerFactory<Bundle | SourceFile> | CustomTransformerFactory)[];
}
interface SourceMapSpan {
/** Line number in the .js file. */
Expand Down Expand Up @@ -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<SourceFile>, prepends?: ReadonlyArray<UnparsedSource>): Bundle;
function updateBundle(node: Bundle, sourceFiles: ReadonlyArray<SourceFile>, prepends?: ReadonlyArray<UnparsedSource | InputFiles>): Bundle;
function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray<Statement>): CallExpression;
function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray<Statement>, param: ParameterDeclaration, paramValue: Expression): CallExpression;
function createImmediatelyInvokedArrowFunction(statements: ReadonlyArray<Statement>): CallExpression;
Expand Down
13 changes: 9 additions & 4 deletions tests/baselines/reference/api/typescript.d.ts
Expand Up @@ -1879,13 +1879,18 @@ declare namespace ts {
sourceFile: SourceFile;
references?: ReadonlyArray<ResolvedProjectReference | undefined>;
}
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<SourceFile>[];
before?: (TransformerFactory<SourceFile> | CustomTransformerFactory)[];
/** Custom transformers to evaluate after built-in .js transformations. */
after?: TransformerFactory<SourceFile>[];
after?: (TransformerFactory<SourceFile> | CustomTransformerFactory)[];
/** Custom transformers to evaluate after built-in .d.ts transformations. */
afterDeclarations?: TransformerFactory<Bundle | SourceFile>[];
afterDeclarations?: (TransformerFactory<Bundle | SourceFile> | CustomTransformerFactory)[];
}
interface SourceMapSpan {
/** Line number in the .js file. */
Expand Down Expand Up @@ -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<SourceFile>, prepends?: ReadonlyArray<UnparsedSource>): Bundle;
function updateBundle(node: Bundle, sourceFiles: ReadonlyArray<SourceFile>, prepends?: ReadonlyArray<UnparsedSource | InputFiles>): Bundle;
function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray<Statement>): CallExpression;
function createImmediatelyInvokedFunctionExpression(statements: ReadonlyArray<Statement>, param: ParameterDeclaration, paramValue: Expression): CallExpression;
function createImmediatelyInvokedArrowFunction(statements: ReadonlyArray<Statement>): CallExpression;
Expand Down