-
Notifications
You must be signed in to change notification settings - Fork 342
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
chore: use rollup instead of tsup for a cleaner dist #255
Conversation
@fvsch is attempting to deploy a commit to the Toss Team on Vercel. A member of the Team first needs to authorize it. |
Hello, thanks for your pull request! This would be a great help. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, I think this is pretty solid now.
@@ -28,6 +28,7 @@ | |||
}, | |||
"files": [ | |||
"dist", | |||
"umd", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fixes publishing the umd/browser.global.js.map
sourcemap.
The package.json#browser
field will prompt npm/yarn to include umd/browser.global.js
in the package, but the related sourcemap is ignored unless explicitly included.
See also: #284
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your catch!
"tsup": "^8.1.0", | ||
"rollup": "^4.19.0", | ||
"rollup-plugin-dts": "^6.1.1", | ||
"tslib": "^2.6.3", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TypeScript compiler (used via @rollup/plugin-typescript
) required tslib
to be installed when compiling to JS (CJS or ESM).
@@ -0,0 +1,192 @@ | |||
// @ts-check |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the Rollup configuration.
Rollup uses Node to load this module, and supports .js
, .cjs
and .mjs
, but unlike Vite or tsup
it does not support TypeScript.
We can still use TypeScript types (which is quite useful for autocompletion) using // @ts-check
and types in JSDoc.
I used ESM, but I’m happy to convert to CJS if that's preferred.
import tsPlugin from '@rollup/plugin-typescript'; | ||
import dtsPlugin from 'rollup-plugin-dts'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're using rollup-plugin-dts
in addition to @rollup/plugin-typescript
because if we generate both JS outputs and declarations with @rollup/plugin-typescript
like this:
const config = {
// …
plugins: [
tsPlugin({
compilerOptions: {
declaration: true,
declarationDir: 'dist',
}
],
{
output: 'esm',
dir: 'dist',
entryFileNames: '[name].mjs',
}
});
Then we get this output:
dist/
index.mjs
index.d.ts
with the declaration file having a .d.ts
extension (instead of the desired .d.mts
). What's more, those declaration files will contain the original import specifiers from the source files, which I suspect would not work well:
// dist/index.d.ts
export * from './array/index.ts';
export * from './error/index.ts';
export * from './function/index.ts';
export * from './math/index.ts';
export * from './object/index.ts';
export * from './predicate/index.ts';
export * from './promise/index.ts';
export * from './string/index.ts';
Finally, these declaration files are emitted by the TypeScript compiler directly to disk. Which means that we cannot use Rollup configuration options, or even a simple custom output plugin, to:
- change the file extension;
- codify the file contents (in order to change the
.ts
extensions in import specifiers).
I was stuck on this problem for a long while, then found rollup-plugin-dts
which abstracts away this problem by telling tsc
to emit declaration files to a temporary folder, then reads back that output and turns it into Rollup inputs. In this process, it handles rewriting the import specifiers with our expected extensions, and we can use Rollup's output.entryFileNames
option to set the extension of the declaration files themselves.
So in total we are doing 4 different Rollup builds:
- Use
@rollup/plugin-typescript
to emit ESM code (dist/**/*.mjs
). - Use
@rollup/plugin-typescript
to emit CJS bundles (dist/**/*.js
). - Use
rollup-plugin-dts
to emit declaration files and translate those into both.d.ts
and.d.mts
with correct import specifiers (.js
and.mjs
). - Use
@rollup/plugin-typescript
and@rollup/plugin-terser
to emitumd/browser.global.js
.
There might be a better solution than using rollup-plugin-dts
here, but I haven't found it. The TypeScript compiler itself is responsible for a lot of the difficulty here, since it's a powerful but somewhat inflexible and stubborn piece of software (very Microsoftian ^^).
browserBuildConfig({ | ||
inputFile: './src/compat/index.ts', | ||
outFile: packageJson.publishConfig.browser, | ||
name: '_', | ||
}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use Rollup to generate the umd/browser.global.js
bundle using src/compat/index.ts
as an entrypoint, without needing an extra src/browser.ts
module.
Here, the { output: { format: 'iife', name: '_' } }
configuration will produce a file looking like:
var _ = function (t) {
"use strict";
// ... implementation, sets t.functionName = ...
}({});
Note that the current src/browser.ts
(removed in this PR) adds a _
property to window
instead:
window._ = toolkit;
This src/browser.ts
creates a couple problems:
- It gets picked up by the ESM and CJS builds, and produces unnecessary cruft in the
dist
output (see https://unpkg.com/browse/es-toolkit@1.13.1/dist/):
File | Size |
---|---|
dist/browser.d.mts | 13 B |
dist/browser.d.ts | 13 B |
dist/browser.js | 31.9 kB |
dist/browser.js.map | 156 kB |
dist/browser.mjs | 3.3 kB |
dist/browser.mjs.map | 360 B |
Solutions include:
- Keep
src/browser.ts
and add ignore/exclusion rules intsconfig.json
and/or bundler config. - Or remove
src/browser.ts
completely and use the bundler to achieve the desired "declare a global var" result.
- Using
window._ = /* toolkit */
will throw in a Web Worker context (e.g. if loading the browser bundle withimportScripts
). Solutions include:- using
var _ = /* toolkit */
- using
self._ = /* toolkit */
- using
clearDir('dist'); | ||
clearDir('umd'); | ||
|
||
const entrypoints = Object.values(packageJson.exports).filter(f => /^(\.\/)?src\//.test(f) && f.endsWith('.ts')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After a bunch of testing: using the paths from package.json#exports
(the dev version pointing to the src
directory) as inputs to Rollup produces the best results:
- The whole module tree gets resolved correctly.
- This avoids pulling in test files, and modules not marked as test files but only consumed by tests (e.g.
src/compat/_internal/*.ts
).
Another approach is to use something like globSync('src/**/*.ts')
and then do a lot of filtering, but I found that it's easy to forget something while filtering.
import * as toolkit from './compat/index.ts'; | ||
|
||
interface Window { | ||
_: typeof toolkit; | ||
} | ||
|
||
declare let window: Window; | ||
window._ = toolkit; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As described above, this is now handled with a Rollup build that looks like:
const config = {
input: 'src/compat/index.ts',
output: {
format: 'iife',
name: '_',
file: 'umd/browser.global.js',
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is decent work. Huge thanks to @fvsch !
Fixes #216.
This PR removes
tsup
and adds an equivalent Rollup configuration, in order to improve the structure and verbosity of thedist
output.Issues I’m trying to solve
ESM build
dist/chunk-{hash}.mjs
, often one peres-toolkit
function, leading to hard-to-browsedist
folder and a deeper-than-necessary module graph (dist/index.mjs -> dist/array/index.mjs -> dist/array/unionWith.mjs -> dist/chunk-OG2RLT5U.mjs
).dist/browser.mjs
(which duplicatesumd/browser.global.js
and is not exposed inpackage.json#exports
).CJS build
dist/index.js
is a full bundle of the whole toolkit, instead of importing from the rest of the CJS output.dist/{category}/index.js
are bundles for those categories.dist/{category}/{functionName}.js
are also produced (along with their sourcemaps), but those files are not useddist/{category}/{functionName}.js
files and their sourcemaps, but those files are never used (becausepackage.json#exports
points to thedist/{category}/index.js
files, which are bundles and don't requiredist/{category}/{functionName}.js
).dist/browser.js
(which duplicatesumd/browser.global.js
and is not exposed inpackage.json#exports
).Proposed
dist
layoutThings look maybe a bit easier to parse if we filter out by format. Here is the output structure for ESM:
and for CJS:
Note: for CJS there is some bundling going on, while for ESM we try to output a transpiled copy of the
src
dir (without tests) that mirrors its structure. I opted for this because that's what is going on currently withtsup
(once you fix some useless files and some excessive indirection created by chunks).It should be possible to do different things though:
{category}/index
bundles of all the functions for that category, in both CJS and ESM builds.src
module graph ({category}/index
entrypoints import function implementations from{category}/{fn}
).Results of this PR
dist
output that mimics the current output, minus the outlined issues.package.tgz
:tsup
.yarn pack
withtsup
takes 3-4 seconds, and with Rollup it takes 4-5 seconds.Other concerns
Choice of bundler
The choice of bundler or transpiler is conditioned by the characteristics of this project:
.ts
extensions in import specifiers..js
(CJS) and.mjs
(ESM) builds.This is a problem for both
tsc
andesbuild
, since both have limitations (by design or de facto) when it comes to modifying extensions used in import specifiers.With ESBuild, this can be worked around by using ESBuild to follow the whole module graph, then do code splitting to have it explode the module graph into many files. This looks like what
tsup
is configuring ESBuild to do. As a result the output has the configured extensions (.js
or.mjs
) in import specifiers, but a side effect is that we end up with most code in chunks atdist/chunk-[hash].mjs
.It looks like there is no way to get
tsup
and ESBuild to fulfill the requirements of this project.When it comes to alternatives:
tsup
), but can be configured to control the output more closely through its options, existing plugins, or even custom plugins if need be.swc
, Parcel, or others.Duplication of code
With the 3 builds for this project (ESM, CJS, and the IIFE build in
umd/browser.global.js
), we end up with a lot of verbatim copies of the same code:TypeScript declaration files for CJS and ESM have identical content.
dist/array/difference.d.ts
anddist/array/difference.d.mts
are identical.JSDoc comments for a function end up duplicated in many files.
difference
function insrc/array/difference.ts
ends up copied verbatim into:removeComments
option to not output comments in the.js
/.mjs
outputs, or in the.d.ts
/.d.mts
outputs. My intuition is that having JSDoc comments in the TypeScript declaration files only should be enough, but there might be IDEs or configurations where the IDE doesn't use the TypeScript declaration files but does use JSDoc comments from the imported.js
/.mjs
sources?tsup
, and it looks like comments are kept in the declaration files but stripped in ESM/CJS modules. I'll update this PR to do the same.Duplication of the original TypeScript sources:
Testing the output
I checked the generated
package.tgz
with https://arethetypeswrong.github.io/ and everything is green.I would still like to verify the changes in this PR further:
@arethetypeswrong/cli
on CI?