Skip to content

Commit 0b71b81

Browse files
authored
Add transpileDeclaration API method (#58261)
1 parent 4900c7f commit 0b71b81

26 files changed

+755
-8
lines changed

src/compiler/emitter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ function getSourceMapFilePath(jsFilePath: string, options: CompilerOptions) {
533533
}
534534

535535
/** @internal */
536-
export function getOutputExtension(fileName: string, options: CompilerOptions): Extension {
536+
export function getOutputExtension(fileName: string, options: Pick<CompilerOptions, "jsx">): Extension {
537537
return fileExtensionIs(fileName, Extension.Json) ? Extension.Json :
538538
options.jsx === JsxEmit.Preserve && fileExtensionIsOneOf(fileName, [Extension.Jsx, Extension.Tsx]) ? Extension.Jsx :
539539
fileExtensionIsOneOf(fileName, [Extension.Mts, Extension.Mjs]) ? Extension.Mjs :

src/harness/harnessIO.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ export namespace Compiler {
303303
{ name: "noTypesAndSymbols", type: "boolean", defaultValueDescription: false },
304304
// Emitted js baseline will print full paths for every output file
305305
{ name: "fullEmitPaths", type: "boolean", defaultValueDescription: false },
306+
{ name: "reportDiagnostics", type: "boolean", defaultValueDescription: false }, // used to enable error collection in `transpile` baselines
306307
];
307308

308309
let optionsIndex: Map<string, ts.CommandLineOption>;

src/harness/runnerbase.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
} from "./_namespaces/Harness";
55
import * as ts from "./_namespaces/ts";
66

7-
export type TestRunnerKind = CompilerTestKind | FourslashTestKind | "project";
7+
export type TestRunnerKind = CompilerTestKind | FourslashTestKind | "project" | "transpile";
88
export type CompilerTestKind = "conformance" | "compiler";
99
export type FourslashTestKind = "fourslash" | "fourslash-server";
1010

src/services/transpile.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
normalizePath,
2626
optionDeclarations,
2727
parseCustomTypeOption,
28+
ScriptTarget,
2829
toPath,
2930
transpileOptionValueCompilerOptions,
3031
} from "./_namespaces/ts";
@@ -51,14 +52,64 @@ const optionsRedundantWithVerbatimModuleSyntax = new Set([
5152

5253
/*
5354
* This function will compile source text from 'input' argument using specified compiler options.
54-
* If not options are provided - it will use a set of default compiler options.
55+
* If no options are provided - it will use a set of default compiler options.
5556
* Extra compiler options that will unconditionally be used by this function are:
5657
* - isolatedModules = true
5758
* - allowNonTsExtensions = true
5859
* - noLib = true
5960
* - noResolve = true
61+
* - declaration = false
6062
*/
6163
export function transpileModule(input: string, transpileOptions: TranspileOptions): TranspileOutput {
64+
return transpileWorker(input, transpileOptions, /*declaration*/ false);
65+
}
66+
67+
/*
68+
* This function will create a declaration file from 'input' argument using specified compiler options.
69+
* If no options are provided - it will use a set of default compiler options.
70+
* Extra compiler options that will unconditionally be used by this function are:
71+
* - isolatedDeclarations = true
72+
* - isolatedModules = true
73+
* - allowNonTsExtensions = true
74+
* - noLib = true
75+
* - noResolve = true
76+
* - declaration = true
77+
* - emitDeclarationOnly = true
78+
* Note that this declaration file may differ from one produced by a full program typecheck,
79+
* in that only types in the single input file are available to be used in the generated declarations.
80+
*/
81+
export function transpileDeclaration(input: string, transpileOptions: TranspileOptions): TranspileOutput {
82+
return transpileWorker(input, transpileOptions, /*declaration*/ true);
83+
}
84+
85+
// Declaration emit works without a `lib`, but some local inferences you'd expect to work won't without
86+
// at least a minimal `lib` available, since the checker will `any` their types without these defined.
87+
// Late bound symbol names, in particular, are impossible to define without `Symbol` at least partially defined.
88+
// TODO: This should *probably* just load the full, real `lib` for the `target`.
89+
const barebonesLibContent = `/// <reference no-default-lib="true"/>
90+
interface Boolean {}
91+
interface Function {}
92+
interface CallableFunction {}
93+
interface NewableFunction {}
94+
interface IArguments {}
95+
interface Number {}
96+
interface Object {}
97+
interface RegExp {}
98+
interface String {}
99+
interface Array<T> { length: number; [n: number]: T; }
100+
interface SymbolConstructor {
101+
(desc?: string | number): symbol;
102+
for(name: string): symbol;
103+
readonly toStringTag: symbol;
104+
}
105+
declare var Symbol: SymbolConstructor;
106+
interface Symbol {
107+
readonly [Symbol.toStringTag]: string;
108+
}`;
109+
const barebonesLibName = "lib.d.ts";
110+
const barebonesLibSourceFile = createSourceFile(barebonesLibName, barebonesLibContent, { languageVersion: ScriptTarget.Latest });
111+
112+
function transpileWorker(input: string, transpileOptions: TranspileOptions, declaration?: boolean): TranspileOutput {
62113
const diagnostics: Diagnostic[] = [];
63114

64115
const options: CompilerOptions = transpileOptions.compilerOptions ? fixupCompilerOptions(transpileOptions.compilerOptions, diagnostics) : {};
@@ -86,10 +137,19 @@ export function transpileModule(input: string, transpileOptions: TranspileOption
86137
// Filename can be non-ts file.
87138
options.allowNonTsExtensions = true;
88139

140+
if (declaration) {
141+
options.declaration = true;
142+
options.emitDeclarationOnly = true;
143+
options.isolatedDeclarations = true;
144+
}
145+
else {
146+
options.declaration = false;
147+
}
148+
89149
const newLine = getNewLineCharacter(options);
90150
// Create a compilerHost object to allow the compiler to read and write files
91151
const compilerHost: CompilerHost = {
92-
getSourceFile: fileName => fileName === normalizePath(inputFileName) ? sourceFile : undefined,
152+
getSourceFile: fileName => fileName === normalizePath(inputFileName) ? sourceFile : fileName === normalizePath(barebonesLibName) ? barebonesLibSourceFile : undefined,
93153
writeFile: (name, text) => {
94154
if (fileExtensionIs(name, ".map")) {
95155
Debug.assertEqual(sourceMapText, undefined, "Unexpected multiple source map outputs, file:", name);
@@ -100,12 +160,12 @@ export function transpileModule(input: string, transpileOptions: TranspileOption
100160
outputText = text;
101161
}
102162
},
103-
getDefaultLibFileName: () => "lib.d.ts",
163+
getDefaultLibFileName: () => barebonesLibName,
104164
useCaseSensitiveFileNames: () => false,
105165
getCanonicalFileName: fileName => fileName,
106166
getCurrentDirectory: () => "",
107167
getNewLine: () => newLine,
108-
fileExists: (fileName): boolean => fileName === inputFileName,
168+
fileExists: (fileName): boolean => fileName === inputFileName || (!!declaration && fileName === barebonesLibName),
109169
readFile: () => "",
110170
directoryExists: () => true,
111171
getDirectories: () => [],
@@ -135,14 +195,17 @@ export function transpileModule(input: string, transpileOptions: TranspileOption
135195
let outputText: string | undefined;
136196
let sourceMapText: string | undefined;
137197

138-
const program = createProgram([inputFileName], options, compilerHost);
198+
const inputs = declaration ? [inputFileName, barebonesLibName] : [inputFileName];
199+
const program = createProgram(inputs, options, compilerHost);
139200

140201
if (transpileOptions.reportDiagnostics) {
141202
addRange(/*to*/ diagnostics, /*from*/ program.getSyntacticDiagnostics(sourceFile));
142203
addRange(/*to*/ diagnostics, /*from*/ program.getOptionsDiagnostics());
143204
}
144205
// Emit
145-
program.emit(/*targetSourceFile*/ undefined, /*writeFile*/ undefined, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ undefined, transpileOptions.transformers);
206+
const result = program.emit(/*targetSourceFile*/ undefined, /*writeFile*/ undefined, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ declaration, transpileOptions.transformers, /*forceDtsEmit*/ declaration);
207+
208+
addRange(/*to*/ diagnostics, /*from*/ result.diagnostics);
146209

147210
if (outputText === undefined) return Debug.fail("Output generation failed");
148211

src/testRunner/_namespaces/Harness.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export { Parallel };
77

88
export * from "../fourslashRunner";
99
export * from "../compilerRunner";
10+
export * from "../transpileRunner";
1011
export * from "../runner";
1112

1213
// If running as emitted CJS, don't start executing the tests here; instead start in runner.ts.

src/testRunner/runner.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
setShardId,
1212
setShards,
1313
TestRunnerKind,
14+
TranspileRunner,
1415
} from "./_namespaces/Harness";
1516
import * as project from "./_namespaces/project";
1617
import * as ts from "./_namespaces/ts";
@@ -66,6 +67,8 @@ export function createRunner(kind: TestRunnerKind): RunnerBase {
6667
return new FourSlashRunner(FourSlash.FourSlashTestType.Server);
6768
case "project":
6869
return new project.ProjectRunner();
70+
case "transpile":
71+
return new TranspileRunner();
6972
}
7073
return ts.Debug.fail(`Unknown runner kind ${kind}`);
7174
}
@@ -190,6 +193,9 @@ function handleTestConfig() {
190193
case "fourslash-generated":
191194
runners.push(new GeneratedFourslashRunner(FourSlash.FourSlashTestType.Native));
192195
break;
196+
case "transpile":
197+
runners.push(new TranspileRunner());
198+
break;
193199
}
194200
}
195201
}
@@ -206,6 +212,9 @@ function handleTestConfig() {
206212
runners.push(new FourSlashRunner(FourSlash.FourSlashTestType.Native));
207213
runners.push(new FourSlashRunner(FourSlash.FourSlashTestType.Server));
208214
// runners.push(new GeneratedFourslashRunner());
215+
216+
// transpile
217+
runners.push(new TranspileRunner());
209218
}
210219
if (runUnitTests === undefined) {
211220
runUnitTests = runners.length !== 1; // Don't run unit tests when running only one runner if unit tests were not explicitly asked for

src/testRunner/transpileRunner.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import {
2+
Baseline,
3+
Compiler,
4+
getFileBasedTestConfigurations,
5+
IO,
6+
RunnerBase,
7+
TestCaseParser,
8+
TestRunnerKind,
9+
} from "./_namespaces/Harness";
10+
import * as ts from "./_namespaces/ts";
11+
import * as vpath from "./_namespaces/vpath";
12+
13+
export class TranspileRunner extends RunnerBase {
14+
protected basePath = "tests/cases/transpile";
15+
protected testSuiteName: TestRunnerKind = "transpile";
16+
17+
public enumerateTestFiles() {
18+
// see also: `enumerateTestFiles` in tests/webTestServer.ts
19+
return this.enumerateFiles(this.basePath, /\.[cm]?[tj]sx?/i, { recursive: true });
20+
}
21+
22+
public kind() {
23+
return this.testSuiteName;
24+
}
25+
26+
public initializeTests() {
27+
if (this.tests.length === 0) {
28+
this.tests = IO.enumerateTestFiles(this);
29+
}
30+
31+
describe(this.testSuiteName + " tests", () => {
32+
this.tests.forEach(file => {
33+
file = vpath.normalizeSeparators(file);
34+
describe(file, () => {
35+
const tests = TranspileTestCase.getConfigurations(file);
36+
for (const test of tests) {
37+
test.run();
38+
}
39+
});
40+
});
41+
});
42+
}
43+
}
44+
45+
enum TranspileKind {
46+
Module,
47+
Declaration,
48+
}
49+
50+
class TranspileTestCase {
51+
static varyBy = [];
52+
53+
static getConfigurations(file: string): TranspileTestCase[] {
54+
const ext = vpath.extname(file);
55+
const baseName = vpath.basename(file);
56+
const justName = baseName.slice(0, baseName.length - ext.length);
57+
const content = IO.readFile(file)!;
58+
const settings = TestCaseParser.extractCompilerSettings(content);
59+
const settingConfigurations = getFileBasedTestConfigurations(settings, TranspileTestCase.varyBy);
60+
return settingConfigurations?.map(c => {
61+
const desc = Object.entries(c).map(([key, value]) => `${key}=${value}`).join(",");
62+
return new TranspileTestCase(`${justName}(${desc})`, ext, content, { ...settings, ...c });
63+
}) ?? [new TranspileTestCase(justName, ext, content, settings)];
64+
}
65+
66+
private jsOutName;
67+
private dtsOutName;
68+
private units;
69+
constructor(
70+
private justName: string,
71+
private ext: string,
72+
private content: string,
73+
private settings: TestCaseParser.CompilerSettings,
74+
) {
75+
this.jsOutName = justName + this.getJsOutputExtension(`${justName}${ext}`);
76+
this.dtsOutName = justName + ts.getDeclarationEmitExtensionForPath(`${justName}${ext}`);
77+
this.units = TestCaseParser.makeUnitsFromTest(content, `${justName}${ext}`, settings);
78+
}
79+
80+
getJsOutputExtension(name: string) {
81+
return ts.getOutputExtension(name, { jsx: this.settings.jsx === "preserve" ? ts.JsxEmit.Preserve : undefined });
82+
}
83+
84+
runKind(kind: TranspileKind) {
85+
it(`transpile test ${this.justName} has expected ${kind === TranspileKind.Module ? "js" : "declaration"} output`, () => {
86+
let baselineText = "";
87+
88+
// include inputs in output so how the test is parsed and broken down is more obvious
89+
this.units.testUnitData.forEach(unit => {
90+
baselineText += `//// [${unit.name}] ////\r\n`;
91+
baselineText += unit.content;
92+
if (!unit.content.endsWith("\n")) {
93+
baselineText += "\r\n";
94+
}
95+
});
96+
97+
this.units.testUnitData.forEach(unit => {
98+
const opts: ts.CompilerOptions = {};
99+
Compiler.setCompilerOptionsFromHarnessSetting(this.settings, opts);
100+
const result = (kind === TranspileKind.Module ? ts.transpileModule : ts.transpileDeclaration)(unit.content, { compilerOptions: opts, fileName: unit.name, reportDiagnostics: this.settings.reportDiagnostics === "true" });
101+
102+
baselineText += `//// [${ts.changeExtension(unit.name, kind === TranspileKind.Module ? this.getJsOutputExtension(unit.name) : ts.getDeclarationEmitExtensionForPath(unit.name))}] ////\r\n`;
103+
baselineText += result.outputText;
104+
if (!result.outputText.endsWith("\n")) {
105+
baselineText += "\r\n";
106+
}
107+
if (result.diagnostics && result.diagnostics.length) {
108+
baselineText += "\r\n\r\n//// [Diagnostics reported]\r\n";
109+
baselineText += Compiler.getErrorBaseline([{ content: unit.content, unitName: unit.name }], result.diagnostics, !!opts.pretty);
110+
if (!baselineText.endsWith("\n")) {
111+
baselineText += "\r\n";
112+
}
113+
}
114+
});
115+
116+
Baseline.runBaseline(`transpile/${kind === TranspileKind.Module ? this.jsOutName : this.dtsOutName}`, baselineText);
117+
});
118+
}
119+
120+
run() {
121+
if (!this.settings.emitDeclarationOnly) {
122+
this.runKind(TranspileKind.Module);
123+
}
124+
if (this.settings.declaration) {
125+
this.runKind(TranspileKind.Declaration);
126+
}
127+
}
128+
}

tests/baselines/reference/api/typescript.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11190,6 +11190,7 @@ declare namespace ts {
1119011190
};
1119111191
function preProcessFile(sourceText: string, readImportFiles?: boolean, detectJavaScriptImports?: boolean): PreProcessedFileInfo;
1119211192
function transpileModule(input: string, transpileOptions: TranspileOptions): TranspileOutput;
11193+
function transpileDeclaration(input: string, transpileOptions: TranspileOptions): TranspileOutput;
1119311194
function transpile(input: string, compilerOptions?: CompilerOptions, fileName?: string, diagnostics?: Diagnostic[], moduleName?: string): string;
1119411195
interface TranspileOptions {
1119511196
compilerOptions?: CompilerOptions;

0 commit comments

Comments
 (0)