lint-staged/lint-staged

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

SerkanSipahi opened this issue ยท 32 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
iiroj commented

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.

iiroj commented

Also, please post your debug logs by enabling them:

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

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 ๐Ÿ‘

iiroj commented

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?

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

iiroj commented

@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"]
},

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.

don't use bash -c, it will run tsc on your whole project instead of just the staged files.
this happens as the additional file arguments are omitted from the tsc options, and it then executes tsc on your whole project. this can take quite some time if your project has multiple hundreds of .ts|.tsx files.
installing tsc-files and using it instead of tsc works as it uses the projects tsconfig.json, and accepts a list of files to run tsc on.

TLDR;
throw a thumbs up (๐Ÿ‘) on this issue microsoft/TypeScript#27379, and use tsc-files instead of tsc in the mean time

detailed explanation:
to get this to work with only tsc the ideal config (in a perfect world) should be:

tsc --project tsconfig.json --noEmit src/staged-file.ts src/other-staged-file.ts

where the src/staged-file.ts and src/other-staged-file.ts are provided by lint-staged.
this is not possible because when running the command, you will get the following error:

error TS5042: Option 'project' cannot be mixed with source files on a command line.

to circumvent this, you need to provide your tsconfig.json options as CLI arguments:

tsc --noEmit --strict --baseUrl "." --paths { "app/*": ["./src/app/*"], # ...

it's not feasible to maintain such code, so the remaining options are either writing a script in your lint-staged.config.js file to parse your tsconfig.json and generate the above mentioned CLI arguments.
or just use the tsc-files package and wait for microsoft/TypeScript#27379 to be resolved.

I use quarkus with the default vite-plugin-checker system for eslint and vue-tsc.
It does live scanning of changes and just works.

it's not feasible to maintain such code, so the remaining options are either writing a script in your lint-staged.config.js file to parse your tsconfig.json and generate the above mentioned CLI arguments. or just use the tsc-files package and wait for microsoft/TypeScript#27379 to be resolved.

@zdenkolini Thank you for sharing tsc-files, it works perfectly in my project.

in my case it fails running it in folders as npx lint-staged

this is my .lintstagedrc.json file

{
  "*.{js,jsx,ts,tsx}": ["eslint --fix", "eslint"],
  "**/*.ts?(x)": "pnpm run types",
  "*.json": ["prettier --write --ignore-path .gitignore"]
}

when running pnpm run types, it reads node_modules even if they are excluded in the tsconfig at the same level as the .lintstagedrc.json file. So the types check fail.

Obviously running pnpm run types in the same directory directly does work

using last version: ^15.2.5",


adding

  "**/*.ts?(x)": "pnpm run types --skipLibCheck",

which is already in my tsconfig fixes it for me, for some reason

Best option is to use sh -c 'tsc --noEmit' or bash -c 'tsc --noEmit'. It works on Windows as well.

It type checks all the files even non-staged ones, but that's fine, a change in a file may break type checking in some other file.

    "*.{js,ts,tsx,jsx}": [
      "sh -c 'tsc --noEmit'",
      "eslint --fix",
      "prettier --write"
    ]