Skip to content

Conversation

@radist2s
Copy link

@radist2s radist2s commented Nov 2, 2025

The main motivation for this PR is to achieve better tree-shaking and optimize bundle size for consumers. To accomplish this, the package now supports both ESM and CommonJS formats, exporting .mjs and .js files as well as explicit type-only exports for maximum compatibility with modern bundlers and tooling.

What's Changed

  • Added tsdown for building both ESM and CommonJS formats
  • Configured package.json with proper exports field for conditional exports
  • Configured sideEffects for optimal tree-shaking support
  • Converted all type exports to explicit export type syntax
  • Added proper repository URL format

Benefits

  • ESM & CJS support: Works natively with both modern (ESM) and legacy (CommonJS) bundlers.
  • Better tree-shaking and optimized builds.
  • No breaking changes for existing users.

Testing

All testing was performed from a separate downstream project (not in the monorepo itself): after publishing the package to a local registry, it was installed and consumed in an external test project.

  • ✅ All existing Jest tests in that external project pass, validating CommonJS format compatibility
  • ✅ Webpack (also in the external project) successfully consumes the ESM build
  • ✅ Type checking with tsc passes from downstream code
  • ✅ Both .mjs and .js files are generated and work as expected when imported in a real project

Breaking Changes

None. This change is fully backward compatible. Existing CommonJS imports will continue to work as before, and the package now additionally supports ESM imports for modern tooling.

@anton-trunov
Copy link

@codex review

Copy link
Collaborator

@verytactical verytactical left a comment

Choose a reason for hiding this comment

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

Please consult the newly added CONTRIBUTING.md.

"ton3-core": "^0.0.20",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"tsdown": "^0.15.12",
Copy link
Collaborator

Choose a reason for hiding this comment

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

tsdown is not stable enough to be used in a security- and safety-critical project.

Copy link
Author

Choose a reason for hiding this comment

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

@verytactical, I understand your concern, and I will not defend this decision (I used it as the simplest option here to get a result). What do you think about bundling using Rollup with rollup-plugin-dts?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I thought about sticking to plain old tsc, and running it twice.

Do we really need anything that advanced?

Copy link
Author

Choose a reason for hiding this comment

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

I would prefer to use bare tsc myself, but at the build stage, renaming files to .mjs for ESM is mandatory.
You could use the rewriteRelativeImportExtensions option from TypeScript 5.7, but then all imports must include the .ts extension:

import { BitString } from './boc/BitString.ts';

In other words, if building for CommonJS and ESM were possible using bare tsc, projects such as tsup, rolldown, and rollup-plugin-dts would never have been created.

Copy link
Author

Choose a reason for hiding this comment

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

I should also mention that there is another way to use .js files for both CommonJS and ESM in a bundle. This is "Subpath exports with multiple package.json". It's a bit old-fashioned, but it's still well supported by bundlers and native to Node.js. I will prepare the changes later and show what happens. The setup will be more complicated, but it will be possible to leave only tsc.

Copy link
Collaborator

@verytactical verytactical Dec 3, 2025

Choose a reason for hiding this comment

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

Sorry, I forgot to put meeting notes from the call with @radist2s.

The main problem here is that this change is inherently backwards incompatible: users would have to configure their build tools to select the exact desired build of the package and its transitive dependencies. It's very likely someone will just update the library, and get into a tarpit of debugging their build.

Worse, we create two separate versions of the library, albeit in the same version of the same npm package. This means that even if there is a single version of the @ton/core in a dependency tree, we might load both ESM and CJS versions at the same time, and suffer from instanceof problems.

So, even if we're ready to ignore this kind of DX problems that ensue from ESM/CJS hybrid libraries, we'd still have to release a new major version. And that version is 1, which means we're publicly promising to keep backward compatibility on all the interfaces, and pretty much all of them are really not good at the moment.

Choose a reason for hiding this comment

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

According to semver we can break stuff in minor versions prior to 1.0.0

Probably we should ditch all the instanceof checks and replace it with Symbol.for tags. I've seen the approach to fix the same problem from @preact/signals guys

Choose a reason for hiding this comment

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

According to semver we can break stuff in minor versions prior to 1.0.0

Probably we should ditch all the instanceof checks and replace it with Symbol.for tags. I've seen the approach to fix the same problem from @preact/signals guys

Copy link
Collaborator

Choose a reason for hiding this comment

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

Well, I wish it was our instanceof, not in dependents. That'd make it much easier :)

Copy link
Author

Choose a reason for hiding this comment

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

@verytactical, unfortunately, I have not yet completed the compatibility study we agreed upon. I was urgently transferred to another project and asked not to be distracted until its release. I will return to this issue any day now.

"./package.json": "./package.json"
},
"sideEffects": [
"./src/boc/Cell.ts",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why these files have to be in sideEffects?

Neither Cell nor Bitstring should have any global state in them.

Copy link
Author

Choose a reason for hiding this comment

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

If we strictly follow the rules, then the structures of the form

static readonly EMPTY = new BitString(Buffer.alloc(0), 0, 0);

are side effects. But for the cases mentioned, I must agree that there will be no side effects. I will delete it if it raises questions, and change sideEffects to false.

Choose a reason for hiding this comment

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

This declaration is actually side effect free. It just creates dependency chain from the other file, but the file itself can be totally removed to optimize bundle size

@radist2s radist2s marked this pull request as draft November 3, 2025 08:34
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

Successfully merging this pull request may close these issues.

4 participants