@omnimod/plugin-lodash-to-es-toolkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 salnika
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @omnimod/plugin-lodash-to-es-toolkit
2
+
3
+ [![npm](https://img.shields.io/npm/v/%40omnimod%2Fplugin-lodash-to-es-toolkit?label=npm)](https://www.npmjs.com/package/@omnimod/plugin-lodash-to-es-toolkit)
4
+ [![CI](https://img.shields.io/github/actions/workflow/status/Salnika/omnimod/ci.yml?branch=master&label=ci)](https://github.com/Salnika/omnimod/actions/workflows/ci.yml)
5
+ [![License](https://img.shields.io/github/license/Salnika/omnimod)](../../LICENSE)
6
+
7
+ omnimod plugin that migrates lodash imports to es-toolkit, and to native APIs
8
+ where the replacement is safe.
9
+
10
+ ## Use With The CLI
11
+
12
+ ```bash
13
+ omnimod run lodash-to-es-toolkit "src/**/*.{ts,tsx,js,jsx}"
14
+ omnimod run lodash-to-es-toolkit "src/**/*.{ts,tsx,js,jsx}" --write
15
+ ```
16
+
17
+ ## Use As A Library
18
+
19
+ ```ts
20
+ import { lodashToEsToolkit } from "@omnimod/plugin-lodash-to-es-toolkit";
21
+ ```
22
+
23
+ ## Notes
24
+
25
+ The plugin rewrites supported named lodash imports. Helpers with semantics that
26
+ need review are left in place and reported as diagnostics.
@@ -0,0 +1,11 @@
1
+ //#region src/plugin.d.ts
2
+ type LodashToEsToolkitOptions = Record<string, unknown>;
3
+ /**
4
+ * Migrate named `lodash` / `lodash-es` imports to `es-toolkit`, rewriting a small
5
+ * set of native-safe helpers to their JS built-ins and leaving divergent helpers
6
+ * on lodash (with a diagnostic). Default (`import _ from "lodash"`) and namespace
7
+ * imports are reported and left untouched.
8
+ */
9
+ declare const lodashToEsToolkit: import("@omnimod/core").Plugin<LodashToEsToolkitOptions, unknown>;
10
+ //#endregion
11
+ export { type LodashToEsToolkitOptions, lodashToEsToolkit as default, lodashToEsToolkit };
package/dist/index.mjs ADDED
@@ -0,0 +1,233 @@
1
+ import { definePlugin, walk } from "@omnimod/core";
2
+ import { cast } from "@omnimod/plugin-utils";
3
+ //#region src/plugin.ts
4
+ const LODASH_SOURCES = /* @__PURE__ */ new Set(["lodash", "lodash-es"]);
5
+ const ES_TOOLKIT_SOURCE = "es-toolkit";
6
+ /**
7
+ * Names that es-toolkit re-exports under the same identifier. The import source is
8
+ * rewritten to "es-toolkit" and the specifier kept verbatim.
9
+ */
10
+ const SUPPORTED = /* @__PURE__ */ new Set([
11
+ "debounce",
12
+ "throttle",
13
+ "cloneDeep",
14
+ "clone",
15
+ "isEqual",
16
+ "isEqualWith",
17
+ "groupBy",
18
+ "keyBy",
19
+ "uniq",
20
+ "uniqBy",
21
+ "uniqWith",
22
+ "difference",
23
+ "differenceBy",
24
+ "intersection",
25
+ "intersectionBy",
26
+ "omit",
27
+ "omitBy",
28
+ "pick",
29
+ "pickBy",
30
+ "mapValues",
31
+ "mapKeys",
32
+ "merge",
33
+ "mergeWith",
34
+ "chunk",
35
+ "compact",
36
+ "flatten",
37
+ "flattenDeep",
38
+ "drop",
39
+ "dropRight",
40
+ "take",
41
+ "takeRight",
42
+ "zip",
43
+ "unzip",
44
+ "sum",
45
+ "sumBy",
46
+ "mean",
47
+ "meanBy",
48
+ "maxBy",
49
+ "minBy",
50
+ "sortBy",
51
+ "orderBy",
52
+ "range",
53
+ "random",
54
+ "sample",
55
+ "sampleSize",
56
+ "shuffle",
57
+ "capitalize",
58
+ "camelCase",
59
+ "kebabCase",
60
+ "snakeCase",
61
+ "startCase",
62
+ "upperFirst",
63
+ "lowerFirst",
64
+ "isNil",
65
+ "isPlainObject",
66
+ "isBoolean",
67
+ "isString",
68
+ "isNumber",
69
+ "isFunction",
70
+ "isDate",
71
+ "isRegExp",
72
+ "isSymbol",
73
+ "delay",
74
+ "once",
75
+ "memoize",
76
+ "partition",
77
+ "countBy",
78
+ "invert",
79
+ "toMerged",
80
+ "head",
81
+ "last",
82
+ "initial",
83
+ "tail"
84
+ ]);
85
+ /**
86
+ * Native-safe rewrites: the import specifier is dropped and simple call sites
87
+ * `name(...args)` are rewritten to the native callee. `noop` maps to an inlined
88
+ * arrow. Only trivial call forms are rewritten; anything else keeps the import.
89
+ */
90
+ const NATIVE_CALLEE = {
91
+ isArray: "Array.isArray",
92
+ isInteger: "Number.isInteger",
93
+ isNaN: "Number.isNaN",
94
+ isFinite: "Number.isFinite"
95
+ };
96
+ const NATIVE_NAMES = /* @__PURE__ */ new Set([...Object.keys(NATIVE_CALLEE), "noop"]);
97
+ /** Names left on lodash because their es-toolkit / native semantics diverge. */
98
+ const UNSUPPORTED_REASON = {
99
+ get: "lodash.get has no direct es-toolkit equivalent (path access differs)",
100
+ set: "lodash.set has no direct es-toolkit equivalent",
101
+ has: "lodash.has has no direct es-toolkit equivalent",
102
+ isEmpty: "es-toolkit.isEmpty diverges from lodash.isEmpty",
103
+ map: "lodash collection semantics (objects/iteratee shorthand) differ",
104
+ filter: "lodash collection semantics (objects/iteratee shorthand) differ",
105
+ reduce: "lodash collection semantics (objects/iteratee shorthand) differ",
106
+ forEach: "lodash collection semantics (objects/iteratee shorthand) differ",
107
+ template: "lodash.template has no es-toolkit equivalent",
108
+ chain: "lodash.chain has no es-toolkit equivalent",
109
+ flow: "lodash.flow has no direct es-toolkit equivalent"
110
+ };
111
+ /**
112
+ * Migrate named `lodash` / `lodash-es` imports to `es-toolkit`, rewriting a small
113
+ * set of native-safe helpers to their JS built-ins and leaving divergent helpers
114
+ * on lodash (with a diagnostic). Default (`import _ from "lodash"`) and namespace
115
+ * imports are reported and left untouched.
116
+ */
117
+ const lodashToEsToolkit = definePlugin({
118
+ name: "lodash-to-es-toolkit",
119
+ description: "Migrate lodash to es-toolkit (and native where safe).",
120
+ include: ["**/*.{ts,tsx,js,jsx,mts,cts}"],
121
+ transform(file) {
122
+ for (const stmt of file.program.body) {
123
+ if (stmt.type !== "ImportDeclaration") continue;
124
+ const imp = cast(stmt);
125
+ if (!LODASH_SOURCES.has(imp.source.value)) continue;
126
+ convertImport(file, imp);
127
+ }
128
+ }
129
+ });
130
+ function convertImport(file, imp) {
131
+ const named = [];
132
+ let hasDefault = false;
133
+ let hasNamespace = false;
134
+ for (const spec of imp.specifiers) if (spec.type === "ImportDefaultSpecifier") hasDefault = true;
135
+ else if (spec.type === "ImportNamespaceSpecifier") hasNamespace = true;
136
+ else if (spec.type === "ImportSpecifier") {
137
+ const s = cast(spec);
138
+ if (s.imported.type !== "Identifier") continue;
139
+ named.push({
140
+ imported: cast(s.imported).name,
141
+ local: s.local.name,
142
+ node: spec
143
+ });
144
+ }
145
+ if (hasDefault) {
146
+ file.report({
147
+ severity: "warn",
148
+ message: `Default \`${imp.source.value}\` import; convert to named imports manually.`
149
+ });
150
+ return;
151
+ }
152
+ if (hasNamespace) {
153
+ file.report({
154
+ severity: "warn",
155
+ message: `Namespace \`${imp.source.value}\` import; convert to named imports manually.`
156
+ });
157
+ return;
158
+ }
159
+ if (named.length === 0) return;
160
+ const supported = [];
161
+ const nativeSafe = [];
162
+ const residual = [];
163
+ for (const spec of named) if (SUPPORTED.has(spec.imported)) supported.push(spec);
164
+ else if (NATIVE_NAMES.has(spec.imported) && canRewriteNative(spec)) nativeSafe.push(spec);
165
+ else {
166
+ residual.push(spec);
167
+ const reason = UNSUPPORTED_REASON[spec.imported] ?? `\`${spec.imported}\` has no known es-toolkit equivalent`;
168
+ file.report({
169
+ severity: "warn",
170
+ message: `Left \`${spec.imported}\` on ${imp.source.value}: ${reason}`
171
+ });
172
+ }
173
+ for (const spec of nativeSafe) rewriteNativeUsages(file, spec);
174
+ if (supported.length === 0 && residual.length === 0) {
175
+ file.magic.remove(imp.start, imp.end);
176
+ return;
177
+ }
178
+ if (residual.length === 0) {
179
+ const line = renderImport(ES_TOOLKIT_SOURCE, supported);
180
+ file.magic.update(imp.start, imp.end, line);
181
+ return;
182
+ }
183
+ if (supported.length === 0) {
184
+ if (nativeSafe.length > 0) {
185
+ const line = renderImport(imp.source.value, residual);
186
+ file.magic.update(imp.start, imp.end, line);
187
+ }
188
+ return;
189
+ }
190
+ const esLine = renderImport(ES_TOOLKIT_SOURCE, supported);
191
+ const residualLine = renderImport(imp.source.value, residual);
192
+ file.magic.update(imp.start, imp.end, `${esLine}\n${residualLine}`);
193
+ }
194
+ /** Render `import { a, b as c } from "source";` for a set of specifiers. */
195
+ function renderImport(source, specs) {
196
+ return `import { ${specs.map((spec) => spec.local === spec.imported ? spec.imported : `${spec.imported} as ${spec.local}`).sort((a, b) => a.localeCompare(b)).join(", ")} } from ${JSON.stringify(source)};`;
197
+ }
198
+ /**
199
+ * Whether a native-safe specifier is trivially rewritable. `noop` is only safe
200
+ * when it is never aliased (its local name is `noop`) — aliasing would require
201
+ * tracking the binding, which v1 does not do.
202
+ */
203
+ function canRewriteNative(spec) {
204
+ if (spec.imported === "noop") return spec.local === "noop";
205
+ return true;
206
+ }
207
+ /**
208
+ * Rewrite call sites of a native-safe helper. For the mapped built-ins we only
209
+ * rewrite direct call callees (`name(...)`), leaving any non-call reference (e.g.
210
+ * passing `isArray` as a value) untouched. `noop` references are replaced with an
211
+ * inlined `(() => {})` arrow.
212
+ */
213
+ function rewriteNativeUsages(file, spec) {
214
+ const nativeCallee = NATIVE_CALLEE[spec.imported];
215
+ walk(file.program, (node, parent, key) => {
216
+ if (node === spec.node) return "skip";
217
+ if (nativeCallee) {
218
+ if (node.type !== "CallExpression") return;
219
+ const call = cast(node);
220
+ if (call.callee.type !== "Identifier") return;
221
+ if (cast(call.callee).name !== spec.local) return;
222
+ file.magic.update(call.callee.start, call.callee.end, nativeCallee);
223
+ return;
224
+ }
225
+ if (node.type !== "Identifier") return;
226
+ if (cast(node).name !== spec.local) return;
227
+ if (parent && parent.type === "MemberExpression" && key === "property") return;
228
+ if (parent && parent.type === "ImportSpecifier") return "skip";
229
+ file.magic.update(node.start, node.end, "(() => {})");
230
+ });
231
+ }
232
+ //#endregion
233
+ export { lodashToEsToolkit as default, lodashToEsToolkit };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@omnimod/plugin-lodash-to-es-toolkit",
3
+ "version": "0.1.0",
4
+ "description": "omnimod plugin: migrate lodash to es-toolkit (and native where safe).",
5
+ "keywords": [
6
+ "codemod",
7
+ "es-toolkit",
8
+ "lodash",
9
+ "migration",
10
+ "omnimod-plugin"
11
+ ],
12
+ "homepage": "https://salnika.github.io/omnimod/",
13
+ "bugs": "https://github.com/salnika/omnimod/issues",
14
+ "license": "MIT",
15
+ "author": "salnika",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/salnika/omnimod.git",
19
+ "directory": "packages/plugin-lodash-to-es-toolkit"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "type": "module",
25
+ "exports": {
26
+ ".": "./dist/index.mjs",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "dependencies": {
33
+ "@omnimod/core": "0.1.0",
34
+ "@omnimod/plugin-utils": "0.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^25.6.2",
38
+ "@typescript/native-preview": "7.0.0-dev.20260509.2",
39
+ "typescript": "^6.0.3",
40
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.2.2",
41
+ "vite-plus": "0.2.2"
42
+ },
43
+ "scripts": {
44
+ "build": "vp pack",
45
+ "dev": "vp pack --watch",
46
+ "test": "vp test",
47
+ "check": "vp check"
48
+ }
49
+ }