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

Combine exports into single file #59

Open
thorlucas opened this issue Nov 21, 2021 · 14 comments · May be fixed by #316
Open

Combine exports into single file #59

thorlucas opened this issue Nov 21, 2021 · 14 comments · May be fixed by #316
Labels
CLI This issue/PR is related to or requires the implementation of the CLI tool enhancement New feature or request

Comments

@thorlucas
Copy link

I think that any exports within the same module should be combined into a single file, rather than splitting them up into separate files. I feel as though this should be the default, but perhaps a directive at the top of the module could indicate whether or not this happens?

@NyxCode
Copy link
Collaborator

NyxCode commented Nov 21, 2021

I would like to support this, but I'd just like to not that this is not an easy feature to implement properly.
Exporting multiple types to a single file will require some sort of coordination.

In earlier versions, there was a export! macro - You had to provide it with all types you wish to export. With that, you could export multiple types to a single file, but it had a major downside: You had to export all types in your project with a single invocation to export!. This ruined encapsulation since you had to make all your types visible from one place.

So I'm open to the idea, but we'll have to figure out how to do the neccessary coordination between the invocations of #[derive(TS)].

@NyxCode NyxCode added the enhancement New feature or request label Nov 21, 2021
@thorlucas
Copy link
Author

@NyxCode I propose that perhaps we can add a #[ts_mod] to a mod. All structs within the mod are automatically derive TS. I'll porbably take a look soon to see if I find a solution, but I haven't played around with proc too much so I'm not sure if what I'm thinking is possible.

@NyxCode
Copy link
Collaborator

NyxCode commented Nov 21, 2021

@thorlucas I was thinking of something different:
The proc macro writes to a file in the build directory (or maybe somewhere else, not yet sure).
Then, in a test (or during runtime), we traverse through the dictionary. There, we can export the bindings, allowing us to properly handle imports and naming conflicts (which would not be possible in the proc macro since we require information about other invocations)

@cauthmann
Copy link

I took a quick look at this, because I'd like to have this feature, too.

@NyxCode I propose that perhaps we can add a #[ts_mod] to a mod.

There was a recent blog entry IDEs and Macros describing the challenges for code-analyzers with non-Derive macros. What you're proposing might work, but it comes at a cost.

Then, in a test (or during runtime), we traverse through the dictionary.

The order in which tests are run is unspecified. We cannot create a test that is guaranteed to run after all others. We could let each test add their info to a static global variable, but the only way to get that to disk is to write it at the end of every single test.

Or, as you said, we write it to disk and run a bundler in a post-processing step, after cargo test finishes. The usual suspects (webpack, rollup, ...) can only output javascript, not typescript. `tsc´ can bundle, but will only output *.js and *.d.ts, not straight typescript code. I don't know how complicated it is to extend one of them, or to create another.

Yet another idea is to embrace the files, but re-export everything in an index.ts. This keeps import paths clean, and the amount of files barely matter as you're going to bundle your javascript anyway. Such an index can be created in a bash one-liner

for f in bindings/*.ts ; do echo "export * from './$f';" ; done > index.ts

@NyxCode
Copy link
Collaborator

NyxCode commented Nov 24, 2021

@cauthmann I think you misunderstood what I was saying - We could write the files in the proc macro, then have a test read those files. The test will definetely run after all proc macros are expanded.

@thorlucas
Copy link
Author

I found a bit of a workaround to do this that allows creation of a static str in the main crate with the TS definitions. This is useful for wasm bindgen as we can use a #61 TS custom section to output the TS directly to the wasm bindgen created .d.ts file. But it involves using a second proc macro crate to loop through all tagged types and combine their ::decl()s into one string.

Perhaps the ts-rs could do something like this?

(apologies for formatting im on mobile)

@NyxCode
Copy link
Collaborator

NyxCode commented Nov 24, 2021

We could write the files in the proc macro, then have a test read those files. The test will definetely run after all proc macros are expanded.

Hm, didn't think about this before, but the issue we run into with this is that after removing a #[derive(TS)] derive, the file we've written is still there.

@cauthmann
Copy link

cauthmann commented Nov 28, 2021

I think you misunderstood what I was saying - We could write the files in the proc macro, then have a test read those files. The test will definetely run after all proc macros are expanded.

Wasn't the point of the testcases that we don't have all the info inside the proc macro, like the full paths of identifiers? What information could we write to disk in a proc_macro that's useful later?

Another approach I found via an unrelated reddit discussion is linkme. It's certainly dark magic, and less portable than abusing tests, but it would allow us to catch all types from a crate and process them together.

I played around with it, and it seems to work. There's still feature-gates and a long command line to generate the bindings, but I can run a function which has access to the info from ALL types contained in the crate.

in ts_rs:

#[linkme::distributed_slice]
pub static TYPEINFO: [fn() -> (PathBuf, String)] = [..];

pub fn export_bindings() {
	println!("Got {} types", TYPEINFO.len());
	for f in TYPEINFO {
		let (path, code) = f();
		println!("Got a type: {}\n{}", path.display(), code);
	}
}

Calling ts_rs::export_bindings() from a downstream crate (with the appropriate feature gates etc set) outputs something like:

Got 3 types
Got a type: .../bindings/Foo.ts
export type Foo = number;

Got a type: .../bindings/UserId.ts
export type UserId = number;

Got a type: .../bindings/User.ts
import type { UserId } from "./UserId";

export interface User {
  user_id: UserId;
  first_name: string;
  last_name: string;
}

In a production solution, the typeinfo would return something akin to dyn TS instead of (path, code) tuples, but we cannot create a dyn TS because we don't have an instance and the trait is not object safe. That can be worked around, but I wanted to hear your opinion on the approach before doing anything serious with it.

@petersn
Copy link

petersn commented Jun 17, 2022

Second that this feature would be great. Is the current status that there's still no good way to do this, and folks aren't interested in a hacky solution of having a test bundle up all the written files?

@chanced
Copy link

chanced commented Nov 19, 2022

Having an index.ts re-export would be perfectly fine by me. For now, I'm just going to have a program collect the ts files and generate the index.ts files. It'd be great to not need that.

@Andful
Copy link

Andful commented Nov 30, 2022

From my understanding, this feature needs an API break to be implemented in a non hacky way. The current API, due to how the trait TS is defined does not allow manipulation of the types and its dependencies.

I would have an API of the form:

trait TSDef: Clone {
  fn type_id(&self) -> u64; //for checking if two TSDef are equal, maybe a better way might be the use of PartialEq
  fn name(&self) -> String;
  fn decl(&self) -> String;
  fn dependencies(&self) -> Vec<Box<dyn TSDef>>;
}
trait TS {
  type Def: TSDef;
  fn get_type_def() -> Self::Def;
}

Having an instantiable object representing a typescript type allows the construction of the entire dependency graph from a single MyType::get_type_def and the recursive call of TSDef::dependencies.

@escritorio-gustavo escritorio-gustavo linked a pull request Jan 26, 2024 that will close this issue
@NyxCode
Copy link
Collaborator

NyxCode commented Feb 29, 2024

@Andful I just came back to this issue, over a year later, and we ended up with something similar-ish.
We did not end up generating a separate type for each TS type (more codegen, generics get complicated, etc.) - But the TS trait got a dependency_types() -> impl TypeList function, making it possible to actually traverse the dependency graph of a type.

On the current master, we use this to automatically export all dependencies, though still into separate files. The main motivation behind this change was the scenario where your types contain types from a library which implement/derive TS.

@NyxCode
Copy link
Collaborator

NyxCode commented Feb 29, 2024

I still believe that, at least by default, exporting one file per type is the right choice.

Exporting into a minimal number of files while not duplicating types would only be possible if the user specifically mentioned the types in one place. With just #[derive(TS)], that amount of coordination still doesn't look feasible.

However, generating an index file which re-exports all types seems like low-hanging fruit. At some point, we'd like to introduce a CLI tool, which would be the perfect place to do that.

@escritorio-gustavo
Copy link
Collaborator

If there's still interest in this feature, please take a look at #316 and test it out! Would love to have some users search for bugs early on! Also, the CLI being developed in #304 has a --merge flag that combines all exports into a single index.ts file, so be sure to try that out as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLI This issue/PR is related to or requires the implementation of the CLI tool enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants