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 transpileDeclaration API method #58261

Merged
merged 8 commits into from
Apr 24, 2024

Conversation

weswigham
Copy link
Member

With an identical signature to transpileModule, it differs only in that it fetches the .d.ts output for the given input, instead of .js, and in that it forces declaration, emitDeclarationOnly, and isolatedDeclarations on.

Do note, this is the... dummiest? simplest? possible version of this API - you can make a transpileDeclaration that actually loads the whole program graph and does not require isolatedDeclarations-compatible input to ensure reasonable output, but such a version is only practical (as far as most people's performance concerns go) once noCheck is merged, which will also help this and transpileModule avoid unnecessary work they're currently doing.

From the infrastructure side, of interest to most people working with isolatedDeclarations, this PR also offers a new transpile test runner, which runs compiler-like tests through the transpileModule and transpileDeclaration APIs (as appropriate for the options in the test) instead of the full compiler ones.

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Apr 19, 2024
@typescript-bot
Copy link
Collaborator

Looks like you're introducing a change to the public API surface area. If this includes breaking changes, please document them on our wiki's API Breaking Changes page.

Also, please make sure @DanielRosenwasser and @RyanCavanaugh are aware of the changes, just as a heads up.

@@ -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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a mega good idea

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we have time, we should port the bulk of the existing transpile unit tests to this runner, probably. (The ones that don't pass in a custom transformer, at least.)

I also have a backlogged idea to make a runner for tscWatch-type tests, because the incredible boilerplate for those gets me every time I write one; just need to figure out a good way to encode the edits you usually want to apply in sequence into the input compiler-test-type fs description.

// 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 {}
Copy link
Member

@DanielRosenwasser DanielRosenwasser Apr 19, 2024

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 without isolatedDeclarations because an inferred Boolean | Function will get reduced.

Copy link
Member Author

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 be unknown/error any.

Copy link
Member Author

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 so unique symbol types can actually be inferred.

Copy link
Member

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

Copy link
Member

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)

Copy link
Member Author

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.

==== class.ts (1 errors) ====
const i = Symbol();
~
!!! error TS9010: Variable must have an explicit type annotation with --isolatedDeclarations.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure this specific pattern (and the Symbol.for variant) is on the backlog of things to remove the isolatedDeclarations error from, right? Since unique symbols are so hard to use without const x = Symbol() allowing an inferred unique symbol type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes; the problem is symbol freshness/widening. I tried my hand at fixing it but was unsuccessful. See also #55943 #55901

src/testRunner/transpileRunner.ts Outdated Show resolved Hide resolved
src/testRunner/transpileRunner.ts Outdated Show resolved Hide resolved
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);

// TODO: Should this require `reportDiagnostics`?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it the case that the JS emit pipeline never reports errors?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct - emit pipeline errors were added for normal declaration emit when we ported them to transforms (and they add visibility errors very late) and are unused by js emit transforms -isolatedDeclarations continues to use the mechanism extensively. It's a longstanding issue I've wanted to fix, since it means we can't get declaration emit errors without actually running declaration emit (remind yourself how getPreEmitDiagnostics works some time 😑), but it's super annoying to try and decouple the errors from the node generation step.

@fatcerberus
Copy link

The name of transpileModule makes sense because it transpiles TS -> JS. transpileDeclarations is just nonsense, imo (extracting metadata is not usually considered "transpiling")

I don't have a suggestion for a better name though 😅

@jakebailey
Copy link
Member

I don't have a suggestion for a better name though 😅

I feel like we also discussed just having it be transpileModule with a flag set such that the output file is a d.ts. But module is also a misnomer, as declaration emit (even with isolated declarations enabled) works on non-modules too!

interface Symbol {
readonly [Symbol.toStringTag]: string;
}`;
const barebonesLibName = "lib.d.ts";
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, it's weirder that we can't typecheck unique symbol literals/calls without the skeleton of the global Symbol definitions present (since it's not like symbol types are ES6+ specific). That's probably the real underlying issue that needs fixing - we really aughta bake their presence into the checker like undefined if they're so central to checking a basic type-syntax construct. Though, there's probably some other annoying things, too, though - like I don't think we'll emit Omit types for generic spreads without the global Omit type present, which even this is currently missing. Eg,

export function f<T extends {x: number, y: number}>(x: T) {
    const {x: num, ...rest} = x;
    return rest;
}

has an inferred return type of Omit<T, "x">, but without Omit in the lib, it's just any (the error kind). There are a lot of "builtins" like this, whose fallback behavior when they aren't present isn't particularly good.

Personally, by default I think I'd rather load the fully correct lib for the options passed in, at least for transpileDeclaration, just because I think emitting export const a: any for export const a = Math.random() feels bad (even if it is an error under ID), but I don't know if that runs counter to the perf goals of the API (even if every invocation with the same lib could share cached lib AST copies). Still, I guess it comes down to a question of scope - loading a real lib to get the builtins to work is easy, but should you need to do that for isolatedDeclarations-compatible input? I appended this barebones lib to the input because I assumed at least unique symbol-related inferences are planned to work, and this is the minimal lib (well, OK, I could omit the empty Array interfaces and the like) to get that.

We could leave it up to the caller - and load the default lib for the options provided unless an override (like this minimal one or a completely empty/missing one) is passed in, or load no lib at all by default unless one is passed in as a transpile option. It's really a question of the intended usage, I suppose.

Copy link
Member

@jakebailey jakebailey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming that we are reporting diagnostics for isolatedDeclarations so people can't misuse the API (which in the design meeting you said was true), this all looks good to me as a start if we want to get feedback early for the beta and this makes it in. I do think that we need to figure out something better for the lib / symbols, but this seems okay to start.

@weswigham
Copy link
Member Author

Assuming that we are reporting diagnostics for isolatedDeclarations

Yup, you can already see the errors in the .d.ts baselines, when emitted.

@weswigham weswigham merged commit 0b71b81 into microsoft:main Apr 24, 2024
25 checks passed
@brandonmcconnell
Copy link

Congrats, this is an exciting development!

Will this also support .d.cts, as checked by attw?

@jakebailey
Copy link
Member

It should work with any TS file if it passes isolated declarations, but you're only going to get d.cts output from a .cts file input. Isolated declarations and transpileDeclaration don't really have anything to do with attw or module resolution.


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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think we should set forceDtsEmit this one generates d.ts file even if declaration options are not set correctly and thats not what we are looking for right? It also generates output even if there are diagnostics. is that intentional.

Eg normally d.ts is not generated for json but this will generate it and i dont think we want that.

Copy link
Member Author

@weswigham weswigham Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that intentional.

Yes. transpileModule only fails (with a debug assert!) when the output isn't emitted at all, and will happily emit buggy output alongside diagnostics (if you ask for them). This keeps that behavior aligned between the two.

forceDtsEmit this one generates d.ts file even if declaration options are not set correctly and thats not what we are looking for right?

We set those options right above. We need to set forceDtsEmit so we still get output when the input has a parse error (because being able to return those errors is contingent on there being output at all).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eg normally d.ts is not generated for json but this will generate it and i dont think we want that.

Honestly, if someone explicitly passes json into this API, it's because they wanted a declaration file for it.

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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

@bigbigDreamer
Copy link

Hi, I have a little confusion. Maybe it's not a problem. I hope someone can answer it. Thank you very much.

image

Here, since the transpileOptions are all optional parameters, why not directly set the second parameter as optional? This way, it won't lead to an error in the downstream when the upstream doesn't pass a parameter.

-function transpileDeclaration(input: string, transpileOptions: TranspileOptions): TranspileOutput;
+function transpileDeclaration(input: string, transpileOptions?: TranspileOptions): TranspileOutput;
image

The above is the problem I encountered when using transpileDeclaration. I'm not quite sure if the second parameter must be passed as an empty object. Maybe my question is a bit strange. If it bothers you, I'm deeply sorry. I'm just a little confused.

@jakebailey
Copy link
Member

jakebailey commented Jul 15, 2024

You most definitely are going to want to call this API with parameters, if only to ensure the compiler options are as expected. Notably strict is not the default, so if you don't pass in parameters, you will emit things like "undefined" incorrectly. An empty object is a bad idea for that reason.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants