Skip to content

Commit 5609abb

Browse files
authored
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

Lines changed: 13 additions & 1 deletion
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

Lines changed: 1 addition & 4 deletions
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

Lines changed: 53 additions & 15 deletions
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])
Lines changed: 7 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)