Skip to content

TypeScript and JavaScript

Fred Souza edited this page Aug 3, 2026 · 1 revision

lanekeep checks .ts, .mts, .cts, .tsx, .js, .mjs, .cjs and .jsx.

Install

npm install --save-dev lanekeep
npx lanekeep check

One package per platform plus a launcher that resolves the right one, so you download one binary rather than five. Node is not required to run lanekeep — only to install it this way. The binary has the JavaScript engine compiled in.

pnpm add -D lanekeep, yarn add -D lanekeep and bun add -d lanekeep all work.

Configure

lanekeep init in a directory with a package.json or tsconfig.json writes this for you:

{
  "$schema": "https://raw.githubusercontent.com/fmsouza/lanekeep/main/schema/lanekeep.schema.json",

  "include": ["src/**/*.{ts,tsx}"],
  "exclude": ["**/*.{test,spec}.{ts,tsx}"],

  "rules": [
    "lanekeep/no-default-export",
    "./lanekeep/rules/no-debugger.ts"
  ]
}

In a monorepo, include is relative to wherever you run lanekeep:

{ "include": ["packages/*/src/**/*.{ts,tsx}"], "exclude": ["**/dist/**", "**/node_modules/**"] }

The one that bites: TSX

The grammar is chosen by the file, not by the rule, and typescript and tsx are different languages here. TypeScript gives up <T>expr casts so the same syntax can open a JSX element, so one grammar cannot parse both.

A rule declaring language: 'typescript' does not run on .tsx files at all. A rule that should cover both needs:

language: ['typescript', 'tsx']

which is the default when you omit language entirely. Add 'javascript' if the project has .js too.

This was a real failure: a rule declaring typescript alone, run over a React Native codebase, matched nothing inside any component and produced 2218 false positives elsewhere — silently, no error, a tree that "parsed".

Built-in rules

lanekeep/no-default-export

A default export has a different name in every importer, so grep stops working and a rename touches nothing. Offers a fix where the export is a named declaration.

lanekeep/no-restricted-imports

{ "rule": "lanekeep/no-restricted-imports", "options": { "restrictions": [
  { "module": "stripe", "from": ["!packages/payments/**"], "reason": "route it through the payments package" },
  { "module": "lodash/*", "reason": "use the standard library" }
] } }

from takes globs; a leading ! inverts, so the example means "anywhere except the payments package". Wildcards work in module.

lanekeep/no-unused-exports

Cross-file. Finds exported symbols nothing imports — dead surface that keeps being maintained. Emits facts in check and consumes them in reduce.

lanekeep/no-circular-imports

Cross-file. Reports import cycles, which make module initialization order load-bearing and undefined-at-import bugs possible.

Both cross-file rules are skipped by --staged and --since, and named on stderr when they are. A whole-corpus rule over a subset gives a wrong answer, not a smaller one.

Writing a rule

import { defineRule } from 'lanekeep'

export default defineRule({
  id: 'local/no-numeric-sizes',
  // language omitted → ['typescript', 'tsx']
  severity: 'error',

  card: {
    message: 'literal numeric size inside makeStyles',
    remediation: 'use theme.spacing.*, theme.borderRadius.* or theme.borders.*',
    examples: { bad: 'padding: 12', good: 'padding: theme.spacing.md' },
  },

  query: `
    (pair
      key: (property_identifier) @prop
      value: [(number) (unary_expression operand: (number))] @value) @match
  `,

  check(ctx, m) {
    if (!/^(padding|margin|gap|borderRadius)/.test(ctx.text(m.prop))) return
    if (Number(ctx.text(m.value)) === 0) return

    const call = ctx.closestAncestor(m.match, '(call_expression function: (identifier) @f)')
    if (!call) return

    // The line that makes this a rule rather than a grep: a local `makeStyles` is not
    // the one from @rneui/themed.
    if (!ctx.resolvesToImport(call.f, { module: '@rneui/themed', name: 'makeStyles' })) return

    ctx.report(m.match)
  },
})

What resolution knows

ctx.bindingKind understands import, const, let, var, param, function, class and catch-param.

ctx.resolvesToImport(node, { module, name }) handles aliasing, so import { makeStyles as ms } resolves correctly — which is the case a text match gets wrong in both directions: it misses the alias, and it fires on a local const makeStyles that has nothing to do with the import.

Clone this wiki locally