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

QUESTION: best way use esbuild to bundle TypeScript? #1984

Closed
gerardolima opened this issue Feb 3, 2022 · 6 comments
Closed

QUESTION: best way use esbuild to bundle TypeScript? #1984

gerardolima opened this issue Feb 3, 2022 · 6 comments

Comments

@gerardolima
Copy link

First of all, THANK YOU! Esbuild is an amazing piece of work that greatly reduces the time required for my build pipelines! Today I found a behaviour I cannot understand, though: from the same TypeScript source file (below), the code generated by compiling with tsc and then bundling with esbuild is significantly smaller than the code generated directly with esbuild. May this be caused by some misconfiguration? What should be the preferred way to build a TypeScript code base with esbuild?

// handler.ts
import {APIGatewayProxyHandler} from 'aws-lambda'
export const handler: APIGatewayProxyHandler = async (_evt, _ctx) => ({ statusCode: 200, body: 'HELLO' })
// handler-tsc.js << after compiling handler.ts, using tsc
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.handler = void 0;
const handler = async (_evt, _ctx) => ({
    statusCode: 200,
    body: 'HELLO'
});
exports.handler = handler;
// 160b :: handler-tsc-bundled.js << after bundling handler-tsc.js, using esbuild
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});exports.handler=void 0;var e=async(t,d)=>({statusCode:200,body:"HELLO"});exports.handler=e;
// 676b :: handler-bundled.mjs << after bundling handler.ts, using esbuild, directly (mjs is required)
/*global handler*/
var a=Object.defineProperty;var s=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var c=Object.prototype.hasOwnProperty;var d=o=>a(o,"__esModule",{value:!0});var l=(o,t)=>{for(var e in t)a(o,e,{get:t[e],enumerable:!0})},i=(o,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of y(t))!c.call(o,n)&&(e||n!=="default")&&a(o,n,{get:()=>t[n],enumerable:!(r=s(t,n))||r.enumerable});return o};var p=(o=>(t,e)=>o&&o.get(t)||(e=i(d({}),t,1),o&&o.set(t,e),e))(typeof WeakMap!="undefined"?new WeakMap:0);var P={};l(P,{handler:()=>x});var x=async(o,t)=>{let e=JSON.stringify(o);return console.log(e),{statusCode:200,body:e,headers:{"Content-Type":"application/json"}}};module.exports=p(P);0&&(module.exports={handler});
@evanw
Copy link
Owner

evanw commented Feb 4, 2022

This is the result of a few different things:

  • The output of TypeScript doesn't follow the specification, which says that the exports of ECMAScript modules are not supposed to be mutable. Enforcing that is more complicated than just doing exports.handler = handler.

  • A lot of cruft has crept in to esbuild's module conversion code as a result of trying to cater to various subtle needs of different parties regarding CommonJS vs. ESM, node vs. Babel, and the default and __esModule properties. See for example Node.js default interop case #532 and The export name "__esModule" is reserved and cannot be used #1591 and Adjust esbuild's handling of default exports and the __esModule marker #1849.

  • There are already a lot of edge cases for this stuff and esbuild isn't tuned for generating the smallest possible output while meeting all of its various goals. Certainly esbuild's output for this could be improved and/or made more compact, but the trade-off is that it would lead to even more edge cases and maintenance overhead. I'm not saying it's not worth working on improving this; it is. Just that it's a lot of work to make sure we don't introduce more bugs and break stuff, and also that it's not my top priority.

By far the easiest way to get the most compact code for CommonJS export is likely to just do this:

// handler.ts
import {APIGatewayProxyHandler} from 'aws-lambda'
exports.handler = async (_evt, _ctx): APIGatewayProxyHandler => ({ statusCode: 200, body: 'HELLO' })

That compiles to the following code, which is even smaller than either of your outputs:

// 61b :: after bundling handler.ts, using esbuild, directly
exports.handler=async(a,e)=>({statusCode:200,body:"HELLO"});

It also doesn't need any changes to esbuild.

@evanw
Copy link
Owner

evanw commented Feb 4, 2022

handler-bundled.mjs << after bundling handler.ts, using esbuild, directly (mjs is required)

The .mjs file extension is only for ESM code, not CommonJS code. Bundling CommonJS code into a .mjs file doesn't make sense. Are you just looking for how to bundle ESM code instead of CommonJS code? You can do that with --format=esm.

@hyrious
Copy link

hyrious commented Feb 4, 2022

I guess you're confused with these extra code injected into the final result. They are used to help reduce the interop with cjs and esm modules, like tslib. i.e. Both "importing esm code in cjs context" and "importing cjs code in esm context" will bring them in.

Most of the typescript code is in esm (since you used "import", "export"). If you bundle them to cjs (--format=cjs), which means we have to do extra work for esm → cjs. That's why __toCommonJS comes in.

The smaller result you get (run esbuild on tsc result cjs code) is cjs → cjs, so there's no extra stuff. In addition the 61b result posted by evan is the same reason. But I think no one would write ts in cjs nowadays.

@gerardolima
Copy link
Author

hey, @evanw and @hyrious, thank you for your prompt response! ❤️

About evanw's suggestion, I'm avoind exports.* on TypeScript code as tsc should transform TS import/export statements into the proper module format (I mean the tsconfig setting here). Your explanation makes the whole sense to me so I'll take this in consideration in my pipeline.

By the way, is there any normative orientation on these subleties on how to use esbuild to bundle TypeScript codebases?

@hyrious
Copy link

hyrious commented Feb 4, 2022

The esbuild only does the same thing as babel to typescript that it removes type decoration then work on js directly. It doesn't (and won't) support all tsc features. You may have a look at the docs.

Since the movement of native es modules (since node12) has been running for a while, IMHO, I suggest you to turn your code base to esm (i.e. module: esnext in tsconfig) and providing the cjs module optionally through "exports" field in package.json. Or if you really care the es5 cjs users of your library, you can still use tsc to transpile your code first.

@evanw
Copy link
Owner

evanw commented Feb 16, 2022

Closing this issue because it's not tracking a problem with esbuild.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants