lint-staged / lint-staged

🚫💩 — Run linters on git staged files

Home Page:https://www.npmjs.com/package/lint-staged

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

lint-staged ignores tsconfig.json when it called through husky hooks

SerkanSipahi opened this issue · comments

Description

lint-staged ignores tsconfig.json when it called through husky hooks (see "Steps to reproduce")

Steps to reproduce

Create these files (test.js, tsconfig, package.json):

test.ts

export class Test {
  static get observedAttributes() {
    return ['foo'];
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ESNEXT"
  }
}

package.json

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{js,ts}": [
      "tsc --noEmit"
    ]
  }
}

Then on command line write:

// everything works fine (it uses tsconfig.json)!
tsc --noEmit

// throw an error because it ignores tsconfig.json
git commit // error TS1056: Accessors are only available when targeting ECMAScript 5 and higher

Environment

  • OS: macOS Catalina
  • Node.js: v13
  • lint-staged: v10.1.0

Can you try explicitly setting the config file for tsc?

"lint-staged": {
    "*.{js,ts}": [
      "tsc -p tsconfig.json --noEmit"
    ]
  }

We have a couple of issues like this, and I can't say for certain what causes it. I assume it might be a wrong working directory.

Also, please post your debug logs by enabling them:

"husky": {
    "hooks": {
      "pre-commit": "lint-staged --debug"
    }
  },

Do you by chance have tsc installed locally in the project, or globally? This might be something that affects it.

It's because you get the filenames passed as an argument. Try using the function syntax.
This is what I use:

// lint-staged.config.js
module.exports = {
  "*.{js,jsx}": [
    "eslint --cache --fix",
  ],
  "*.{ts,tsx}": [
    () => "tsc --skipLibCheck --noEmit", 
    "eslint --cache --fix",
  ],
}

Similar issue with tsc.
Passing files AND project config is not possible.

When invoked from husky, it does not find the project folder or configuration :$
When invoked from bash, it finds the project folder... but the config prevents passing files as args.

Here's my setup. It works as long as all changes are staged :P

// package.json
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "yarn eslint --quiet --fix",
      "bash -c tsc --noEmit" // notice bash!
    ]
  },
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
      "pre-commit": "lint-staged"
    }
  },
// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  // notice the filters!
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

same issue

@sombreroEnPuntas thank you! it's working for me

adding bash fixed it for me, thanks @sombreroEnPuntas

 "*.{ts,tsx}": [
    "npm run lint",
    "bash -c 'npm run check-types'"
],

@antoinerousseau thanks! using it in a js file with a function syntax works.
Hardcoding bash is not really a good solution for crossplatform development.

Thank you all 👍

Seems to have been solved. If someone wants to open a PR about adding this to the README, we'd appreciate it!

The bash solution helped me. Please don't forget to add single qoutes around the command itself (after bash -c)

"*.{ts,tsx}": [
    "npm run lint",
    "bash -c 'npm run check-types'"
]

@sombreroEnPuntas
basch -c tsc --noEmit I think this means that it's not only running tsc on the staged files but the whole project right?

commented

I've tried lots of different ways to get this working on a project (gatsby)

"lint-staged": {
    "*/**/*.{js,ts,tsx}": [
      "yarn type-check",
      "eslint"
    ]

but when i run this i get weird react native errors when the project isn't react native

node_modules/@types/react-native/globals.d.ts(40,15): error TS2300: Duplicate identifier 'FormData'.
node_modules/@types/react-native/globals.d.ts(85,5): error TS2717: Subsequent property declarations must have the same type.  Property 'body' must be of type 'BodyInit', but here has type 'string | ArrayBuffer | DataView | Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | ... 6 more ... | FormData'.
node_modules/@types/react-native/globals.d.ts(112,14): error TS2300: Duplicate identifier 'RequestInfo'.
node_modules/@types/react-native/globals.d.ts(131,13): error TS2403: Subsequent variable declarations must have the same type.  Variable 'Response' must be of type '{ new (body?: BodyInit, init?: ResponseInit): Response; prototype: Response; error(): Response; redirect(url: string, status?: number): Response; }', but here has type '{ new (body?: string | ArrayBuffer | DataView | Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | ... 6 more ... | FormData, init?: ResponseInit): Response; prototype: Response; error: () => Response; redirect: (url: string, status?: number) => Response; }'.
node_modules/@types/react-native/globals.d.ts(207,5): error TS2717: Subsequent property declarations must have the same type.  Property 'abort' must be of type 'ProgressEvent<XMLHttpRequestEventTarget>', but here has type 'ProgressEvent<EventTarget>'.
node_modules/@types/react-native/globals.d.ts(208,5): error TS2717: Subsequent property declarations must have the same type.  Property 'error' must be of type 'ProgressEvent<XMLHttpRequestEventTarget>', but here has type 'ProgressEvent<EventTarget>'.
node_modules/@types/react-native/globals.d.ts(209,5): error TS2717: Subsequent property declarations must have the same type.  Property 'load' must be of type 'ProgressEvent<XMLHttpRequestEventTarget>', but here has type 'ProgressEvent<EventTarget>'.
node_modules/@types/react-native/globals.d.ts(210,5): error TS2717: Subsequent property declarations must have the same type.  Property 'loadend' must be of type 'ProgressEvent<XMLHttpRequestEventTarget>', but here has type 'ProgressEvent<EventTarget>'.
node_modules/@types/react-native/globals.d.ts(211,5): error TS2717: Subsequent property declarations must have the same type.  Property 'loadstart' must be of type 'ProgressEvent<XMLHttpRequestEventTarget>', but here has type 'ProgressEvent<EventTarget>'.
node_modules/@types/react-native/globals.d.ts(212,5): error TS2717: Subsequent property declarations must have the same type.  Property 'progress' must be of type 'ProgressEvent<XMLHttpRequestEventTarget>', but here has type 'ProgressEvent<EventTarget>'.
node_modules/@types/react-native/globals.d.ts(213,5): error TS2717: Subsequent property declarations must have the same type.  Property 'timeout' must be of type```

any ideas?

I managed to get around this by emulating the config with the appropriate flags for the tsc command.

const tscFlags = [
  "--target es5",
  "--allowJs",
  "--skipLibCheck",
  "--strict",
  "--forceConsistentCasingInFileNames",
  "--noEmit",
  "--esModuleInterop",
  "--module esnext",
  "--moduleResolution node",
  "--resolveJsonModule",
  "--isolatedModules",
  "--noImplicitAny",
  "--jsx preserve",
];

module.exports = {
  "**/*.ts?(x)": (filenames) =>
    `tsc ${tscFlags.join(" ")} ${filenames.map((fN) => `"${fN}"`).join(" ")}`,
  "**/*.(ts|js)?(x)": () => [
    "prettier --ignore-unknown --write",
    "npm run lint",
  ],
};

Not the most elegant solution😅 But works like a charm telling the TypeScript compiler to only compile files that are staged, while mapping onto the output of your actual tsconfig.json.

Also! You'll notice the string escaping of the file names. 👀 That is to get around issues on Windows where paths to the files contain spaces. (In my case my username)

I know it's little bit late for this issue, but I want to share my solution for this problem.

First of all, there are only 2 ways to use tsc to do type checking:

  • tsc xxx.ts --noEmit only for some specific files
  • tsc -p ./tsconfig.json for those files that defined in the include field

When you use lint-staged, it will append the key (which is like **/*.ts) to your command, it would be like this tsc --noEmit **/.*.ts, which is perfectly fine for the first case I said.

But here's the thing: if you only changing one file like people.ts that has the import syntax: import Button from 'button', and the command will looks like this tsc --noEmit people.ts, and it will ignore the button.ts file! That would cause the problem even if you know it was right.

Some people would suggest the second case to do the type checking, like changing the setting to be:

'**/*.{ts,tsx}': [
  () => "tsc-files --noEmit -p tsconfig.json",
  "eslint --cache --fix",
],

So that it would ignore the **/*.ts pattern, but tsc would scan the whole project to slow down your commit.

It seems like this is a dilemma. I would suggest putting the tsc -p tsconfig.json --noEmit --skipLibCheck in pre-push using husky:

husky add .husky/pre-push
# pre-push
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

tsc -p ./tsconfig.json --noEmit --skipLibCheck

Yet another 🚲 which allows to run tsc only on staged files.

package.json

"scripts": {
	"lint-staged": "lint-staged"
},
"devDependencies": {
	"husky": "^7.0.4",
	"lint-staged": "^12.3.4",
	"vue-tsc": "^0.31.4"
}

vue-tsc is like tsc, but also for .vue type cheking
why lint-staged is in scripts? see below.

.lintstagedrc.js

module.exports = {
  "*.{ts,vue}": [
    () => "./node_modules/.bin/vue-tsc --noEmit",
    "./node_modules/.bin/eslint -f stylish --fix"
  ]
}

.husky/pre-commit

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# 🚲 stash unstaged changes so vue-tsc won't cover 'em, then pop 'em after
# https://stackoverflow.com/a/71150883/12496886
# https://github.com/okonet/lint-staged/issues/825
unstaged_files=$(git diff --name-only | tr '\n' ' ');

if [[ -n $unstaged_files ]]; then
    (git stash push $unstaged_files && NODE_ENV=production npm run lint-staged && git stash pop) || git stash pop #handle error
else
    NODE_ENV=production npm run lint-staged
fi

For some reason lint-staged instead of npm run lint-staged throws an error lint-staged: command not found.

⚠️ use it on your risk

@souljorje your shell script can't find the lint-staged executable automatically from inside node_modules/. You can probably just use npx lint-staged instead of needing the package.json script. 👍

I use lint-staged in a monorepo. I have set tsc cli's parameters manually. But when I trigger lint-staged check. tsc can not read My custom global.d.ts's configuration.

This is my lint-staged.config.js

image

image

This is my global.d.ts file.
image

How to fix it? thanks.

@shikelong I suggest a project-based type-check instead of checking files individually, as they're dependent.

#825 (comment)

const getTscFlags = () => {
  const compilerOptions = {
    allowJs: true,
    allowSyntheticDefaultImports: true,
    esModuleInterop: true,
    isolatedModules: true,
    jsx: 'react-native',
    lib: ['es2017'],
    types: ['react-native', 'jest'],
    moduleResolution: 'node',
    noEmit: true,
    target: 'esnext',
    skipLibCheck: true,
    resolveJsonModule: true
  }

  return Object.keys(compilerOptions)
    .flatMap(key => {
      const value = compilerOptions[key]
      if (Array.isArray(value)) {
        return `${key} ${value.join(',')}`
      }
      if (typeof value === 'string') {
        return `${key} ${value}`
      }
      return key
    })
    .map(key => `--${key}`)
    .join(' ')
}

module.exports = {
  '*.{js,jsx}': ['eslint --fix', 'jest -u --bail --findRelatedTests'],
  '*.{ts,tsx}': [`tsc ${getTscFlags()}`, 'eslint --fix', 'jest -u --bail --findRelatedTests']
}

"I would suggest putting the tsc -p tsconfig.json --noEmit --skipLibCheck in pre-push using husky"

I think Using Husky will alse scan the whole project.

@sombreroEnPuntas it works , nice bro!!!!!!

我在 monorepo 中使用 lint-staged。我已经手动设置了 tsc cli 的参数。但是当我触发 lint-staged 检查时。tsc 无法读取我的自定义 global.d.ts 的配置。

这是我的 lint-staged.config.js

图像

图像

这是我的 global.d.ts 文件。 图像

如何解决?谢谢。
Has this problem been solved

这个问题解决了吗

package.json:

"lint-staged": {
  "*": ["bash -c \"pnpm run typecheck\"", "eslint --fix"]
},
commented

When using bash -c, make sure that you put the params into quotes, for example the code shared by @sombreroEnPuntas (bash -c tsc --noEmit) causes the --noEmit to be passed to bash instead of tsc.