eslint-plugin-bunisms 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 +21 -0
- package/README.md +82 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +260 -0
- package/dist/rules/prefer-bun-file.d.ts +2 -0
- package/dist/rules/prefer-bun-spawn.d.ts +2 -0
- package/dist/rules/prefer-bun-write.d.ts +2 -0
- package/dist/utils/create-rule.d.ts +2 -0
- package/dist/utils/imports.d.ts +8 -0
- package/docs/rules/prefer-bun-file.md +48 -0
- package/docs/rules/prefer-bun-spawn.md +49 -0
- package/docs/rules/prefer-bun-write.md +48 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dave Lunny
|
|
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,82 @@
|
|
|
1
|
+
# eslint-plugin-bunisms 🐰
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/eslint-plugin-bunisms)
|
|
4
|
+
[](https://github.com/himynameisdave/eslint-plugin-bunisms/actions/workflows/ci.yml?query=branch%3Amain)
|
|
5
|
+
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fhimynameisdave%2Feslint-plugin-bunisms?ref=badge_shield&issueType=license)
|
|
6
|
+
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fhimynameisdave%2Feslint-plugin-bunisms?ref=badge_shield&issueType=security)
|
|
7
|
+
|
|
8
|
+
> ESLint rules for idiomatic and correct Bun code. Supports TypeScript and JavaScript, Oxlint, and ESLint 9–10.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
bun add -D eslint-plugin-bunisms
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
You also need `oxlint` or `eslint` installed.
|
|
17
|
+
|
|
18
|
+
## Oxlint
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// oxlint.config.ts
|
|
22
|
+
import { defineConfig } from 'oxlint';
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
jsPlugins: [{ name: 'bun', specifier: 'eslint-plugin-bunisms' }],
|
|
26
|
+
rules: {
|
|
27
|
+
'bun/prefer-bun-file': 'warn',
|
|
28
|
+
'bun/prefer-bun-write': 'warn',
|
|
29
|
+
'bun/prefer-bun-spawn': 'warn',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
ESLint is optional when using Oxlint. Oxlint's JavaScript plugin support is currently alpha; compatibility is tested in CI.
|
|
35
|
+
|
|
36
|
+
## ESLint
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// eslint.config.ts
|
|
40
|
+
import bun from 'eslint-plugin-bunisms';
|
|
41
|
+
|
|
42
|
+
export default [bun.configs.recommended];
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
ESLint needs [`jiti`](https://github.com/unjs/jiti) to load a TypeScript config file on Node. For TypeScript files, configure a TypeScript parser such as `@typescript-eslint/parser`. These rules do not require type information.
|
|
46
|
+
|
|
47
|
+
## Rules
|
|
48
|
+
|
|
49
|
+
| Rule | Recommends |
|
|
50
|
+
| ------------------------------------------------------ | -------------------------------------------------------------------------- |
|
|
51
|
+
| [bun/prefer-bun-file](docs/rules/prefer-bun-file.md) | `Bun.file()` instead of Node's `readFile()` |
|
|
52
|
+
| [bun/prefer-bun-write](docs/rules/prefer-bun-write.md) | `Bun.write()` instead of Node's `writeFile()` |
|
|
53
|
+
| [bun/prefer-bun-spawn](docs/rules/prefer-bun-spawn.md) | `Bun.spawn()` / `Bun.spawnSync()` instead of Node's subprocess equivalents |
|
|
54
|
+
|
|
55
|
+
Rules recognize imports, aliases and CommonJS bindings, respecting lexical scope. They report calls without automatically rewriting them: Node and Bun APIs have different options and return values.
|
|
56
|
+
|
|
57
|
+
The `recommended`, `strict` and `all` ESLint presets currently enable all three rules as warnings. Override individual rules after the preset:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
export default [bun.configs.recommended, { rules: { 'bun/prefer-bun-file': 'error' } }];
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Compatibility
|
|
64
|
+
|
|
65
|
+
Targets **Bun >=1.4.0** applications. The plugin itself runs on **Node >=18.18.0**, subject to your linter's Node requirements, and has no runtime dependencies.
|
|
66
|
+
|
|
67
|
+
Apply the plugin only to code intended for Bun. For a mixed-runtime project, scope the ESLint preset with `files`:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
export default [{ ...bun.configs.recommended, files: ['scripts/**/*.ts'] }];
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
See [Contributing](CONTRIBUTING.md) for development and [the roadmap](https://github.com/himynameisdave/eslint-plugin-bunisms/blob/main/docs/roadmap.md) for planned rules.
|
|
74
|
+
|
|
75
|
+
## See also
|
|
76
|
+
|
|
77
|
+
- [`@himynameisdave/oxlint-config`](https://github.com/himynameisdave/oxlint-config)
|
|
78
|
+
- [`@himynameisdave/oxfmt-config`](https://github.com/himynameisdave/oxfmt-config)
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
<sub>_[MIT](LICENSE) © Dave Lunny_</sub>
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ESLint, Linter } from 'eslint';
|
|
2
|
+
declare const rules: {
|
|
3
|
+
'prefer-bun-file': import("eslint").Rule.RuleModule;
|
|
4
|
+
'prefer-bun-write': import("eslint").Rule.RuleModule;
|
|
5
|
+
'prefer-bun-spawn': import("eslint").Rule.RuleModule;
|
|
6
|
+
};
|
|
7
|
+
type Preset = 'recommended' | 'strict' | 'all';
|
|
8
|
+
declare const plugin: ESLint.Plugin & {
|
|
9
|
+
rules: typeof rules;
|
|
10
|
+
configs: Record<Preset, Linter.Config>;
|
|
11
|
+
};
|
|
12
|
+
export default plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// package.json
|
|
2
|
+
var package_default = {
|
|
3
|
+
name: "eslint-plugin-bunisms",
|
|
4
|
+
version: "0.1.0",
|
|
5
|
+
description: "ESLint rules for idiomatic and correct Bun code.",
|
|
6
|
+
keywords: [
|
|
7
|
+
"bun",
|
|
8
|
+
"bunjs",
|
|
9
|
+
"eslint",
|
|
10
|
+
"eslintplugin",
|
|
11
|
+
"lint",
|
|
12
|
+
"linter",
|
|
13
|
+
"oxlint"
|
|
14
|
+
],
|
|
15
|
+
homepage: "https://github.com/himynameisdave/eslint-plugin-bunisms#readme",
|
|
16
|
+
bugs: "https://github.com/himynameisdave/eslint-plugin-bunisms/issues",
|
|
17
|
+
license: "MIT",
|
|
18
|
+
author: "Dave Lunny",
|
|
19
|
+
repository: {
|
|
20
|
+
type: "git",
|
|
21
|
+
url: "git+https://github.com/himynameisdave/eslint-plugin-bunisms.git"
|
|
22
|
+
},
|
|
23
|
+
files: [
|
|
24
|
+
"dist",
|
|
25
|
+
"docs/rules",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
type: "module",
|
|
30
|
+
sideEffects: false,
|
|
31
|
+
main: "./dist/index.js",
|
|
32
|
+
types: "./dist/index.d.ts",
|
|
33
|
+
exports: {
|
|
34
|
+
".": {
|
|
35
|
+
types: "./dist/index.d.ts",
|
|
36
|
+
import: "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
scripts: {
|
|
40
|
+
build: "bun scripts/build.ts && tsc -p tsconfig.build.json",
|
|
41
|
+
check: "bun run typecheck && bun run lint && bun run format:check && bun run test && bun run build && bun run test:node && bun run test:oxlint && bun run test:package",
|
|
42
|
+
format: "oxfmt --write",
|
|
43
|
+
"format:check": "oxfmt --check",
|
|
44
|
+
lint: "oxlint -c oxlint.config.ts --deny-warnings",
|
|
45
|
+
"lint:fix": "bun run lint --fix",
|
|
46
|
+
prepublishOnly: "bun run check",
|
|
47
|
+
test: "bun test tests/rules.test.ts",
|
|
48
|
+
"test:node": "node --test tests/node.test.mjs",
|
|
49
|
+
"test:oxlint": "bun scripts/test-oxlint.ts",
|
|
50
|
+
"test:package": "bun scripts/test-package.ts",
|
|
51
|
+
typecheck: "tsc --noEmit"
|
|
52
|
+
},
|
|
53
|
+
devDependencies: {
|
|
54
|
+
"@himynameisdave/oxfmt-config": "^1.0.0",
|
|
55
|
+
"@himynameisdave/oxlint-config": "^1.2.0",
|
|
56
|
+
"@types/bun": "^1.4.2",
|
|
57
|
+
"@types/estree": "^1.0.9",
|
|
58
|
+
"@typescript-eslint/parser": "^8.70.1",
|
|
59
|
+
eslint: "^9.39.5",
|
|
60
|
+
eslint10: "npm:eslint@^10",
|
|
61
|
+
oxfmt: "0.70.0",
|
|
62
|
+
oxlint: "^1.85.0",
|
|
63
|
+
typescript: "~5.9.3"
|
|
64
|
+
},
|
|
65
|
+
peerDependencies: {
|
|
66
|
+
eslint: ">=9.0.0 <11"
|
|
67
|
+
},
|
|
68
|
+
peerDependenciesMeta: {
|
|
69
|
+
eslint: {
|
|
70
|
+
optional: true
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
engines: {
|
|
74
|
+
node: ">=18.18.0"
|
|
75
|
+
},
|
|
76
|
+
packageManager: "bun@1.4.2"
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/utils/imports.ts
|
|
80
|
+
function variableFor(context, node) {
|
|
81
|
+
let scope = context.sourceCode.getScope(node);
|
|
82
|
+
while (scope) {
|
|
83
|
+
const variable = scope.set.get(node.name);
|
|
84
|
+
if (variable) {
|
|
85
|
+
return variable;
|
|
86
|
+
}
|
|
87
|
+
scope = scope.upper;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function propertyName(node, computed) {
|
|
91
|
+
if (!computed && node.type === "Identifier") {
|
|
92
|
+
return node.name;
|
|
93
|
+
}
|
|
94
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
95
|
+
return node.value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function isModified(variable) {
|
|
99
|
+
return variable.references.some((reference) => {
|
|
100
|
+
if (reference.isWrite() && !reference.init) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
let node = reference.identifier;
|
|
104
|
+
while (node.parent?.type === "MemberExpression" && node.parent.object === node) {
|
|
105
|
+
node = node.parent;
|
|
106
|
+
}
|
|
107
|
+
const { parent } = node;
|
|
108
|
+
return parent?.type === "AssignmentExpression" && parent.left === node || parent?.type === "UpdateExpression" || parent?.type === "UnaryExpression" && parent.operator === "delete";
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function resolveBuiltin(context, node) {
|
|
112
|
+
if (node.type === "MemberExpression") {
|
|
113
|
+
const property = propertyName(node.property, node.computed);
|
|
114
|
+
const object = resolveBuiltin(context, node.object);
|
|
115
|
+
if (property && object) {
|
|
116
|
+
return { ...object, path: [...object.path, property] };
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "require") {
|
|
121
|
+
const variable = variableFor(context, node.callee);
|
|
122
|
+
if (variable?.defs.length || node.arguments.length !== 1) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const [source] = node.arguments;
|
|
126
|
+
if (source?.type === "Literal" && typeof source.value === "string") {
|
|
127
|
+
return { module: source.value.replace(/^node:/u, ""), path: [] };
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (node.type !== "Identifier") {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const variable = variableFor(context, node);
|
|
135
|
+
if (!variable || variable.defs.length !== 1 || isModified(variable)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const [definition] = variable.defs;
|
|
139
|
+
if (!definition) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (definition.type === "ImportBinding") {
|
|
143
|
+
const declaration = definition.parent;
|
|
144
|
+
if (declaration.importKind === "type" || definition.node.importKind === "type") {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const source = declaration.source.value;
|
|
148
|
+
if (typeof source !== "string") {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const specifier = definition.node;
|
|
152
|
+
const path = specifier.type === "ImportSpecifier" ? [
|
|
153
|
+
specifier.imported.type === "Identifier" ? specifier.imported.name : String(specifier.imported.value)
|
|
154
|
+
] : [];
|
|
155
|
+
return { module: source.replace(/^node:/u, ""), path };
|
|
156
|
+
}
|
|
157
|
+
if (definition.type !== "Variable" || definition.parent.kind !== "const") {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const { id, init } = definition.node;
|
|
161
|
+
if (!init || init.type !== "CallExpression") {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const origin = resolveBuiltin(context, init);
|
|
165
|
+
if (!origin) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (id.type === "Identifier") {
|
|
169
|
+
return origin;
|
|
170
|
+
}
|
|
171
|
+
if (id.type === "ObjectPattern") {
|
|
172
|
+
const property = id.properties.find((entry) => entry.type === "Property" && entry.value.type === "Identifier" && entry.value.name === node.name);
|
|
173
|
+
if (property?.type !== "Property") {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const name = propertyName(property.key, property.computed);
|
|
177
|
+
if (name) {
|
|
178
|
+
return { ...origin, path: [...origin.path, name] };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/utils/create-rule.ts
|
|
184
|
+
function createRule(name, methods, modules, fsPromises = false) {
|
|
185
|
+
return {
|
|
186
|
+
meta: {
|
|
187
|
+
type: "suggestion",
|
|
188
|
+
docs: {
|
|
189
|
+
description: `Prefer ${Object.values(methods).join(" or ")} over the corresponding Node.js APIs.`,
|
|
190
|
+
url: `https://github.com/himynameisdave/eslint-plugin-bunisms/blob/main/docs/rules/${name}.md`
|
|
191
|
+
},
|
|
192
|
+
schema: [],
|
|
193
|
+
messages: { preferBun: "Prefer {{replacement}} over Node.js {{method}}() when targeting Bun." }
|
|
194
|
+
},
|
|
195
|
+
create(context) {
|
|
196
|
+
return {
|
|
197
|
+
CallExpression(node) {
|
|
198
|
+
const reference = resolveBuiltin(context, node.callee);
|
|
199
|
+
if (!reference || !modules.includes(reference.module)) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
let { path } = reference;
|
|
203
|
+
if (fsPromises && reference.module === "fs" && path[0] === "promises") {
|
|
204
|
+
path = path.slice(1);
|
|
205
|
+
}
|
|
206
|
+
if (path.length !== 1) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const [method] = path;
|
|
210
|
+
if (!method || !Object.hasOwn(methods, method)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const replacement = methods[method];
|
|
214
|
+
if (!replacement) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
context.report({
|
|
218
|
+
node: node.callee,
|
|
219
|
+
messageId: "preferBun",
|
|
220
|
+
data: { method, replacement }
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/rules/prefer-bun-file.ts
|
|
229
|
+
var prefer_bun_file_default = createRule("prefer-bun-file", { readFile: "Bun.file()" }, ["fs", "fs/promises"], true);
|
|
230
|
+
|
|
231
|
+
// src/rules/prefer-bun-spawn.ts
|
|
232
|
+
var prefer_bun_spawn_default = createRule("prefer-bun-spawn", { spawn: "Bun.spawn()", spawnSync: "Bun.spawnSync()" }, [
|
|
233
|
+
"child_process"
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
// src/rules/prefer-bun-write.ts
|
|
237
|
+
var prefer_bun_write_default = createRule("prefer-bun-write", { writeFile: "Bun.write()" }, ["fs", "fs/promises"], true);
|
|
238
|
+
|
|
239
|
+
// src/index.ts
|
|
240
|
+
var rules = {
|
|
241
|
+
"prefer-bun-file": prefer_bun_file_default,
|
|
242
|
+
"prefer-bun-write": prefer_bun_write_default,
|
|
243
|
+
"prefer-bun-spawn": prefer_bun_spawn_default
|
|
244
|
+
};
|
|
245
|
+
var plugin = {
|
|
246
|
+
meta: { name: "eslint-plugin-bunisms", version: package_default.version },
|
|
247
|
+
rules,
|
|
248
|
+
configs: {}
|
|
249
|
+
};
|
|
250
|
+
for (const preset of ["recommended", "strict", "all"]) {
|
|
251
|
+
plugin.configs[preset] = {
|
|
252
|
+
name: `bun/${preset}`,
|
|
253
|
+
plugins: { bun: plugin },
|
|
254
|
+
rules: Object.fromEntries(Object.keys(rules).map((name) => [`bun/${name}`, "warn"]))
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
var src_default = plugin;
|
|
258
|
+
export {
|
|
259
|
+
src_default as default
|
|
260
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
import type { Node, Expression } from 'estree';
|
|
3
|
+
export type BuiltinReference = {
|
|
4
|
+
module: string;
|
|
5
|
+
path: string[];
|
|
6
|
+
};
|
|
7
|
+
/** Resolve only direct builtin imports/requires, never names alone or arbitrary aliases. */
|
|
8
|
+
export declare function resolveBuiltin(context: Rule.RuleContext, node: Expression | Node): BuiltinReference | undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# prefer-bun-file
|
|
2
|
+
|
|
3
|
+
📝 Prefer `Bun.file()` over the corresponding Node.js APIs.
|
|
4
|
+
|
|
5
|
+
⚠️ This rule _warns_ in the following [configs](https://github.com/himynameisdave/eslint-plugin-bunisms#eslint): ✅ `recommended`, 🔒 `strict`, 🌐 `all`.
|
|
6
|
+
|
|
7
|
+
[`Bun.file()`](https://bun.sh/docs/runtime/file-io) exposes text, JSON, binary and streaming readers without needing the Node filesystem API.
|
|
8
|
+
|
|
9
|
+
This rule reports [`readFile()`](https://nodejs.org/api/fs.html) from `fs` and `fs/promises`, including `fs.promises.readFile()`. It does not report `readFileSync`, streams, directory operations or unrelated functions.
|
|
10
|
+
|
|
11
|
+
Named, aliased, default and namespace imports are supported, with either bare or `node:` module names. CommonJS direct calls and `const` require bindings (including destructuring) are supported. Lexical shadowing and visible binding/module-property mutations suppress reports. Dynamic imports, indirect aliases, mutable CommonJS declarations and interprocedural mutation tracking are not supported. Only actual calls report; unused imports or function references do not.
|
|
12
|
+
|
|
13
|
+
The rule reports without a fix, because the APIs are not drop-in replacements. `Bun.file()` creates a lazy file object; reading requires `.text()`, `.json()`, `.arrayBuffer()` or another reader, and its binary result is not a Node `Buffer`. Keep the Node API when the module must also run under Node, when callback behavior is required, or when encodings, options or file descriptors need Node-specific handling. Disable the rule for such files or scope the preset to Bun-only code.
|
|
14
|
+
|
|
15
|
+
## Examples
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// ❌
|
|
19
|
+
import { readFile } from 'node:fs/promises';
|
|
20
|
+
const text = await readFile('hello.txt', 'utf8');
|
|
21
|
+
|
|
22
|
+
// ✅
|
|
23
|
+
const text = await Bun.file('hello.txt').text();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// ❌
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
const bytes = await fs.promises.readFile('hello.bin');
|
|
30
|
+
|
|
31
|
+
// ✅
|
|
32
|
+
const bytes = await Bun.file('hello.bin').arrayBuffer();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
// ❌
|
|
37
|
+
const { readFile } = require('fs/promises');
|
|
38
|
+
const config = JSON.parse(await readFile('config.json', 'utf8'));
|
|
39
|
+
|
|
40
|
+
// ✅
|
|
41
|
+
const config = await Bun.file('config.json').json();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// ✅
|
|
46
|
+
import { readFileSync } from 'node:fs';
|
|
47
|
+
const text = readFileSync('hello.txt', 'utf8');
|
|
48
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# prefer-bun-spawn
|
|
2
|
+
|
|
3
|
+
📝 Prefer `Bun.spawn()` or `Bun.spawnSync()` over the corresponding Node.js APIs.
|
|
4
|
+
|
|
5
|
+
⚠️ This rule _warns_ in the following [configs](https://github.com/himynameisdave/eslint-plugin-bunisms#eslint): ✅ `recommended`, 🔒 `strict`, 🌐 `all`.
|
|
6
|
+
|
|
7
|
+
[`Bun.spawn()` and `Bun.spawnSync()`](https://bun.sh/docs/runtime/child-process) integrate with Bun streams and offer asynchronous and synchronous process execution.
|
|
8
|
+
|
|
9
|
+
This rule reports [`spawn()` and `spawnSync()`](https://nodejs.org/api/child_process.html) from `child_process`. It does not report `exec`, `execSync` or `fork`; shell migration belongs to a future separate rule.
|
|
10
|
+
|
|
11
|
+
Named, aliased, default and namespace imports are supported, with either bare or `node:` module names. CommonJS direct calls and `const` require bindings (including destructuring) are supported. Lexical shadowing and visible binding/module-property mutations suppress reports. Dynamic imports, indirect aliases, mutable CommonJS declarations and interprocedural mutation tracking are not supported. Only actual calls report; unused imports or function references do not.
|
|
12
|
+
|
|
13
|
+
The rule reports without a fix, because the APIs are not drop-in replacements: argument shapes, return objects, signals, buffering and error handling differ. Keep the Node API in cross-runtime tools, or where Node `ChildProcess` events, IPC or specific stdio/options semantics are required. Disable the rule for such files or scope the preset to Bun-only code.
|
|
14
|
+
|
|
15
|
+
## Examples
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// ❌
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
spawn('echo', ['hello']);
|
|
21
|
+
|
|
22
|
+
// ✅
|
|
23
|
+
const child = Bun.spawn(['echo', 'hello']);
|
|
24
|
+
await child.exited;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
// ❌
|
|
29
|
+
import * as cp from 'node:child_process';
|
|
30
|
+
cp.spawnSync('echo', ['hello']);
|
|
31
|
+
|
|
32
|
+
// ✅
|
|
33
|
+
const result = Bun.spawnSync(['echo', 'hello']);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
// ❌
|
|
38
|
+
const { spawn: run } = require('child_process');
|
|
39
|
+
run('echo', ['hello']);
|
|
40
|
+
|
|
41
|
+
// ✅
|
|
42
|
+
const child = Bun.spawn(['echo', 'hello']);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
// ✅
|
|
47
|
+
import { exec } from 'node:child_process';
|
|
48
|
+
exec('echo hello');
|
|
49
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# prefer-bun-write
|
|
2
|
+
|
|
3
|
+
📝 Prefer `Bun.write()` over the corresponding Node.js APIs.
|
|
4
|
+
|
|
5
|
+
⚠️ This rule _warns_ in the following [configs](https://github.com/himynameisdave/eslint-plugin-bunisms#eslint): ✅ `recommended`, 🔒 `strict`, 🌐 `all`.
|
|
6
|
+
|
|
7
|
+
[`Bun.write()`](https://bun.sh/docs/runtime/file-io) accepts strings, binary data, blobs and responses for Bun-native file writes.
|
|
8
|
+
|
|
9
|
+
This rule reports [`writeFile()`](https://nodejs.org/api/fs.html) from `fs` and `fs/promises`, including `fs.promises.writeFile()`. It does not report `appendFile`, `writeFileSync`, streams or other filesystem operations.
|
|
10
|
+
|
|
11
|
+
Named, aliased, default and namespace imports are supported, with either bare or `node:` module names. CommonJS direct calls and `const` require bindings (including destructuring) are supported. Lexical shadowing and visible binding/module-property mutations suppress reports. Dynamic imports, indirect aliases, mutable CommonJS declarations and interprocedural mutation tracking are not supported. Only actual calls report; unused imports or function references do not.
|
|
12
|
+
|
|
13
|
+
The rule reports without a fix, because the APIs are not drop-in replacements. `Bun.write()` returns a byte count; Node's `writeFile()` does not. Keep the Node API in shared Node/Bun code, or where callback, encoding, permissions, flags, abort signals or file-descriptor semantics are needed. Review options and error handling before migrating. Disable the rule for such files or scope the preset to Bun-only code.
|
|
14
|
+
|
|
15
|
+
## Examples
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// ❌
|
|
19
|
+
import { writeFile } from 'node:fs/promises';
|
|
20
|
+
await writeFile('hello.txt', 'Hello!');
|
|
21
|
+
|
|
22
|
+
// ✅
|
|
23
|
+
await Bun.write('hello.txt', 'Hello!');
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// ❌
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
await fs.promises.writeFile('data.json', JSON.stringify(data));
|
|
30
|
+
|
|
31
|
+
// ✅
|
|
32
|
+
await Bun.write('data.json', JSON.stringify(data));
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
// ❌
|
|
37
|
+
const { writeFile: save } = require('fs/promises');
|
|
38
|
+
await save('hello.txt', 'Hello!');
|
|
39
|
+
|
|
40
|
+
// ✅
|
|
41
|
+
await Bun.write('hello.txt', 'Hello!');
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// ✅
|
|
46
|
+
import { appendFile } from 'node:fs/promises';
|
|
47
|
+
await appendFile('log.txt', 'Hello!\n');
|
|
48
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-bunisms",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint rules for idiomatic and correct Bun code.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bun",
|
|
7
|
+
"bunjs",
|
|
8
|
+
"eslint",
|
|
9
|
+
"eslintplugin",
|
|
10
|
+
"lint",
|
|
11
|
+
"linter",
|
|
12
|
+
"oxlint"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/himynameisdave/eslint-plugin-bunisms#readme",
|
|
15
|
+
"bugs": "https://github.com/himynameisdave/eslint-plugin-bunisms/issues",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Dave Lunny",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/himynameisdave/eslint-plugin-bunisms.git"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"docs/rules",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "bun scripts/build.ts && tsc -p tsconfig.build.json",
|
|
40
|
+
"check": "bun run typecheck && bun run lint && bun run format:check && bun run test && bun run build && bun run test:node && bun run test:oxlint && bun run test:package",
|
|
41
|
+
"format": "oxfmt --write",
|
|
42
|
+
"format:check": "oxfmt --check",
|
|
43
|
+
"lint": "oxlint -c oxlint.config.ts --deny-warnings",
|
|
44
|
+
"lint:fix": "bun run lint --fix",
|
|
45
|
+
"prepublishOnly": "bun run check",
|
|
46
|
+
"test": "bun test tests/rules.test.ts",
|
|
47
|
+
"test:node": "node --test tests/node.test.mjs",
|
|
48
|
+
"test:oxlint": "bun scripts/test-oxlint.ts",
|
|
49
|
+
"test:package": "bun scripts/test-package.ts",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@himynameisdave/oxfmt-config": "^1.0.0",
|
|
54
|
+
"@himynameisdave/oxlint-config": "^1.2.0",
|
|
55
|
+
"@types/bun": "^1.4.2",
|
|
56
|
+
"@types/estree": "^1.0.9",
|
|
57
|
+
"@typescript-eslint/parser": "^8.70.1",
|
|
58
|
+
"eslint": "^9.39.5",
|
|
59
|
+
"eslint10": "npm:eslint@^10",
|
|
60
|
+
"oxfmt": "0.70.0",
|
|
61
|
+
"oxlint": "^1.85.0",
|
|
62
|
+
"typescript": "~5.9.3"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"eslint": ">=9.0.0 <11"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"eslint": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=18.18.0"
|
|
74
|
+
},
|
|
75
|
+
"packageManager": "bun@1.4.2"
|
|
76
|
+
}
|