Skip to content

Commit

Permalink
feat(shebang): Add options to ignore unpublished files (#172)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
scagood authored Feb 7, 2024
1 parent cd5cbbb commit 5609abb
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 22 deletions.
14 changes: 13 additions & 1 deletion docs/rules/shebang.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ console.log("hello");

```json
{
"n/shebang": ["error", {"convertPath": null}]
"n/shebang": ["error", {
"convertPath": null,
"ignoreUnpublished": false,
"additionalExecutables": [],
}]
}
```

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

#### ignoreUnpublished

Allow for files that are not published to npm to be ignored by this rule.

#### additionalExecutables

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

## 🔎 Implementation

- [Rule source](../../lib/rules/shebang.js)
Expand Down
5 changes: 1 addition & 4 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,14 @@ module.exports = [
{
languageOptions: { globals: globals.mocha },
linterOptions: { reportUnusedDisableDirectives: true },
settings: {
n: { allowModules: ["#eslint-rule-tester"] }, // the plugin does not support import-maps yet.
},
},
{
ignores: [
".nyc_output/",
"coverage/",
"docs/",
"lib/converted-esm/",
"test/fixtures/",
"tests/fixtures/",
],
},
js.configs.recommended,
Expand Down
68 changes: 53 additions & 15 deletions lib/rules/shebang.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
"use strict"

const path = require("path")
const matcher = require("ignore")

const getConvertPath = require("../util/get-convert-path")
const getPackageJson = require("../util/get-package-json")
const getNpmignore = require("../util/get-npmignore")

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

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
Expand All @@ -79,8 +83,12 @@ module.exports = {
{
type: "object",
properties: {
//
convertPath: getConvertPath.schema,
ignoreUnpublished: { type: "boolean" },
additionalExecutables: {
type: "array",
items: { type: "string" },
},
},
additionalProperties: false,
},
Expand All @@ -95,30 +103,60 @@ module.exports = {
},
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9
let filePath = context.filename ?? context.getFilename()
const filePath = context.filename ?? context.getFilename()
if (filePath === "<input>") {
return {}
}
filePath = path.resolve(filePath)

const p = getPackageJson(filePath)
if (!p) {
return {}
}

const basedir = path.dirname(p.filePath)
filePath = path.join(
basedir,
getConvertPath(context)(
path.relative(basedir, filePath).replace(/\\/gu, "/")
)
const packageDirectory = path.dirname(p.filePath)

const originalAbsolutePath = path.resolve(filePath)
const originalRelativePath = path
.relative(packageDirectory, originalAbsolutePath)
.replace(/\\/gu, "/")

const convertedRelativePath =
getConvertPath(context)(originalRelativePath)
const convertedAbsolutePath = path.resolve(
packageDirectory,
convertedRelativePath
)

const needsShebang = isBinFile(filePath, p.bin, basedir)
const { additionalExecutables = [] } = context.options?.[0] ?? {}

const executable = matcher()
executable.add(additionalExecutables)
const isExecutable = executable.test(convertedRelativePath)

if (
(additionalExecutables.length === 0 ||
isExecutable.ignored === false) &&
context.options?.[0]?.ignoreUnpublished === true
) {
const npmignore = getNpmignore(convertedAbsolutePath)

if (npmignore.match(convertedRelativePath)) {
return {}
}
}

const needsShebang =
isExecutable.ignored === true ||
isBinFile(convertedAbsolutePath, p.bin, packageDirectory)
const info = getShebangInfo(sourceCode)

return {
Program(node) {
Program() {
const loc = {
start: { line: 1, column: 0 },
end: { line: 1, column: sourceCode.lines.at(0).length },
}

if (
needsShebang
? NODE_SHEBANG_PATTERN.test(info.shebang)
Expand All @@ -128,7 +166,7 @@ module.exports = {
// Checks BOM and \r.
if (needsShebang && info.bom) {
context.report({
node,
loc,
messageId: "unexpectedBOM",
fix(fixer) {
return fixer.removeRange([-1, 0])
Expand All @@ -137,7 +175,7 @@ module.exports = {
}
if (needsShebang && info.cr) {
context.report({
node,
loc,
messageId: "expectedLF",
fix(fixer) {
const index = sourceCode.text.indexOf("\r")
Expand All @@ -148,7 +186,7 @@ module.exports = {
} else if (needsShebang) {
// Shebang is lacking.
context.report({
node,
loc,
messageId: "expectedHashbangNode",
fix(fixer) {
return fixer.replaceTextRange(
Expand All @@ -160,7 +198,7 @@ module.exports = {
} else {
// Shebang is extra.
context.report({
node,
loc,
messageId: "expectedHashbang",
fix(fixer) {
return fixer.removeRange([0, info.length])
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/shebang/unpublished/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "test",
"version": "0.0.0",
"files": [
"./published.js"
]
}
Loading

0 comments on commit 5609abb

Please sign in to comment.