ts-rs icon indicating copy to clipboard operation
ts-rs copied to clipboard

Combine exports into single file

Open thorlucas opened this issue 4 years ago • 14 comments

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?

thorlucas avatar Nov 21 '21 10:11 thorlucas

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 avatar Nov 21 '21 16:11 NyxCode

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

thorlucas avatar Nov 21 '21 20:11 thorlucas

@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)

NyxCode avatar Nov 21 '21 22:11 NyxCode

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

cauthmann avatar Nov 24 '21 20:11 cauthmann

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

NyxCode avatar Nov 24 '21 20:11 NyxCode

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)

thorlucas avatar Nov 24 '21 21:11 thorlucas

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.

NyxCode avatar Nov 24 '21 21:11 NyxCode

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.

cauthmann avatar Nov 28 '21 10:11 cauthmann

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?

petersn avatar Jun 17 '22 18:06 petersn

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.

chanced avatar Nov 19 '22 14:11 chanced

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.

Andful avatar Nov 30 '22 10:11 Andful

@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 avatar Feb 29 '24 18:02 NyxCode

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.

NyxCode avatar Feb 29 '24 18:02 NyxCode

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

escritorio-gustavo avatar May 10 '24 17:05 escritorio-gustavo