diff --git a/src/compiler/tsbuildPublic.ts b/src/compiler/tsbuildPublic.ts index cdca55c4e4e8e..2a2b39cee826f 100644 --- a/src/compiler/tsbuildPublic.ts +++ b/src/compiler/tsbuildPublic.ts @@ -131,9 +131,9 @@ namespace ts { } export interface SolutionBuilder { - build(project?: string, cancellationToken?: CancellationToken): ExitStatus; + build(project?: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus; clean(project?: string): ExitStatus; - buildReferences(project: string, cancellationToken?: CancellationToken): ExitStatus; + buildReferences(project: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus; cleanReferences(project?: string): ExitStatus; getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; @@ -1125,7 +1125,7 @@ namespace ts { break; case BuildStep.BuildInvalidatedProjectOfBundle: - Debug.checkDefined(invalidatedProjectOfBundle).done(cancellationToken); + Debug.checkDefined(invalidatedProjectOfBundle).done(cancellationToken, writeFile, customTransformers); step = BuildStep.Done; break; @@ -1649,7 +1649,7 @@ namespace ts { } } - function build(state: SolutionBuilderState, project?: string, cancellationToken?: CancellationToken, onlyReferences?: boolean): ExitStatus { + function build(state: SolutionBuilderState, project?: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers, onlyReferences?: boolean): ExitStatus { const buildOrder = getBuildOrderFor(state, project, onlyReferences); if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; @@ -1661,7 +1661,7 @@ namespace ts { const invalidatedProject = getNextInvalidatedProject(state, buildOrder, reportQueue); if (!invalidatedProject) break; reportQueue = false; - invalidatedProject.done(cancellationToken); + invalidatedProject.done(cancellationToken, writeFile, getCustomTransformers?.(invalidatedProject.project)); if (!state.diagnostics.has(invalidatedProject.projectPath)) successfulProjects++; } @@ -1894,9 +1894,9 @@ namespace ts { function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: readonly string[], options: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder { const state = createSolutionBuilderState(watch, hostOrHostWithWatch, rootNames, options, baseWatchOptions); return { - build: (project, cancellationToken) => build(state, project, cancellationToken), + build: (project, cancellationToken, writeFile, getCustomTransformers) => build(state, project, cancellationToken, writeFile, getCustomTransformers), clean: project => clean(state, project), - buildReferences: (project, cancellationToken) => build(state, project, cancellationToken, /*onlyReferences*/ true), + buildReferences: (project, cancellationToken, writeFile, getCustomTransformers) => build(state, project, cancellationToken, writeFile, getCustomTransformers, /*onlyReferences*/ true), cleanReferences: project => clean(state, project, /*onlyReferences*/ true), getNextInvalidatedProject: cancellationToken => { setupInitialBuild(state, cancellationToken); diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index bf372efb81c20..96f2132224bf4 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -131,6 +131,7 @@ "unittests/tsbuild/noEmitOnError.ts", "unittests/tsbuild/outFile.ts", "unittests/tsbuild/outputPaths.ts", + "unittests/tsbuild/publicApi.ts", "unittests/tsbuild/referencesWithRootDirInParent.ts", "unittests/tsbuild/resolveJsonModule.ts", "unittests/tsbuild/sample.ts", diff --git a/src/testRunner/unittests/tsbuild/publicApi.ts b/src/testRunner/unittests/tsbuild/publicApi.ts new file mode 100644 index 0000000000000..6c4e634108044 --- /dev/null +++ b/src/testRunner/unittests/tsbuild/publicApi.ts @@ -0,0 +1,124 @@ +namespace ts { + describe("unittests:: tsbuild:: Public API with custom transformers when passed to build", () => { + let sys: TscCompileSystem; + before(() => { + const initialFs = getFsWithTime(loadProjectFromFiles({ + "/src/tsconfig.json": JSON.stringify({ + references: [ + { path: "./shared/tsconfig.json" }, + { path: "./webpack/tsconfig.json" } + ], + files: [] + }), + "/src/shared/tsconfig.json": JSON.stringify({ + compilerOptions: { composite: true }, + }), + "/src/shared/index.ts": `export function f1() { } +export class c { } +export enum e { } +// leading +export function f2() { } // trailing`, + "/src/webpack/tsconfig.json": JSON.stringify({ + compilerOptions: { + composite: true, + }, + references: [{ path: "../shared/tsconfig.json" }] + }), + "/src/webpack/index.ts": `export function f2() { } +export class c2 { } +export enum e2 { } +// leading +export function f22() { } // trailing`, + })).fs.makeReadonly(); + const inputFs = initialFs.shadow(); + inputFs.makeReadonly(); + const fs = inputFs.shadow(); + + // Create system + sys = new fakes.System(fs, { executingFilePath: "/lib/tsc" }) as TscCompileSystem; + fakes.patchHostForBuildInfoReadWrite(sys); + const commandLineArgs = ["--b", "/src/tsconfig.json"]; + sys.write(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}\n`); + sys.exit = exitCode => sys.exitCode = exitCode; + const writtenFiles = sys.writtenFiles = new Set(); + const originalWriteFile = sys.writeFile; + sys.writeFile = (fileName, content, writeByteOrderMark) => { + const path = toPathWithSystem(sys, fileName); + assert.isFalse(writtenFiles.has(path)); + writtenFiles.add(path); + return originalWriteFile.call(sys, fileName, content, writeByteOrderMark); + }; + const { cb, getPrograms } = commandLineCallbacks(sys, /*originalReadCall*/ undefined, originalWriteFile); + const buildHost = createSolutionBuilderHost( + sys, + /*createProgram*/ undefined, + createDiagnosticReporter(sys, /*pretty*/ true), + createBuilderStatusReporter(sys, /*pretty*/ true), + errorCount => sys.write(getErrorSummaryText(errorCount, sys.newLine)) + ); + buildHost.afterProgramEmitAndDiagnostics = cb; + buildHost.afterEmitBundle = cb; + const builder = createSolutionBuilder(buildHost, [commandLineArgs[1]], { verbose: true }); + const exitStatus = builder.build(/*project*/ undefined, /*cancellationToken*/ undefined, /*writeFile*/ undefined, getCustomTransformers); + sys.exit(exitStatus); + sys.write(`exitCode:: ExitStatus.${ExitStatus[sys.exitCode as ExitStatus]}\n`); + const baseline: string[] = []; + tscWatch.baselinePrograms(baseline, getPrograms, emptyArray, /*baselineDependencies*/ false); + sys.write(baseline.join("\n")); + fs.makeReadonly(); + sys.baseLine = () => { + const baseFsPatch = inputFs.diff(/*base*/ undefined, { baseIsNotShadowRoot: true }); + const patch = fs.diff(inputFs, { includeChangedFileWithSameContent: true }); + return { + file: `tsbuild/$publicAPI/${BuildKind.Initial}/${"build with custom transformers".split(" ").join("-")}.js`, + text: `Input:: +${baseFsPatch ? vfs.formatPatch(baseFsPatch) : ""} + +Output:: +${sys.output.join("")} + +${patch ? vfs.formatPatch(patch) : ""}` + }; + }; + + function getCustomTransformers(project: string): CustomTransformers { + const before: TransformerFactory = context => { + return file => visitEachChild(file, visit, context); + function visit(node: Node): VisitResult { + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + return visitFunction(node); + default: + return visitEachChild(node, visit, context); + } + } + function visitFunction(node: FunctionDeclaration) { + addSyntheticLeadingComment(node, SyntaxKind.MultiLineCommentTrivia, `@before${project}`, /*hasTrailingNewLine*/ true); + return node; + } + }; + + const after: TransformerFactory = context => { + return file => visitEachChild(file, visit, context); + function visit(node: Node): VisitResult { + switch (node.kind) { + case SyntaxKind.VariableStatement: + return visitVariableStatement(node); + default: + return visitEachChild(node, visit, context); + } + } + function visitVariableStatement(node: VariableStatement) { + addSyntheticLeadingComment(node, SyntaxKind.SingleLineCommentTrivia, `@after${project}`); + return node; + } + }; + return { before: [before], after: [after] }; + } + }); + after(() => { + sys = undefined!; + }); + verifyTscBaseline(() => sys); + }); +} \ No newline at end of file diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index e94b1da823f34..6195d669a8306 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -5219,9 +5219,9 @@ declare namespace ts { interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { } interface SolutionBuilder { - build(project?: string, cancellationToken?: CancellationToken): ExitStatus; + build(project?: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus; clean(project?: string): ExitStatus; - buildReferences(project: string, cancellationToken?: CancellationToken): ExitStatus; + buildReferences(project: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus; cleanReferences(project?: string): ExitStatus; getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; } diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 7b8a24ba088b4..b86a3a29970a4 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -5219,9 +5219,9 @@ declare namespace ts { interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { } interface SolutionBuilder { - build(project?: string, cancellationToken?: CancellationToken): ExitStatus; + build(project?: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus; clean(project?: string): ExitStatus; - buildReferences(project: string, cancellationToken?: CancellationToken): ExitStatus; + buildReferences(project: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus; cleanReferences(project?: string): ExitStatus; getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; } diff --git a/tests/baselines/reference/tsbuild/$publicAPI/initial-build/build-with-custom-transformers.js b/tests/baselines/reference/tsbuild/$publicAPI/initial-build/build-with-custom-transformers.js new file mode 100644 index 0000000000000..53ecd86e9add0 --- /dev/null +++ b/tests/baselines/reference/tsbuild/$publicAPI/initial-build/build-with-custom-transformers.js @@ -0,0 +1,216 @@ +Input:: +//// [/lib/lib.d.ts] +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +declare const console: { log(msg: any): void; }; + +//// [/src/shared/index.ts] +export function f1() { } +export class c { } +export enum e { } +// leading +export function f2() { } // trailing + +//// [/src/shared/tsconfig.json] +{"compilerOptions":{"composite":true}} + +//// [/src/tsconfig.json] +{"references":[{"path":"./shared/tsconfig.json"},{"path":"./webpack/tsconfig.json"}],"files":[]} + +//// [/src/webpack/index.ts] +export function f2() { } +export class c2 { } +export enum e2 { } +// leading +export function f22() { } // trailing + +//// [/src/webpack/tsconfig.json] +{"compilerOptions":{"composite":true},"references":[{"path":"../shared/tsconfig.json"}]} + + + +Output:: +/lib/tsc --b /src/tsconfig.json +[12:00:00 AM] Projects in this build: + * src/shared/tsconfig.json + * src/webpack/tsconfig.json + * src/tsconfig.json + +[12:00:00 AM] Project 'src/shared/tsconfig.json' is out of date because output file 'src/shared/index.js' does not exist + +[12:00:00 AM] Building project '/src/shared/tsconfig.json'... + +[12:00:00 AM] Project 'src/webpack/tsconfig.json' is out of date because output file 'src/webpack/index.js' does not exist + +[12:00:00 AM] Building project '/src/webpack/tsconfig.json'... + +exitCode:: ExitStatus.Success +Program root files: ["/src/shared/index.ts"] +Program options: {"composite":true,"configFilePath":"/src/shared/tsconfig.json"} +Program structureReused: Not +Program files:: +/lib/lib.d.ts +/src/shared/index.ts + +Semantic diagnostics in builder refreshed for:: +/lib/lib.d.ts +/src/shared/index.ts + +Program root files: ["/src/webpack/index.ts"] +Program options: {"composite":true,"configFilePath":"/src/webpack/tsconfig.json"} +Program structureReused: Not +Program files:: +/lib/lib.d.ts +/src/webpack/index.ts + +Semantic diagnostics in builder refreshed for:: +/lib/lib.d.ts +/src/webpack/index.ts + + +//// [/src/shared/index.d.ts] +export declare function f1(): void; +export declare class c { +} +export declare enum e { +} +export declare function f2(): void; + + +//// [/src/shared/index.js] +"use strict"; +exports.__esModule = true; +exports.f2 = exports.e = exports.c = exports.f1 = void 0; +/*@before/src/shared/tsconfig.json*/ +function f1() { } +exports.f1 = f1; +//@after/src/shared/tsconfig.json +var c = /** @class */ (function () { + function c() { + } + return c; +}()); +exports.c = c; +//@after/src/shared/tsconfig.json +var e; +(function (e) { +})(e = exports.e || (exports.e = {})); +// leading +/*@before/src/shared/tsconfig.json*/ +function f2() { } // trailing +exports.f2 = f2; + + +//// [/src/shared/tsconfig.tsbuildinfo] +{"program":{"fileNames":["../../lib/lib.d.ts","./index.ts"],"fileInfos":[{"version":"3858781397-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true},"8649344783-export function f1() { }\nexport class c { }\nexport enum e { }\n// leading\nexport function f2() { } // trailing"],"options":{"composite":true},"referencedMap":[],"exportedModulesMap":[],"semanticDiagnosticsPerFile":[1,2]},"version":"FakeTSVersion"} + +//// [/src/shared/tsconfig.tsbuildinfo.readable.baseline.txt] +{ + "program": { + "fileNames": [ + "../../lib/lib.d.ts", + "./index.ts" + ], + "fileInfos": { + "../../lib/lib.d.ts": { + "version": "3858781397-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ndeclare const console: { log(msg: any): void; };", + "signature": "3858781397-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ndeclare const console: { log(msg: any): void; };", + "affectsGlobalScope": true + }, + "./index.ts": { + "version": "8649344783-export function f1() { }\nexport class c { }\nexport enum e { }\n// leading\nexport function f2() { } // trailing", + "signature": "8649344783-export function f1() { }\nexport class c { }\nexport enum e { }\n// leading\nexport function f2() { } // trailing" + } + }, + "options": { + "composite": true + }, + "referencedMap": {}, + "exportedModulesMap": {}, + "semanticDiagnosticsPerFile": [ + "../../lib/lib.d.ts", + "./index.ts" + ] + }, + "version": "FakeTSVersion", + "size": 814 +} + +//// [/src/webpack/index.d.ts] +export declare function f2(): void; +export declare class c2 { +} +export declare enum e2 { +} +export declare function f22(): void; + + +//// [/src/webpack/index.js] +"use strict"; +exports.__esModule = true; +exports.f22 = exports.e2 = exports.c2 = exports.f2 = void 0; +/*@before/src/webpack/tsconfig.json*/ +function f2() { } +exports.f2 = f2; +//@after/src/webpack/tsconfig.json +var c2 = /** @class */ (function () { + function c2() { + } + return c2; +}()); +exports.c2 = c2; +//@after/src/webpack/tsconfig.json +var e2; +(function (e2) { +})(e2 = exports.e2 || (exports.e2 = {})); +// leading +/*@before/src/webpack/tsconfig.json*/ +function f22() { } // trailing +exports.f22 = f22; + + +//// [/src/webpack/tsconfig.tsbuildinfo] +{"program":{"fileNames":["../../lib/lib.d.ts","./index.ts"],"fileInfos":[{"version":"3858781397-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true},"20140662566-export function f2() { }\nexport class c2 { }\nexport enum e2 { }\n// leading\nexport function f22() { } // trailing"],"options":{"composite":true},"referencedMap":[],"exportedModulesMap":[],"semanticDiagnosticsPerFile":[1,2]},"version":"FakeTSVersion"} + +//// [/src/webpack/tsconfig.tsbuildinfo.readable.baseline.txt] +{ + "program": { + "fileNames": [ + "../../lib/lib.d.ts", + "./index.ts" + ], + "fileInfos": { + "../../lib/lib.d.ts": { + "version": "3858781397-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ndeclare const console: { log(msg: any): void; };", + "signature": "3858781397-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ndeclare const console: { log(msg: any): void; };", + "affectsGlobalScope": true + }, + "./index.ts": { + "version": "20140662566-export function f2() { }\nexport class c2 { }\nexport enum e2 { }\n// leading\nexport function f22() { } // trailing", + "signature": "20140662566-export function f2() { }\nexport class c2 { }\nexport enum e2 { }\n// leading\nexport function f22() { } // trailing" + } + }, + "options": { + "composite": true + }, + "referencedMap": {}, + "exportedModulesMap": {}, + "semanticDiagnosticsPerFile": [ + "../../lib/lib.d.ts", + "./index.ts" + ] + }, + "version": "FakeTSVersion", + "size": 818 +} +