Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(shebang): Add options to ignore unpublished files #172

Merged
merged 9 commits into from
Feb 7, 2024
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"
]
}