denoland / deno_emit

Transpile and bundle JavaScript and TypeScript under Deno and Deno Deploy
https://jsr.io/@deno/emit
MIT License
225 stars 23 forks source link

Provide option to avoid tree walking during transpilation #133

Open jordanbtucker opened 1 year ago

jordanbtucker commented 1 year ago

I have a use case where I do not want deno_emit to walk the import tree when transpiling. When I tell it to transpile index.ts I only want it to give me the JS for index.ts, but not the JS for everything index.ts imports recursively.

My use case is that I don't want to have to specify which files are "roots" or "entry points" for transpiling. I want to say "transpile all TS files in this directory" so I can output them to another folder as modules rather than bundles.

Currently, I have three workarounds.

  1. Walk the directory, call transpile for each TS file, pick out the current file from the returned map. This has the unfortunate side-effect of performing many redundant transpilations.
  2. Create a main map, walk the directory, if the current file already exists in the main map, skip it, otherwise call transpile on the current TS file and add its results to the main map. This is more efficient, but it still walks the entire import tree.
  3. Walk the directory, call transpile for each TS file, provide a load function that returns { kind: "module", specifier, content } for imports that match the current entry and { kind: "external", specifier } for those that don't, which seems to exclude it from the map. This is complicated and requires implementing something that could be built in, but it does prevent walking the entire import tree, so it's the most efficient.
yacinehmito commented 1 year ago

This is complicated and requires implementing something that could be built in, but it does prevent walking the entire import tree, so it's the most efficient.

This seems to cover your use case very well. Is there a compelling reason to make this behaviour built-in?

Can you explain your use-case a bit more? Maybe this is something that we can expect other people to need, but it is hard to tell until we know the why behind the use case.

jordanbtucker commented 1 year ago

There are two use cases here.

  1. Creating a JS library for browsers using Deno's tooling and ecosystem. While it may be standard to have one mod.ts file that re-exports everything, that isn't the only way to do things. In fact, I think that's bad practice for browser modules because it forces the browser to fetch all dependencies even if they're not needed for the current task. I'd rather just give deno_emit one directory and have it transpile all files inside it.
  2. Creating a full-stack Deno web app. The browser scripts need to be transpiled from TS to JS, however I don't want to worry about which scripts are going to be entry points. Again, I just want to give deno_emit a directory and transpile all the files in it.

I suppose this could also apply to any project that has multiple entry points that rely on common code. For example, foo.ts ultimately relies on common.ts, and bar.ts ultimately relies on common.ts too. Both foo.ts and bar.ts are entry points, but since they both rely on common.ts, common.ts will get transpiled twice.

The behavior I'm proposing also mirrors tools like tsc and babel, which are transpilers, rather than webpack and esbuild, which are bundlers. In other words, it makes sense for deno_emit's bundle to accept entry points and walk the tree, but it doesn't make sense to me to have transpile walk the tree.

benStre commented 1 year ago

We also faced this issue in our project and implemented a transpileIsolated function in our fork of deno_emit, which provides this exact functionality.

As an alternative, we also created a standalone TS transpiler based on deno_ast/deno_graph, that takes TS module code as an input and returns the transpiled JS code.

jordanbtucker commented 6 months ago

I just wanted to follow up on this to see if this is something that sounds worthwhile to implement. I also want to emphasize that this behavior would mirror other transpilers like TypeScript's tsc and Babel.js.

jordanbtucker commented 5 months ago

Chiming in again to say that I've been investigating Bun as an alternative since its Transpiler does not walk the tree.

Bun has Bun.build for bundling, which walks the tree, and Bun.Transpiler, which is a pure transpiler with the ability to replace and manipulate imports and exports.

yacinehmito commented 5 months ago

What are the limitations of the third workaround you proposed? It seems like it only takes a few lines of code. Is there something I am not seeing?