Skip to content

Commit 3fd7639

Browse files
authoredMay 14, 2024··
feat(hashbang): Add support to map extensions to executables (#278)
* test: Rename shebang tests to be more verbose * chore: Prepare for #220 by making shebang checks more verbose * feat(hashbang): Add support to map extensions to executables * chore: remove "\b" in char group * docs(hashbang): Add docs for "executableMap"
1 parent 704f0b9 commit 3fd7639

File tree

5 files changed

+116
-82
lines changed

5 files changed

+116
-82
lines changed
 

‎docs/rules/hashbang.md

+15
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ console.log("hello");
6565
"convertPath": null,
6666
"ignoreUnpublished": false,
6767
"additionalExecutables": [],
68+
"executableMap": {
69+
".js": "node"
70+
}
6871
}]
6972
}
7073
```
@@ -82,6 +85,18 @@ Allow for files that are not published to npm to be ignored by this rule.
8285

8386
Mark files as executable that are not referenced by the package.json#bin property
8487

88+
#### executableMap
89+
90+
Allow for different executables to be used based on file extension.
91+
This is in the form `"{extension}": "{binaryName}"`.
92+
93+
```js
94+
{
95+
".js": "node",
96+
".ts": "ts-node"
97+
}
98+
```
99+
85100
## 🔎 Implementation
86101

87102
- [Rule source](../../lib/rules/hashbang.js)

‎docs/rules/shebang.md

+1-70
Original file line numberDiff line numberDiff line change
@@ -11,76 +11,7 @@ This rule suggests correct usage of shebang.
1111

1212
## 📖 Rule Details
1313

14-
This rule looks up `package.json` file from each linting target file.
15-
Starting from the directory of the target file, it goes up ancestor directories until found.
16-
17-
If `package.json` was not found, this rule does nothing.
18-
19-
This rule checks `bin` field of `package.json`, then if a target file matches one of `bin` files, it checks whether or not there is a correct shebang.
20-
Otherwise it checks whether or not there is not a shebang.
21-
22-
The following patterns are considered problems for files in `bin` field of `package.json`:
23-
24-
```js
25-
console.log("hello"); /*error This file needs shebang "#!/usr/bin/env node".*/
26-
```
27-
28-
```js
29-
#!/usr/bin/env node /*error This file must not have Unicode BOM.*/
30-
console.log("hello");
31-
// If this file has Unicode BOM.
32-
```
33-
34-
```js
35-
#!/usr/bin/env node /*error This file must have Unix linebreaks (LF).*/
36-
console.log("hello");
37-
// If this file has Windows' linebreaks (CRLF).
38-
```
39-
40-
The following patterns are considered problems for other files:
41-
42-
```js
43-
#!/usr/bin/env node /*error This file needs no shebang.*/
44-
console.log("hello");
45-
```
46-
47-
The following patterns are not considered problems for files in `bin` field of `package.json`:
48-
49-
```js
50-
#!/usr/bin/env node
51-
console.log("hello");
52-
```
53-
54-
The following patterns are not considered problems for other files:
55-
56-
```js
57-
console.log("hello");
58-
```
59-
60-
### Options
61-
62-
```json
63-
{
64-
"n/shebang": ["error", {
65-
"convertPath": null,
66-
"ignoreUnpublished": false,
67-
"additionalExecutables": [],
68-
}]
69-
}
70-
```
71-
72-
#### convertPath
73-
74-
This can be configured in the rule options or as a shared setting [`settings.convertPath`](../shared-settings.md#convertpath).
75-
Please see the shared settings documentation for more information.
76-
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
14+
The details for this rule can be found in [docs/rules/hashbang.md](https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/hashbang.md#-rule-details)
8415

8516
## 🔎 Implementation
8617

‎lib/rules/hashbang.js

+61-6
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,50 @@ const { getPackageJson } = require("../util/get-package-json")
1212
const getNpmignore = require("../util/get-npmignore")
1313
const { isBinFile } = require("../util/is-bin-file")
1414

15-
const NODE_SHEBANG = "#!/usr/bin/env node\n"
15+
const ENV_SHEBANG = "#!/usr/bin/env"
16+
const NODE_SHEBANG = `${ENV_SHEBANG} {{executableName}}\n`
1617
const SHEBANG_PATTERN = /^(#!.+?)?(\r)?\n/u
17-
const NODE_SHEBANG_PATTERN =
18-
/^#!\/usr\/bin\/env(?: -\S+)*(?: [^\s=-]+=\S+)* node(?: [^\r\n]+?)?\n/u
18+
19+
// -i -S
20+
// -u name
21+
// --ignore-environment
22+
// --block-signal=SIGINT
23+
const ENV_FLAGS = /^\s*-(-.*?\b|[ivS]+|[Pu](\s+|=)\S+)(?=\s|$)/
24+
25+
// NAME="some variable"
26+
// FOO=bar
27+
const ENV_VARS = /^\s*\w+=(?:"(?:[^"\\]|\\.)*"|\w+)/
28+
29+
/**
30+
* @param {string} shebang
31+
* @param {string} executableName
32+
* @returns {boolean}
33+
*/
34+
function isNodeShebang(shebang, executableName) {
35+
if (shebang == null || shebang.length === 0) {
36+
return false
37+
}
38+
39+
shebang = shebang.slice(shebang.indexOf(ENV_SHEBANG) + ENV_SHEBANG.length)
40+
while (ENV_FLAGS.test(shebang) || ENV_VARS.test(shebang)) {
41+
shebang = shebang.replace(ENV_FLAGS, "").replace(ENV_VARS, "")
42+
}
43+
44+
const [command] = shebang.trim().split(" ")
45+
return command === executableName
46+
}
47+
48+
/**
49+
* @param {import('eslint').Rule.RuleContext} context The rule context.
50+
* @returns {string}
51+
*/
52+
function getExpectedExecutableName(context) {
53+
const extension = path.extname(context.filename)
54+
/** @type {{ executableMap: Record<string, string> }} */
55+
const { executableMap = {} } = context.options?.[0] ?? {}
56+
57+
return executableMap[extension] ?? "node"
58+
}
1959

2060
/**
2161
* Gets the shebang line (includes a line ending) from a given code.
@@ -56,6 +96,16 @@ module.exports = {
5696
type: "array",
5797
items: { type: "string" },
5898
},
99+
executableMap: {
100+
type: "object",
101+
patternProperties: {
102+
"^\\.\\w+$": {
103+
type: "string",
104+
pattern: "^[\\w-]+$",
105+
},
106+
},
107+
additionalProperties: false,
108+
},
59109
},
60110
additionalProperties: false,
61111
},
@@ -64,7 +114,7 @@ module.exports = {
64114
unexpectedBOM: "This file must not have Unicode BOM.",
65115
expectedLF: "This file must have Unix linebreaks (LF).",
66116
expectedHashbangNode:
67-
'This file needs shebang "#!/usr/bin/env node".',
117+
'This file needs shebang "#!/usr/bin/env {{executableName}}".',
68118
expectedHashbang: "This file needs no shebang.",
69119
},
70120
},
@@ -116,6 +166,7 @@ module.exports = {
116166
const needsShebang =
117167
isExecutable.ignored === true ||
118168
isBinFile(convertedAbsolutePath, packageJson?.bin, packageDirectory)
169+
const executableName = getExpectedExecutableName(context)
119170
const info = getShebangInfo(sourceCode)
120171

121172
return {
@@ -130,7 +181,7 @@ module.exports = {
130181

131182
if (
132183
needsShebang
133-
? NODE_SHEBANG_PATTERN.test(info.shebang)
184+
? isNodeShebang(info.shebang, executableName)
134185
: !info.shebang
135186
) {
136187
// Good the shebang target.
@@ -159,10 +210,14 @@ module.exports = {
159210
context.report({
160211
loc,
161212
messageId: "expectedHashbangNode",
213+
data: { executableName },
162214
fix(fixer) {
163215
return fixer.replaceTextRange(
164216
[-1, info.length],
165-
NODE_SHEBANG
217+
NODE_SHEBANG.replaceAll(
218+
"{{executableName}}",
219+
executableName
220+
)
166221
)
167222
},
168223
})

‎tests/fixtures/shebang/object-bin/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"bin": {
55
"a": "./bin/a.js",
66
"b": "./bin/b.js",
7-
"c": "./bin"
7+
"c": "./bin",
8+
"t": "./bin/t.ts"
89
}
910
}

‎tests/lib/rules/hashbang.js

+37-5
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,27 @@ ruleTester.run("shebang", rule, {
4242
code: "#!/usr/bin/env node\nhello();",
4343
},
4444
{
45-
name: "string-bin/bin/test.js",
45+
name: "string-bin/bin/test-env-flag.js",
4646
filename: fixture("string-bin/bin/test.js"),
4747
code: "#!/usr/bin/env -S node\nhello();",
4848
},
4949
{
50-
name: "string-bin/bin/test.js",
50+
name: "string-bin/bin/test-env-flag-node-flag.js",
5151
filename: fixture("string-bin/bin/test.js"),
5252
code: "#!/usr/bin/env -S node --loader tsm\nhello();",
5353
},
5454
{
55-
name: "string-bin/bin/test.js",
55+
name: "string-bin/bin/test-env-ignore-environment.js",
5656
filename: fixture("string-bin/bin/test.js"),
5757
code: "#!/usr/bin/env --ignore-environment node\nhello();",
5858
},
5959
{
60-
name: "string-bin/bin/test.js",
60+
name: "string-bin/bin/test-env-flags-node-flag.js",
6161
filename: fixture("string-bin/bin/test.js"),
6262
code: "#!/usr/bin/env -i -S node --loader tsm\nhello();",
6363
},
6464
{
65-
name: "string-bin/bin/test.js",
65+
name: "string-bin/bin/test-block-signal.js",
6666
filename: fixture("string-bin/bin/test.js"),
6767
code: "#!/usr/bin/env --block-signal=SIGINT -S FOO=bar node --loader tsm\nhello();",
6868
},
@@ -204,6 +204,20 @@ ruleTester.run("shebang", rule, {
204204
code: "#!/usr/bin/env node\nhello();",
205205
options: [{ additionalExecutables: ["*.test.js"] }],
206206
},
207+
208+
// executableMap
209+
{
210+
name: ".ts maps to ts-node",
211+
filename: fixture("object-bin/bin/t.ts"),
212+
code: "#!/usr/bin/env ts-node\nhello();",
213+
options: [{ executableMap: { ".ts": "ts-node" } }],
214+
},
215+
{
216+
name: ".ts maps to ts-node",
217+
filename: fixture("object-bin/bin/a.js"),
218+
code: "#!/usr/bin/env node\nhello();",
219+
options: [{ executableMap: { ".ts": "ts-node" } }],
220+
},
207221
],
208222
invalid: [
209223
{
@@ -461,5 +475,23 @@ ruleTester.run("shebang", rule, {
461475
output: "hello();",
462476
errors: ["This file needs no shebang."],
463477
},
478+
479+
// executableMap
480+
{
481+
name: ".ts maps to ts-node",
482+
filename: fixture("object-bin/bin/t.ts"),
483+
code: "hello();",
484+
options: [{ executableMap: { ".ts": "ts-node" } }],
485+
output: "#!/usr/bin/env ts-node\nhello();",
486+
errors: ['This file needs shebang "#!/usr/bin/env ts-node".'],
487+
},
488+
{
489+
name: ".ts maps to ts-node",
490+
filename: fixture("object-bin/bin/t.ts"),
491+
code: "#!/usr/bin/env node\nhello();",
492+
options: [{ executableMap: { ".ts": "ts-node" } }],
493+
output: "#!/usr/bin/env ts-node\nhello();",
494+
errors: ['This file needs shebang "#!/usr/bin/env ts-node".'],
495+
},
464496
],
465497
})

0 commit comments

Comments
 (0)
Please sign in to comment.