-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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 transpileDeclaration
API method
#58261
Changes from all commits
4e2febf
425ffd0
26cae5b
94ab02d
18c6810
b0a4b72
6ee4762
764f861
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,7 @@ import { | |
normalizePath, | ||
optionDeclarations, | ||
parseCustomTypeOption, | ||
ScriptTarget, | ||
toPath, | ||
transpileOptionValueCompilerOptions, | ||
} from "./_namespaces/ts"; | ||
|
@@ -51,14 +52,64 @@ const optionsRedundantWithVerbatimModuleSyntax = new Set([ | |
|
||
/* | ||
* This function will compile source text from 'input' argument using specified compiler options. | ||
* If not options are provided - it will use a set of default compiler options. | ||
* If no options are provided - it will use a set of default compiler options. | ||
* Extra compiler options that will unconditionally be used by this function are: | ||
* - isolatedModules = true | ||
* - allowNonTsExtensions = true | ||
* - noLib = true | ||
* - noResolve = true | ||
* - declaration = false | ||
*/ | ||
export function transpileModule(input: string, transpileOptions: TranspileOptions): TranspileOutput { | ||
return transpileWorker(input, transpileOptions, /*declaration*/ false); | ||
} | ||
|
||
/* | ||
* This function will create a declaration file from 'input' argument using specified compiler options. | ||
* If no options are provided - it will use a set of default compiler options. | ||
* Extra compiler options that will unconditionally be used by this function are: | ||
* - isolatedDeclarations = true | ||
* - isolatedModules = true | ||
* - allowNonTsExtensions = true | ||
* - noLib = true | ||
* - noResolve = true | ||
* - declaration = true | ||
* - emitDeclarationOnly = true | ||
* Note that this declaration file may differ from one produced by a full program typecheck, | ||
* in that only types in the single input file are available to be used in the generated declarations. | ||
*/ | ||
export function transpileDeclaration(input: string, transpileOptions: TranspileOptions): TranspileOutput { | ||
return transpileWorker(input, transpileOptions, /*declaration*/ true); | ||
} | ||
|
||
// Declaration emit works without a `lib`, but some local inferences you'd expect to work won't without | ||
// at least a minimal `lib` available, since the checker will `any` their types without these defined. | ||
// Late bound symbol names, in particular, are impossible to define without `Symbol` at least partially defined. | ||
// TODO: This should *probably* just load the full, real `lib` for the `target`. | ||
const barebonesLibContent = `/// <reference no-default-lib="true"/> | ||
interface Boolean {} | ||
interface Function {} | ||
interface CallableFunction {} | ||
interface NewableFunction {} | ||
interface IArguments {} | ||
interface Number {} | ||
interface Object {} | ||
interface RegExp {} | ||
interface String {} | ||
interface Array<T> { length: number; [n: number]: T; } | ||
interface SymbolConstructor { | ||
(desc?: string | number): symbol; | ||
for(name: string): symbol; | ||
readonly toStringTag: symbol; | ||
} | ||
declare var Symbol: SymbolConstructor; | ||
interface Symbol { | ||
readonly [Symbol.toStringTag]: string; | ||
}`; | ||
const barebonesLibName = "lib.d.ts"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly this sorta weirds me out; is this something we should be formalizing in our real dts files somehow? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Eh, it's weirder that we can't typecheck export function f<T extends {x: number, y: number}>(x: T) {
const {x: num, ...rest} = x;
return rest;
} has an inferred return type of Personally, by default I think I'd rather load the fully correct We could leave it up to the caller - and load the default |
||
const barebonesLibSourceFile = createSourceFile(barebonesLibName, barebonesLibContent, { languageVersion: ScriptTarget.Latest }); | ||
|
||
function transpileWorker(input: string, transpileOptions: TranspileOptions, declaration?: boolean): TranspileOutput { | ||
const diagnostics: Diagnostic[] = []; | ||
|
||
const options: CompilerOptions = transpileOptions.compilerOptions ? fixupCompilerOptions(transpileOptions.compilerOptions, diagnostics) : {}; | ||
|
@@ -86,10 +137,19 @@ export function transpileModule(input: string, transpileOptions: TranspileOption | |
// Filename can be non-ts file. | ||
options.allowNonTsExtensions = true; | ||
|
||
if (declaration) { | ||
options.declaration = true; | ||
options.emitDeclarationOnly = true; | ||
options.isolatedDeclarations = true; | ||
} | ||
else { | ||
options.declaration = false; | ||
} | ||
|
||
const newLine = getNewLineCharacter(options); | ||
// Create a compilerHost object to allow the compiler to read and write files | ||
const compilerHost: CompilerHost = { | ||
getSourceFile: fileName => fileName === normalizePath(inputFileName) ? sourceFile : undefined, | ||
getSourceFile: fileName => fileName === normalizePath(inputFileName) ? sourceFile : fileName === normalizePath(barebonesLibName) ? barebonesLibSourceFile : undefined, | ||
writeFile: (name, text) => { | ||
if (fileExtensionIs(name, ".map")) { | ||
Debug.assertEqual(sourceMapText, undefined, "Unexpected multiple source map outputs, file:", name); | ||
|
@@ -100,12 +160,12 @@ export function transpileModule(input: string, transpileOptions: TranspileOption | |
outputText = text; | ||
} | ||
}, | ||
getDefaultLibFileName: () => "lib.d.ts", | ||
getDefaultLibFileName: () => barebonesLibName, | ||
useCaseSensitiveFileNames: () => false, | ||
getCanonicalFileName: fileName => fileName, | ||
getCurrentDirectory: () => "", | ||
getNewLine: () => newLine, | ||
fileExists: (fileName): boolean => fileName === inputFileName, | ||
fileExists: (fileName): boolean => fileName === inputFileName || (!!declaration && fileName === barebonesLibName), | ||
readFile: () => "", | ||
directoryExists: () => true, | ||
getDirectories: () => [], | ||
|
@@ -135,14 +195,17 @@ export function transpileModule(input: string, transpileOptions: TranspileOption | |
let outputText: string | undefined; | ||
let sourceMapText: string | undefined; | ||
|
||
const program = createProgram([inputFileName], options, compilerHost); | ||
const inputs = declaration ? [inputFileName, barebonesLibName] : [inputFileName]; | ||
const program = createProgram(inputs, options, compilerHost); | ||
|
||
if (transpileOptions.reportDiagnostics) { | ||
addRange(/*to*/ diagnostics, /*from*/ program.getSyntacticDiagnostics(sourceFile)); | ||
addRange(/*to*/ diagnostics, /*from*/ program.getOptionsDiagnostics()); | ||
} | ||
// Emit | ||
program.emit(/*targetSourceFile*/ undefined, /*writeFile*/ undefined, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ undefined, transpileOptions.transformers); | ||
const result = program.emit(/*targetSourceFile*/ undefined, /*writeFile*/ undefined, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ declaration, transpileOptions.transformers, /*forceDtsEmit*/ declaration); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I dont think we should set Eg normally d.ts is not generated for json but this will generate it and i dont think we want that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes.
We set those options right above. We need to set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Honestly, if someone explicitly passes json into this API, it's because they wanted a declaration file for it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JSON support sounds like a useful feature we might use. Our system treats JSON modules as frozen so potentially we could postprocess the output to express this readonly nature. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just noting the differences between our normal emit vs setting forceDtsEmit which i had added for incremental checks |
||
|
||
addRange(/*to*/ diagnostics, /*from*/ result.diagnostics); | ||
|
||
if (outputText === undefined) return Debug.fail("Output generation failed"); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import { | |
setShardId, | ||
setShards, | ||
TestRunnerKind, | ||
TranspileRunner, | ||
} from "./_namespaces/Harness"; | ||
import * as project from "./_namespaces/project"; | ||
import * as ts from "./_namespaces/ts"; | ||
|
@@ -66,6 +67,8 @@ export function createRunner(kind: TestRunnerKind): RunnerBase { | |
return new FourSlashRunner(FourSlash.FourSlashTestType.Server); | ||
case "project": | ||
return new project.ProjectRunner(); | ||
case "transpile": | ||
return new TranspileRunner(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a mega good idea There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we have time, we should port the bulk of the existing I also have a backlogged idea to make a runner for |
||
} | ||
return ts.Debug.fail(`Unknown runner kind ${kind}`); | ||
} | ||
|
@@ -190,6 +193,9 @@ function handleTestConfig() { | |
case "fourslash-generated": | ||
runners.push(new GeneratedFourslashRunner(FourSlash.FourSlashTestType.Native)); | ||
break; | ||
case "transpile": | ||
runners.push(new TranspileRunner()); | ||
break; | ||
} | ||
} | ||
} | ||
|
@@ -206,6 +212,9 @@ function handleTestConfig() { | |
runners.push(new FourSlashRunner(FourSlash.FourSlashTestType.Native)); | ||
runners.push(new FourSlashRunner(FourSlash.FourSlashTestType.Server)); | ||
// runners.push(new GeneratedFourslashRunner()); | ||
|
||
// transpile | ||
runners.push(new TranspileRunner()); | ||
} | ||
if (runUnitTests === undefined) { | ||
runUnitTests = runners.length !== 1; // Don't run unit tests when running only one runner if unit tests were not explicitly asked for | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import { | ||
Baseline, | ||
Compiler, | ||
getFileBasedTestConfigurations, | ||
IO, | ||
RunnerBase, | ||
TestCaseParser, | ||
TestRunnerKind, | ||
} from "./_namespaces/Harness"; | ||
import * as ts from "./_namespaces/ts"; | ||
import * as vpath from "./_namespaces/vpath"; | ||
|
||
export class TranspileRunner extends RunnerBase { | ||
protected basePath = "tests/cases/transpile"; | ||
protected testSuiteName: TestRunnerKind = "transpile"; | ||
|
||
public enumerateTestFiles() { | ||
// see also: `enumerateTestFiles` in tests/webTestServer.ts | ||
return this.enumerateFiles(this.basePath, /\.[cm]?[tj]sx?/i, { recursive: true }); | ||
} | ||
|
||
public kind() { | ||
return this.testSuiteName; | ||
} | ||
|
||
public initializeTests() { | ||
if (this.tests.length === 0) { | ||
this.tests = IO.enumerateTestFiles(this); | ||
} | ||
|
||
describe(this.testSuiteName + " tests", () => { | ||
this.tests.forEach(file => { | ||
file = vpath.normalizeSeparators(file); | ||
describe(file, () => { | ||
const tests = TranspileTestCase.getConfigurations(file); | ||
for (const test of tests) { | ||
test.run(); | ||
} | ||
}); | ||
}); | ||
}); | ||
} | ||
} | ||
|
||
enum TranspileKind { | ||
Module, | ||
Declaration, | ||
} | ||
|
||
class TranspileTestCase { | ||
static varyBy = []; | ||
|
||
static getConfigurations(file: string): TranspileTestCase[] { | ||
const ext = vpath.extname(file); | ||
const baseName = vpath.basename(file); | ||
const justName = baseName.slice(0, baseName.length - ext.length); | ||
const content = IO.readFile(file)!; | ||
const settings = TestCaseParser.extractCompilerSettings(content); | ||
const settingConfigurations = getFileBasedTestConfigurations(settings, TranspileTestCase.varyBy); | ||
return settingConfigurations?.map(c => { | ||
const desc = Object.entries(c).map(([key, value]) => `${key}=${value}`).join(","); | ||
return new TranspileTestCase(`${justName}(${desc})`, ext, content, { ...settings, ...c }); | ||
}) ?? [new TranspileTestCase(justName, ext, content, settings)]; | ||
} | ||
|
||
private jsOutName; | ||
private dtsOutName; | ||
private units; | ||
constructor( | ||
private justName: string, | ||
private ext: string, | ||
private content: string, | ||
private settings: TestCaseParser.CompilerSettings, | ||
) { | ||
this.jsOutName = justName + this.getJsOutputExtension(`${justName}${ext}`); | ||
this.dtsOutName = justName + ts.getDeclarationEmitExtensionForPath(`${justName}${ext}`); | ||
this.units = TestCaseParser.makeUnitsFromTest(content, `${justName}${ext}`, settings); | ||
} | ||
|
||
getJsOutputExtension(name: string) { | ||
return ts.getOutputExtension(name, { jsx: this.settings.jsx === "preserve" ? ts.JsxEmit.Preserve : undefined }); | ||
} | ||
|
||
runKind(kind: TranspileKind) { | ||
it(`transpile test ${this.justName} has expected ${kind === TranspileKind.Module ? "js" : "declaration"} output`, () => { | ||
let baselineText = ""; | ||
|
||
// include inputs in output so how the test is parsed and broken down is more obvious | ||
this.units.testUnitData.forEach(unit => { | ||
baselineText += `//// [${unit.name}] ////\r\n`; | ||
baselineText += unit.content; | ||
if (!unit.content.endsWith("\n")) { | ||
baselineText += "\r\n"; | ||
} | ||
}); | ||
|
||
this.units.testUnitData.forEach(unit => { | ||
const opts: ts.CompilerOptions = {}; | ||
Compiler.setCompilerOptionsFromHarnessSetting(this.settings, opts); | ||
const result = (kind === TranspileKind.Module ? ts.transpileModule : ts.transpileDeclaration)(unit.content, { compilerOptions: opts, fileName: unit.name, reportDiagnostics: this.settings.reportDiagnostics === "true" }); | ||
|
||
baselineText += `//// [${ts.changeExtension(unit.name, kind === TranspileKind.Module ? this.getJsOutputExtension(unit.name) : ts.getDeclarationEmitExtensionForPath(unit.name))}] ////\r\n`; | ||
baselineText += result.outputText; | ||
if (!result.outputText.endsWith("\n")) { | ||
baselineText += "\r\n"; | ||
} | ||
if (result.diagnostics && result.diagnostics.length) { | ||
baselineText += "\r\n\r\n//// [Diagnostics reported]\r\n"; | ||
baselineText += Compiler.getErrorBaseline([{ content: unit.content, unitName: unit.name }], result.diagnostics, !!opts.pretty); | ||
if (!baselineText.endsWith("\n")) { | ||
baselineText += "\r\n"; | ||
} | ||
} | ||
}); | ||
|
||
Baseline.runBaseline(`transpile/${kind === TranspileKind.Module ? this.jsOutName : this.dtsOutName}`, baselineText); | ||
}); | ||
} | ||
|
||
run() { | ||
if (!this.settings.emitDeclarationOnly) { | ||
this.runKind(TranspileKind.Module); | ||
} | ||
if (this.settings.declaration) { | ||
this.runKind(TranspileKind.Declaration); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose
transpileDeclaration
truly becomes unsafe to use withoutisolatedDeclarations
because an inferredBoolean | Function
will get reduced.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without loading a full lib at least (which is doable! Just probably not worth if you're going to force on
isolatedDeclarations
anyway), yeah. Also your imports are all going to beunknown
/errorany
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specifically, this barebones lib is the strategic mini-lib we use in our test harness to silence all global checker-construction errors, with the
Symbol
addenda sounique symbol
types can actually be inferred.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be interesting to see what you end up with if you just add index signatures like
[prop: string | symbol]: any
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(probably don't do that in this PR though)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤷♂️ even if the checker is recording the diagnostics when
getEmitResolver
is called right now, nothing goes and gets the semantic errors to report them. The emit might change if you add a test case with an inferred type that's sensitive to that, though.