Skip to content

Commit 1a0e21f

Browse files
authored
feat!: transform individual lodash.method package imports (#499)
1 parent 3f3dd5b commit 1a0e21f

23 files changed

+923
-246
lines changed

.changeset/bright-colts-try.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@optimize-lodash/esbuild-plugin": major
3+
"@optimize-lodash/rollup-plugin": major
4+
"@optimize-lodash/transform": major
5+
---
6+
7+
Rewrite ["modularized" lodash packages](https://www.npmjs.com/search?q=keywords%3Alodash-modularized) such as `lodash.isnil` / `lodash.camelcase` / `lodash.clonedeep` to use the standard `lodash` / `lodash-es` packages. This enables tree-shaking of modularized imports for significant size savings.
8+
9+
This feature can be disabled by setting `optimizeModularizedImports` to `false` (it is on by default).

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
"devDependencies": {
88
"@changesets/changelog-github": "0.5.1",
9-
"@changesets/cli": "2.27.12"
9+
"@changesets/cli": "2.29.7"
1010
},
1111
"pnpm": {
1212
"peerDependencyRules": {

packages/esbuild-plugin/README.md

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ _**This is a proof of concept! esbuild loader plugins are "greedy" and need addi
1212

1313
There are [multiple](https://github.com/webpack/webpack/issues/6925) [issues](https://github.com/lodash/lodash/issues/3839) [surrounding](https://github.com/rollup/rollup/issues/1403) [tree-shaking](https://github.com/rollup/rollup/issues/691) of lodash. Minifiers, even with dead-code elimination, cannot currently solve this problem. With this plugin, bundled code output will _only_ include the specific lodash methods your code requires.
1414

15-
There is also an option to use [lodash-es](https://www.npmjs.com/package/lodash-es) for projects which ship CommonJS and ES builds: the ES build will be transformed to import from `lodash-es`.
15+
There is also an option to use [lodash-es](https://www.npmjs.com/package/lodash-es) for projects which ship ESM: transform all your `lodash` imports to use `lodash-es` which is tree-shakable.
1616

1717
Versions of this plugin _before_ 3.x did not support Typescript. 3.x and later support Typescript, although Typescript support is considered experimental.
1818

@@ -21,6 +21,7 @@ Versions of this plugin _before_ 3.x did not support Typescript. 3.x and later s
2121
```javascript
2222
import { isNil, isString } from "lodash";
2323
import { padStart as padStartFp } from "lodash/fp";
24+
import kebabCase from "lodash.kebabcase";
2425
```
2526

2627
### Becomes this output
@@ -29,32 +30,63 @@ import { padStart as padStartFp } from "lodash/fp";
2930
import isNil from "lodash/isNil.js";
3031
import isString from "lodash/isString.js";
3132
import padStartFp from "lodash/fp/padStart.js";
33+
import kebabCase from "lodash/kebabCase.js";
3234
```
3335

3436
## `useLodashEs` for ES Module Output
3537

36-
While `lodash-es` is not usable from CommonJS modules, some projects use Rollup to create two outputs: one for ES and one for CommonJS.
38+
While `lodash-es` is not usable in CommonJS modules, some projects only need ESM output or build both CommonJS and ESM outputs.
3739

38-
In this case, you can offer your users the best of both:
40+
In these cases, you can optimize by transforming `lodash` imports to `lodash-es` imports:
3941

4042
### Your source input
4143

4244
```javascript
4345
import { isNil } from "lodash";
46+
import kebabCase from "lodash.kebabcase";
4447
```
4548

4649
#### CommonJS output
4750

4851
```javascript
4952
import isNil from "lodash/isNil.js";
53+
import kebabCase from "lodash/kebabCase.js";
5054
```
5155

5256
#### ES output (with `useLodashEs: true`)
5357

5458
```javascript
5559
import { isNil } from "lodash-es";
60+
import { kebabCase } from "lodash-es";
5661
```
5762

63+
## Individual `lodash.*` Method Packages
64+
65+
Imports from individual lodash method packages like `lodash.isnil` or `lodash.flattendeep` are transformed to use the optimized import path of `lodash` or `lodash-es`, consolidating your lodash usage to a single, tree-shakable ESM package.
66+
67+
### Your source input
68+
69+
```javascript
70+
import isNil from "lodash.isnil";
71+
import flattenDeep from "lodash.flattendeep";
72+
```
73+
74+
#### CommonJS output
75+
76+
```javascript
77+
import isNil from "lodash/isNil.js";
78+
import flattenDeep from "lodash/flattenDeep.js";
79+
```
80+
81+
#### ES output (with `useLodashEs: true`)
82+
83+
```javascript
84+
import { isNil } from "lodash-es";
85+
import { flattenDeep } from "lodash-es";
86+
```
87+
88+
Aliased local names are supported (`import checkNull from "lodash.isnil"` becomes `import checkNull from "lodash/isNil.js"`).
89+
5890
## Usage
5991

6092
_Please see the [esbuild docs for the most up to date info on using plugins](https://esbuild.github.io/plugins/#using-plugins)._
@@ -89,6 +121,15 @@ If `true`, the plugin will append `.js` to the end of CommonJS lodash imports.
89121

90122
Set to `false` if you don't want the `.js` suffix added (prior to v2.x, this was the default).
91123

124+
### `optimizeModularizedImports`
125+
126+
Type: `boolean`<br>
127+
Default: `true`
128+
129+
When `true`, imports from individual lodash method packages (e.g., `lodash.isnil`, `lodash.kebabcase`) are transformed to optimized imports from `lodash` or `lodash-es`.
130+
131+
Set to `false` if you need to disable this behavior (prior to 6.x, this transformation did not ooccur).
132+
92133
## Limitations
93134

94135
### Default imports are not optimized

packages/esbuild-plugin/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,19 @@ const isTypescriptPath = (path: string): boolean =>
5555
export type PluginOptions = {
5656
useLodashEs?: true;
5757
appendDotJs?: boolean;
58+
/**
59+
* Default: true. When true, imports from individual lodash method packages
60+
* (e.g., lodash.isnil, lodash.kebabcase) are transformed to optimized imports.
61+
* Set to false to disable this behavior.
62+
*/
63+
optimizeModularizedImports?: boolean;
5864
};
5965

6066
// TODO: filter https://golang.org/pkg/regexp/
6167
export function lodashOptimizeImports({
6268
useLodashEs,
6369
appendDotJs = true,
70+
optimizeModularizedImports,
6471
}: PluginOptions = {}): Plugin {
6572
const cache = new Map<
6673
string,
@@ -94,6 +101,7 @@ export function lodashOptimizeImports({
94101
: wrappedParse,
95102
useLodashEs,
96103
appendDotJs,
104+
optimizeModularizedImports,
97105
});
98106
if (result === UNCHANGED) {
99107
cache.set(path, { input, output: UNCHANGED });

packages/rollup-plugin/README.md

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,25 @@
33
[![npm](https://img.shields.io/npm/v/@optimize-lodash/rollup-plugin)](https://www.npmjs.com/package/@optimize-lodash/rollup-plugin)
44
![node-current](https://img.shields.io/node/v/@optimize-lodash/rollup-plugin)
55
![npm peer dependency version](https://img.shields.io/npm/dependency-version/@optimize-lodash/rollup-plugin/peer/rollup)
6-
![compatible with Vite 3.x](https://img.shields.io/badge/vite-%3E%3D3.x-blue)
6+
![compatible with Vite](https://img.shields.io/badge/vite-%3E%3D3.x-blue)
77
![compatible with rolldown](https://img.shields.io/badge/rolldown-compatible-blue)
88
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/kyle-johnson/rollup-plugin-optimize-lodash-imports/main.yml?branch=main)](https://github.com/kyle-johnson/rollup-plugin-optimize-lodash-imports/actions)
99
[![license](https://img.shields.io/npm/l/@optimize-lodash/rollup-plugin)](https://github.com/kyle-johnson/rollup-plugin-optimize-lodash-imports/blob/main/packages/rollup-plugin/LICENSE)
1010
[![Codecov](https://img.shields.io/codecov/c/github/kyle-johnson/rollup-plugin-optimize-lodash-imports?flag=rollup-plugin&label=coverage)](https://app.codecov.io/gh/kyle-johnson/rollup-plugin-optimize-lodash-imports/)
1111
![GitHub last commit](https://img.shields.io/github/last-commit/kyle-johnson/rollup-plugin-optimize-lodash-imports)
1212

13-
There are [multiple](https://github.com/webpack/webpack/issues/6925) [issues](https://github.com/lodash/lodash/issues/3839) [surrounding](https://github.com/rollup/rollup/issues/1403) [tree-shaking](https://github.com/rollup/rollup/issues/691) of lodash. Minifiers, even with dead-code elimination, cannot currently solve this problem. Check out the test showing that even with terser as a minifier, [this plugin can still reduce bundle size by 70%](https://github.com/kyle-johnson/rollup-plugin-optimize-lodash-imports/blob/main/packages/rollup-plugin/tests/bundle-size.test.ts) for [an example input](https://github.com/kyle-johnson/rollup-plugin-optimize-lodash-imports/blob/main/packages/rollup-plugin/tests/fixtures/standard-and-fp.js). With this plugin, bundled code output will _only_ include the specific lodash methods your code requires.
13+
There are [multiple](https://github.com/webpack/webpack/issues/6925) [issues](https://github.com/lodash/lodash/issues/3839) [surrounding](https://github.com/rollup/rollup/issues/1403) [tree-shaking](https://github.com/rollup/rollup/issues/691) of lodash. Minifiers, even with dead-code elimination, cannot currently solve this problem. Check out the test showing [this plugin can reduce bundle size by 70%](https://github.com/kyle-johnson/rollup-plugin-optimize-lodash-imports/blob/main/packages/rollup-plugin/tests/bundle-size.test.ts) for [an example input](https://github.com/kyle-johnson/rollup-plugin-optimize-lodash-imports/blob/main/packages/rollup-plugin/tests/fixtures/standard-and-fp.js) (and that's with a minifier enabled!). With this plugin, bundled code output will _only_ include the specific lodash methods your code requires.
1414

15-
There is also an option to use [lodash-es](https://www.npmjs.com/package/lodash-es) for projects which ship CommonJS and ES builds: the ES build will be transformed to import from `lodash-es`.
15+
There is also an option to use [lodash-es](https://www.npmjs.com/package/lodash-es) for projects which ship ESM: transform all your `lodash` imports to use `lodash-es` which is tree-shakable.
1616

17-
Note: versions of this plugin prior to 5.x supported NodeJS 12 and Rollup 2.x - 3.x. If you need support for these older versions, please use the 4.x release. [Rollup 4.x contains significant performance improvements](https://github.com/rollup/rollup/releases/tag/v4.0.0) over previous versions and is highly recommended.
17+
Version 6.x introduces a new feature: imports of ["modularized" lodash packages](https://www.npmjs.com/search?q=keywords%3Alodash-modularized), such as [`lodash.camelcase`](https://www.npmjs.com/package/lodash.camelcase) are rewritten to use optimized imports of `lodash` or `lodash-es`. This can _significantly_ reduce bundle size when using third-party dependencies that require these modularized imports: you no longer have to ship copies of the same lodash functions simply because one comes from `lodash` and another from `lodash.camelcase`. Applying this to [`kebabCase`](https://github.com/kyle-johnson/rollup-plugin-optimize-lodash-imports/blob/a05bd96a451cf2b4eb7065c83bfe25969ab5282a/packages/rollup-plugin/tests/fixtures/mixed-lodash.js) saves ~3.5kB after minification.
1818

1919
### This input
2020

2121
```javascript
2222
import { isNil, isString } from "lodash";
2323
import { padStart as padStartFp } from "lodash/fp";
24+
import kebabCase from "lodash.kebabcase";
2425
```
2526

2627
### Becomes this output
@@ -29,32 +30,63 @@ import { padStart as padStartFp } from "lodash/fp";
2930
import isNil from "lodash/isNil.js";
3031
import isString from "lodash/isString.js";
3132
import padStartFp from "lodash/fp/padStart.js";
33+
import kebabCase from "lodash/kebabCase.js";
3234
```
3335

3436
## `useLodashEs` for ES Module Output
3537

36-
While `lodash-es` is not usable from CommonJS modules, some projects use Rollup to create two outputs: one for ES and one for CommonJS.
38+
While `lodash-es` is not usable in CommonJS modules, some projects only need ESM output or build both CommonJS and ESM outputs.
3739

38-
In this case, you can offer your users the best of both:
40+
In these cases, you can optimize by transforming `lodash` imports to `lodash-es` imports:
3941

4042
### Your source input
4143

4244
```javascript
4345
import { isNil } from "lodash";
46+
import kebabCase from "lodash.kebabcase";
4447
```
4548

4649
#### CommonJS output
4750

4851
```javascript
4952
import isNil from "lodash/isNil.js";
53+
import kebabCase from "lodash/kebabCase.js";
5054
```
5155

5256
#### ES output (with `useLodashEs: true`)
5357

5458
```javascript
5559
import { isNil } from "lodash-es";
60+
import { kebabCase } from "lodash-es";
5661
```
5762

63+
## Individual `lodash.*` Method Packages
64+
65+
Imports from individual lodash method packages like `lodash.isnil` or `lodash.flattendeep` are transformed to use the optimized import path of `lodash` or `lodash-es`, consolidating your lodash usage to a single, tree-shakable ESM package.
66+
67+
### Your source input
68+
69+
```javascript
70+
import isNil from "lodash.isnil";
71+
import flattenDeep from "lodash.flattendeep";
72+
```
73+
74+
#### CommonJS output
75+
76+
```javascript
77+
import isNil from "lodash/isNil.js";
78+
import flattenDeep from "lodash/flattenDeep.js";
79+
```
80+
81+
#### ES output (with `useLodashEs: true`)
82+
83+
```javascript
84+
import { isNil } from "lodash-es";
85+
import { flattenDeep } from "lodash-es";
86+
```
87+
88+
Aliased local names are supported (`import checkNull from "lodash.isnil"` becomes `import checkNull from "lodash/isNil.js"`).
89+
5890
## Usage
5991

6092
```javascript
@@ -108,7 +140,7 @@ Set to `false` if you don't want the `.js` suffix added (prior to v3.x, this was
108140

109141
### `parseOptions`
110142

111-
Type: `Record<string, unknown> |((id: string) => Record<string, unknown>)`<br>
143+
Type: `Record<string, unknown> | ((id: string) => Record<string, unknown>)`<br>
112144
Default: `undefined`
113145

114146
If defined as a static object, this is passed to rollup's internal `parse` method. This can be combined with [`jsx.mode`](https://rollupjs.org/configuration-options/#jsx) to enable jsx parsing: `parseOptions: { jsx: true }`
@@ -119,26 +151,18 @@ If defined as a function, it is called with the filename. For instance, opt-in t
119151
parseOptions: (filename) => filename.endsWith(".jsx") ? { jsx: true } : {}
120152
```
121153

122-
## Rolldown Compatibility
154+
### `optimizeModularizedImports`
123155

124-
For basic use, this plugin "just works" with [Rolldown](https://rolldown.rs/). There is [a small test suite verifying it](https://github.com/kyle-johnson/rollup-plugin-optimize-lodash-imports/tree/main/packages/rollup-plugin/tests/rolldown).
156+
Type: `boolean`<br>
157+
Default: `true`
125158

126-
If you're relying on Rolldown to handle ts/tsx internally, you may need to use `parseOptions` to configure `lang` or [other parsing options](https://github.com/rolldown/rolldown/blob/f46e1d61d0de6f1d6c1968f3d20898e43fa3d2d7/packages/rolldown/src/binding.d.cts#L314):
159+
When `true`, imports from individual lodash method packages (e.g., `lodash.isnil`, `lodash.kebabcase`) are transformed to optimized imports from `lodash` or `lodash-es`.
127160

128-
```javascript
129-
optimizeLodashImports({
130-
// static
131-
parseOptions: { lang: "ts" },
132-
// or, dynamically by filename
133-
parseOptions: (filename) => ({
134-
lang: filename.endsWith(".ts") ? "ts" : "js",
135-
}),
136-
});
137-
```
161+
Set to `false` if you need to disable this behavior (prior to 6.x, this transformation did not ooccur).
138162

139163
## Vite Compatibility
140164

141-
This plugin "just works" as a [Vite 3.x plugin](https://vitejs.dev/guide/api-plugin.html#rollup-plugin-compatibility). Simply add it to `plugins` in your [Vite config](https://vitejs.dev/config/):
165+
This plugin "just works" as a [Vite plugin](https://vitejs.dev/guide/api-plugin.html#rollup-plugin-compatibility). Simply add it to `plugins` in your [Vite config](https://vitejs.dev/config/):
142166

143167
```javascript
144168
import { defineConfig } from "vite";
@@ -159,6 +183,23 @@ Example Vite output for a use of [kebabCase](https://lodash.com/docs/4.17.15#keb
159183

160184
A ~23 KiB reduction in compressed size!
161185

186+
## Rolldown Compatibility
187+
188+
For basic use, this plugin "just works" with [Rolldown](https://rolldown.rs/). There is [a small test suite verifying it](https://github.com/kyle-johnson/rollup-plugin-optimize-lodash-imports/tree/main/packages/rollup-plugin/tests/rolldown).
189+
190+
If you're relying on Rolldown to handle ts/tsx internally, you may need to use `parseOptions` to configure `lang` or [other parsing options](https://github.com/rolldown/rolldown/blob/f46e1d61d0de6f1d6c1968f3d20898e43fa3d2d7/packages/rolldown/src/binding.d.cts#L314):
191+
192+
```javascript
193+
optimizeLodashImports({
194+
// static
195+
parseOptions: { lang: "ts" },
196+
// or, dynamically by filename
197+
parseOptions: (filename) => ({
198+
lang: filename.endsWith(".ts") ? "ts" : "js",
199+
}),
200+
});
201+
```
202+
162203
## Limitations
163204

164205
### Default imports are not optimized
@@ -191,8 +232,12 @@ export function testX(x) {
191232

192233
The `chain()` method from `lodash` cannot be successfully imported from `"lodash/chain"` without also importing from `"lodash"`. Imports which include `chain()` are _not modified_ and the plugin prints a warning.
193234

235+
## NodeJS 12 and Rollup 2.x / 3.x support
236+
237+
versions of this plugin prior to 5.x supported NodeJS 12 and Rollup 2.x - 3.x. If you need support for these older versions, please use the 4.x release.
238+
194239
## Alternatives
195240

196-
[`babel-plugin-lodash`](https://www.npmjs.com/package/babel-plugin-lodash) solves the issue for CommonJS outputs and modifies default imports as well. However, it doesn't enable transparent `lodash-es` use and may not make sense for projects using [@rollup/plugin-typescript](https://www.npmjs.com/package/@rollup/plugin-typescript) which don't wish to add a Babel step.
241+
[`babel-plugin-lodash`](https://www.npmjs.com/package/babel-plugin-lodash) solves the issue for CommonJS outputs and modifies default imports as well. However, it doesn't enable transparent `lodash-es` use and may not make sense for projects using [@rollup/plugin-typescript](https://www.npmjs.com/package/@rollup/plugin-typescript) which don't wish to add a Babel step. It also does not modify modularized package imports (`lodash.isnil`, etc).
197242

198243
Other alternatives include `eslint-plugin-lodash` with the [`import-scope` rule enabled](https://github.com/wix/eslint-plugin-lodash/blob/HEAD/docs/rules/import-scope.md). This works for CommonJS outputs, but may require manual effort to stay on top of imports.

packages/rollup-plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"rollup": "4.52.5",
7474
"ts-jest": "catalog:",
7575
"typescript": "catalog:",
76-
"tsx": "4.20.6"
76+
"tsx": "catalog:"
7777
},
7878
"dependencies": {
7979
"@rollup/pluginutils": "^5.1.0",

packages/rollup-plugin/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ export type OptimizeLodashOptions = {
2525
*/
2626
appendDotJs?: boolean;
2727

28+
/**
29+
* Default: true. When true, imports from individual lodash method packages
30+
* (e.g., lodash.isnil, lodash.kebabcase) are transformed to optimized imports.
31+
* Set to false to disable this behavior.
32+
*/
33+
optimizeModularizedImports?: boolean;
34+
2835
/**
2936
* Additional options to pass to rollup's internal parse() function. For example:
3037
* `{ jsx: true }`
@@ -73,6 +80,7 @@ export const optimizeLodashImports: PluginImpl<OptimizeLodashOptions> = ({
7380
exclude,
7481
useLodashEs,
7582
appendDotJs,
83+
optimizeModularizedImports,
7684
parseOptions,
7785
}: OptimizeLodashOptions = {}) => {
7886
const filter = createFilter(include, exclude);
@@ -118,6 +126,7 @@ export const optimizeLodashImports: PluginImpl<OptimizeLodashOptions> = ({
118126
warn,
119127
useLodashEs,
120128
appendDotJs,
129+
optimizeModularizedImports,
121130
});
122131
},
123132
};

0 commit comments

Comments
 (0)