Skip to content

Commit 5609abb

Browse files
authoredFeb 7, 2024
feat(shebang): Add options to ignore unpublished files (#172)
* feat: Add shebangs to all ignored executable files * chore: Add names to shebang tests * fix: Ignore shebangs for all files not published * feat(shebang): Add "ignoreUnpublished" option * chore: Actually ignore test fixtures * chore: Remove import-maps module disable * feat(shebang): Add "additionalExecutables" option * docs(shebang): Add two new options to docs * chore(shebang): Only report the first line #85
1 parent cd5cbbb commit 5609abb

File tree

5 files changed

+193
-22
lines changed

5 files changed

+193
-22
lines changed
 

‎docs/rules/shebang.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ console.log("hello");
6161

6262
```json
6363
{
64-
"n/shebang": ["error", {"convertPath": null}]
64+
"n/shebang": ["error", {
65+
"convertPath": null,
66+
"ignoreUnpublished": false,
67+
"additionalExecutables": [],
68+
}]
6569
}
6670
```
6771

@@ -70,6 +74,14 @@ console.log("hello");
7074
This can be configured in the rule options or as a shared setting [`settings.convertPath`](../shared-settings.md#convertpath).
7175
Please see the shared settings documentation for more information.
7276

77+
#### ignoreUnpublished
78+
79+
Allow for files that are not published to npm to be ignored by this rule.
80+
81+
#### additionalExecutables
82+
83+
Mark files as executable that are not referenced by the package.json#bin property
84+
7385
## 🔎 Implementation
7486

7587
- [Rule source](../../lib/rules/shebang.js)

‎eslint.config.js

+1-4
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,14 @@ module.exports = [
1313
{
1414
languageOptions: { globals: globals.mocha },
1515
linterOptions: { reportUnusedDisableDirectives: true },
16-
settings: {
17-
n: { allowModules: ["#eslint-rule-tester"] }, // the plugin does not support import-maps yet.
18-
},
1916
},
2017
{
2118
ignores: [
2219
".nyc_output/",
2320
"coverage/",
2421
"docs/",
2522
"lib/converted-esm/",
26-
"test/fixtures/",
23+
"tests/fixtures/",
2724
],
2825
},
2926
js.configs.recommended,

‎lib/rules/shebang.js

+53-15
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
"use strict"
66

77
const path = require("path")
8+
const matcher = require("ignore")
9+
810
const getConvertPath = require("../util/get-convert-path")
911
const getPackageJson = require("../util/get-package-json")
12+
const getNpmignore = require("../util/get-npmignore")
1013

1114
const NODE_SHEBANG = "#!/usr/bin/env node\n"
1215
const SHEBANG_PATTERN = /^(#!.+?)?(\r)?\n/u
@@ -66,6 +69,7 @@ function getShebangInfo(sourceCode) {
6669
}
6770
}
6871

72+
/** @type {import('eslint').Rule.RuleModule} */
6973
module.exports = {
7074
meta: {
7175
docs: {
@@ -79,8 +83,12 @@ module.exports = {
7983
{
8084
type: "object",
8185
properties: {
82-
//
8386
convertPath: getConvertPath.schema,
87+
ignoreUnpublished: { type: "boolean" },
88+
additionalExecutables: {
89+
type: "array",
90+
items: { type: "string" },
91+
},
8492
},
8593
additionalProperties: false,
8694
},
@@ -95,30 +103,60 @@ module.exports = {
95103
},
96104
create(context) {
97105
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
98-
let filePath = context.filename ?? context.getFilename()
106+
const filePath = context.filename ?? context.getFilename()
99107
if (filePath === "<input>") {
100108
return {}
101109
}
102-
filePath = path.resolve(filePath)
103110

104111
const p = getPackageJson(filePath)
105112
if (!p) {
106113
return {}
107114
}
108115

109-
const basedir = path.dirname(p.filePath)
110-
filePath = path.join(
111-
basedir,
112-
getConvertPath(context)(
113-
path.relative(basedir, filePath).replace(/\\/gu, "/")
114-
)
116+
const packageDirectory = path.dirname(p.filePath)
117+
118+
const originalAbsolutePath = path.resolve(filePath)
119+
const originalRelativePath = path
120+
.relative(packageDirectory, originalAbsolutePath)
121+
.replace(/\\/gu, "/")
122+
123+
const convertedRelativePath =
124+
getConvertPath(context)(originalRelativePath)
125+
const convertedAbsolutePath = path.resolve(
126+
packageDirectory,
127+
convertedRelativePath
115128
)
116129

117-
const needsShebang = isBinFile(filePath, p.bin, basedir)
130+
const { additionalExecutables = [] } = context.options?.[0] ?? {}
131+
132+
const executable = matcher()
133+
executable.add(additionalExecutables)
134+
const isExecutable = executable.test(convertedRelativePath)
135+
136+
if (
137+
(additionalExecutables.length === 0 ||
138+
isExecutable.ignored === false) &&
139+
context.options?.[0]?.ignoreUnpublished === true
140+
) {
141+
const npmignore = getNpmignore(convertedAbsolutePath)
142+
143+
if (npmignore.match(convertedRelativePath)) {
144+
return {}
145+
}
146+
}
147+
148+
const needsShebang =
149+
isExecutable.ignored === true ||
150+
isBinFile(convertedAbsolutePath, p.bin, packageDirectory)
118151
const info = getShebangInfo(sourceCode)
119152

120153
return {
121-
Program(node) {
154+
Program() {
155+
const loc = {
156+
start: { line: 1, column: 0 },
157+
end: { line: 1, column: sourceCode.lines.at(0).length },
158+
}
159+
122160
if (
123161
needsShebang
124162
? NODE_SHEBANG_PATTERN.test(info.shebang)
@@ -128,7 +166,7 @@ module.exports = {
128166
// Checks BOM and \r.
129167
if (needsShebang && info.bom) {
130168
context.report({
131-
node,
169+
loc,
132170
messageId: "unexpectedBOM",
133171
fix(fixer) {
134172
return fixer.removeRange([-1, 0])
@@ -137,7 +175,7 @@ module.exports = {
137175
}
138176
if (needsShebang && info.cr) {
139177
context.report({
140-
node,
178+
loc,
141179
messageId: "expectedLF",
142180
fix(fixer) {
143181
const index = sourceCode.text.indexOf("\r")
@@ -148,7 +186,7 @@ module.exports = {
148186
} else if (needsShebang) {
149187
// Shebang is lacking.
150188
context.report({
151-
node,
189+
loc,
152190
messageId: "expectedHashbangNode",
153191
fix(fixer) {
154192
return fixer.replaceTextRange(
@@ -160,7 +198,7 @@ module.exports = {
160198
} else {
161199
// Shebang is extra.
162200
context.report({
163-
node,
201+
loc,
164202
messageId: "expectedHashbang",
165203
fix(fixer) {
166204
return fixer.removeRange([0, info.length])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "test",
3+
"version": "0.0.0",
4+
"files": [
5+
"./published.js"
6+
]
7+
}

‎tests/lib/rules/shebang.js

+119-2
Original file line numberDiff line numberDiff line change
@@ -17,152 +17,214 @@ function fixture(name) {
1717
return path.resolve(__dirname, "../../fixtures/shebang", name)
1818
}
1919

20+
/** @type {import('eslint').RuleTester} */
2021
const ruleTester = new RuleTester()
2122
ruleTester.run("shebang", rule, {
2223
valid: [
2324
{
25+
name: "string-bin/bin/test.js",
2426
filename: fixture("string-bin/bin/test.js"),
2527
code: "#!/usr/bin/env node\nhello();",
2628
},
2729
{
30+
name: "string-bin/lib/test.js",
2831
filename: fixture("string-bin/lib/test.js"),
2932
code: "hello();",
3033
},
3134
{
35+
name: "object-bin/bin/a.js",
3236
filename: fixture("object-bin/bin/a.js"),
3337
code: "#!/usr/bin/env node\nhello();",
3438
},
3539
{
40+
name: "object-bin/bin/b.js",
3641
filename: fixture("object-bin/bin/b.js"),
3742
code: "#!/usr/bin/env node\nhello();",
3843
},
3944
{
45+
name: "object-bin/bin/c.js",
4046
filename: fixture("object-bin/bin/c.js"),
4147
code: "hello();",
4248
},
4349
{
50+
name: "no-bin-field/lib/test.js",
4451
filename: fixture("no-bin-field/lib/test.js"),
4552
code: "hello();",
4653
},
47-
"#!/usr/bin/env node\nhello();",
48-
"hello();",
54+
{
55+
name: "<input> with shebang",
56+
code: "#!/usr/bin/env node\nhello();",
57+
},
58+
{
59+
name: "<input> without shebang",
60+
code: "hello();",
61+
},
4962

5063
// convertPath
5164
{
65+
name: "convertPath - string-bin/src/bin/test.js",
5266
filename: fixture("string-bin/src/bin/test.js"),
5367
code: "#!/usr/bin/env node\nhello();",
5468
options: [{ convertPath: { "src/**": ["^src/(.+)$", "$1"] } }],
5569
},
5670
{
71+
name: "convertPath - string-bin/src/lib/test.js",
5772
filename: fixture("string-bin/src/lib/test.js"),
5873
code: "hello();",
5974
options: [{ convertPath: { "src/**": ["^src/(.+)$", "$1"] } }],
6075
},
6176
{
77+
name: "convertPath - object-bin/src/bin/a.js",
6278
filename: fixture("object-bin/src/bin/a.js"),
6379
code: "#!/usr/bin/env node\nhello();",
6480
options: [{ convertPath: { "src/**": ["^src/(.+)$", "$1"] } }],
6581
},
6682
{
83+
name: "convertPath - object-bin/src/bin/b.js",
6784
filename: fixture("object-bin/src/bin/b.js"),
6885
code: "#!/usr/bin/env node\nhello();",
6986
options: [{ convertPath: { "src/**": ["^src/(.+)$", "$1"] } }],
7087
},
7188
{
89+
name: "convertPath - object-bin/src/bin/c.js",
7290
filename: fixture("object-bin/src/bin/c.js"),
7391
code: "hello();",
7492
options: [{ convertPath: { "src/**": ["^src/(.+)$", "$1"] } }],
7593
},
7694
{
95+
name: "convertPath - no-bin-field/src/lib/test.js",
7796
filename: fixture("no-bin-field/src/lib/test.js"),
7897
code: "hello();",
7998
options: [{ convertPath: { "src/**": ["^src/(.+)$", "$1"] } }],
8099
},
81100

82101
// Should work fine if the filename is relative.
83102
{
103+
name: "relative path - string-bin/bin/test.js",
84104
filename: "tests/fixtures/shebang/string-bin/bin/test.js",
85105
code: "#!/usr/bin/env node\nhello();",
86106
},
87107
{
108+
name: "relative path - string-bin/lib/test.js",
88109
filename: "tests/fixtures/shebang/string-bin/lib/test.js",
89110
code: "hello();",
90111
},
91112

92113
// BOM and \r\n
93114
{
115+
name: "BOM without newline",
94116
filename: fixture("string-bin/lib/test.js"),
95117
code: "\uFEFFhello();",
96118
},
97119
{
120+
name: "BOM with newline",
98121
filename: fixture("string-bin/lib/test.js"),
99122
code: "\uFEFFhello();\n",
100123
},
101124
{
125+
name: "with windows newline",
102126
filename: fixture("string-bin/lib/test.js"),
103127
code: "hello();\r\n",
104128
},
105129
{
130+
name: "BOM with windows newline",
106131
filename: fixture("string-bin/lib/test.js"),
107132
code: "\uFEFFhello();\r\n",
108133
},
109134

110135
// blank lines on the top of files.
111136
{
137+
name: "blank lines on the top of files.",
112138
filename: fixture("string-bin/lib/test.js"),
113139
code: "\n\n\nhello();",
114140
},
115141

116142
// https://github.com/mysticatea/eslint-plugin-node/issues/51
117143
{
144+
name: "Shebang with CLI flags",
118145
filename: fixture("string-bin/bin/test.js"),
119146
code: "#!/usr/bin/env node --harmony\nhello();",
120147
},
121148

122149
// use node resolution
123150
{
151+
name: "use node resolution",
124152
filename: fixture("object-bin/bin/index.js"),
125153
code: "#!/usr/bin/env node\nhello();",
126154
},
155+
156+
// npm unpublished files are ignored
157+
{
158+
name: "published file cant have shebang",
159+
filename: fixture("unpublished/published.js"),
160+
code: "hello();",
161+
options: [{ ignoreUnpublished: true }],
162+
},
163+
{
164+
name: "unpublished file can have shebang",
165+
filename: fixture("unpublished/unpublished.js"),
166+
code: "#!/usr/bin/env node\nhello();",
167+
options: [{ ignoreUnpublished: true }],
168+
},
169+
{
170+
name: "unpublished file can have noshebang",
171+
filename: fixture("unpublished/unpublished.js"),
172+
code: "hello();",
173+
options: [{ ignoreUnpublished: true }],
174+
},
175+
176+
{
177+
name: "file matching additionalExecutables",
178+
filename: fixture("unpublished/something.test.js"),
179+
code: "#!/usr/bin/env node\nhello();",
180+
options: [{ additionalExecutables: ["*.test.js"] }],
181+
},
127182
],
128183
invalid: [
129184
{
185+
name: "bin: string - match - no shebang",
130186
filename: fixture("string-bin/bin/test.js"),
131187
code: "hello();",
132188
output: "#!/usr/bin/env node\nhello();",
133189
errors: ['This file needs shebang "#!/usr/bin/env node".'],
134190
},
135191
{
192+
name: "bin: string - match - incorrect shebang",
136193
filename: fixture("string-bin/bin/test.js"),
137194
code: "#!/usr/bin/node\nhello();",
138195
output: "#!/usr/bin/env node\nhello();",
139196
errors: ['This file needs shebang "#!/usr/bin/env node".'],
140197
},
141198
{
199+
name: "bin: string - no match - with shebang",
142200
filename: fixture("string-bin/lib/test.js"),
143201
code: "#!/usr/bin/env node\nhello();",
144202
output: "hello();",
145203
errors: ["This file needs no shebang."],
146204
},
147205
{
206+
name: 'bin: {a: "./bin/a.js"} - match - no shebang',
148207
filename: fixture("object-bin/bin/a.js"),
149208
code: "hello();",
150209
output: "#!/usr/bin/env node\nhello();",
151210
errors: ['This file needs shebang "#!/usr/bin/env node".'],
152211
},
153212
{
213+
name: 'bin: {b: "./bin/b.js"} - match - no shebang',
154214
filename: fixture("object-bin/bin/b.js"),
155215
code: "#!/usr/bin/node\nhello();",
156216
output: "#!/usr/bin/env node\nhello();",
157217
errors: ['This file needs shebang "#!/usr/bin/env node".'],
158218
},
159219
{
220+
name: 'bin: {c: "./bin"} - no match - with shebang',
160221
filename: fixture("object-bin/bin/c.js"),
161222
code: "#!/usr/bin/env node\nhello();",
162223
output: "hello();",
163224
errors: ["This file needs no shebang."],
164225
},
165226
{
227+
name: "bin: undefined - no match - with shebang",
166228
filename: fixture("no-bin-field/lib/test.js"),
167229
code: "#!/usr/bin/env node\nhello();",
168230
output: "hello();",
@@ -171,13 +233,15 @@ ruleTester.run("shebang", rule, {
171233

172234
// convertPath
173235
{
236+
name: "convertPath in options",
174237
filename: fixture("string-bin/src/bin/test.js"),
175238
code: "hello();",
176239
output: "#!/usr/bin/env node\nhello();",
177240
options: [{ convertPath: { "src/**": ["^src/(.+)$", "$1"] } }],
178241
errors: ['This file needs shebang "#!/usr/bin/env node".'],
179242
},
180243
{
244+
name: "convertPath in settings",
181245
filename: fixture("string-bin/src/bin/test.js"),
182246
code: "hello();",
183247
output: "#!/usr/bin/env node\nhello();",
@@ -187,41 +251,47 @@ ruleTester.run("shebang", rule, {
187251
},
188252
},
189253
{
254+
name: "converted path - string-bin/src/bin/test.js",
190255
filename: fixture("string-bin/src/bin/test.js"),
191256
code: "#!/usr/bin/node\nhello();",
192257
output: "#!/usr/bin/env node\nhello();",
193258
options: [{ convertPath: { "src/**": ["^src/(.+)$", "$1"] } }],
194259
errors: ['This file needs shebang "#!/usr/bin/env node".'],
195260
},
196261
{
262+
name: "converted path - string-bin/src/lib/test.js",
197263
filename: fixture("string-bin/src/lib/test.js"),
198264
code: "#!/usr/bin/env node\nhello();",
199265
output: "hello();",
200266
options: [{ convertPath: { "src/**": ["^src/(.+)$", "$1"] } }],
201267
errors: ["This file needs no shebang."],
202268
},
203269
{
270+
name: "converted path - object-bin/src/bin/a.js",
204271
filename: fixture("object-bin/src/bin/a.js"),
205272
code: "hello();",
206273
output: "#!/usr/bin/env node\nhello();",
207274
options: [{ convertPath: { "src/**": ["^src/(.+)$", "$1"] } }],
208275
errors: ['This file needs shebang "#!/usr/bin/env node".'],
209276
},
210277
{
278+
name: "converted path - object-bin/src/bin/b.js",
211279
filename: fixture("object-bin/src/bin/b.js"),
212280
code: "#!/usr/bin/node\nhello();",
213281
output: "#!/usr/bin/env node\nhello();",
214282
options: [{ convertPath: { "src/**": ["^src/(.+)$", "$1"] } }],
215283
errors: ['This file needs shebang "#!/usr/bin/env node".'],
216284
},
217285
{
286+
name: "converted path - object-bin/src/bin/c.js",
218287
filename: fixture("object-bin/src/bin/c.js"),
219288
code: "#!/usr/bin/env node\nhello();",
220289
output: "hello();",
221290
options: [{ convertPath: { "src/**": ["^src/(.+)$", "$1"] } }],
222291
errors: ["This file needs no shebang."],
223292
},
224293
{
294+
name: "converted path - no-bin-field/src/lib/test.js",
225295
filename: fixture("no-bin-field/src/lib/test.js"),
226296
code: "#!/usr/bin/env node\nhello();",
227297
output: "hello();",
@@ -231,12 +301,14 @@ ruleTester.run("shebang", rule, {
231301

232302
// Should work fine if the filename is relative.
233303
{
304+
name: "relative path - string-bin/bin/test.js",
234305
filename: "tests/fixtures/shebang/string-bin/bin/test.js",
235306
code: "hello();",
236307
output: "#!/usr/bin/env node\nhello();",
237308
errors: ['This file needs shebang "#!/usr/bin/env node".'],
238309
},
239310
{
311+
name: "relative path - string-bin/lib/test.js",
240312
filename: "tests/fixtures/shebang/string-bin/lib/test.js",
241313
code: "#!/usr/bin/env node\nhello();",
242314
output: "hello();",
@@ -245,6 +317,7 @@ ruleTester.run("shebang", rule, {
245317

246318
// header comments
247319
{
320+
name: "header comments",
248321
filename: fixture("string-bin/bin/test.js"),
249322
code: "/* header */\nhello();",
250323
output: "#!/usr/bin/env node\n/* header */\nhello();",
@@ -306,6 +379,7 @@ ruleTester.run("shebang", rule, {
306379

307380
// https://github.com/mysticatea/eslint-plugin-node/issues/51
308381
{
382+
name: "Shebang with CLI flags",
309383
filename: fixture("string-bin/lib/test.js"),
310384
code: "#!/usr/bin/env node --harmony\nhello();",
311385
output: "hello();",
@@ -314,10 +388,53 @@ ruleTester.run("shebang", rule, {
314388

315389
// use node resolution
316390
{
391+
name: "use node resolution",
317392
filename: fixture("object-bin/bin/index.js"),
318393
code: "hello();",
319394
output: "#!/usr/bin/env node\nhello();",
320395
errors: ['This file needs shebang "#!/usr/bin/env node".'],
321396
},
397+
398+
// npm unpublished files are ignored
399+
{
400+
name: "unpublished file should not have shebang",
401+
filename: fixture("unpublished/unpublished.js"),
402+
code: "#!/usr/bin/env node\nhello();",
403+
output: "hello();",
404+
errors: ["This file needs no shebang."],
405+
},
406+
{
407+
name: "published file should have shebang",
408+
filename: fixture("unpublished/published.js"),
409+
code: "#!/usr/bin/env node\nhello();",
410+
output: "hello();",
411+
errors: ["This file needs no shebang."],
412+
},
413+
414+
{
415+
name: "unpublished file shebang ignored",
416+
filename: fixture("unpublished/published.js"),
417+
code: "#!/usr/bin/env node\nhello();",
418+
options: [{ ignoreUnpublished: true }],
419+
output: "hello();",
420+
errors: ["This file needs no shebang."],
421+
},
422+
423+
{
424+
name: "executable in additionalExecutables without shebang",
425+
filename: fixture("unpublished/something.test.js"),
426+
code: "hello();",
427+
options: [{ additionalExecutables: ["*.test.js"] }],
428+
output: "#!/usr/bin/env node\nhello();",
429+
errors: ['This file needs shebang "#!/usr/bin/env node".'],
430+
},
431+
{
432+
name: "file not in additionalExecutables with shebang",
433+
filename: fixture("unpublished/not-a-test.js"),
434+
code: "#!/usr/bin/env node\nhello();",
435+
options: [{ additionalExecutables: ["*.test.js"] }],
436+
output: "hello();",
437+
errors: ["This file needs no shebang."],
438+
},
322439
],
323440
})

0 commit comments

Comments
 (0)
Please sign in to comment.