{"_id":"laint","_rev":"6-72fa598fd6f709a7da62642fdf883d15","name":"laint","dist-tags":{"latest":"0.1.7"},"versions":{"0.1.0":{"name":"laint","version":"0.1.0","keywords":["lint","linter","jsx","tsx","react","react-native","expo","ai","code-generation","ast"],"license":"MIT","_id":"laint@0.1.0","maintainers":[{"name":"daniel1234567890","email":"dchen0@pm.me"}],"dist":{"shasum":"455557c4e2ab6272413cf62b71b484a7e2f84839","tarball":"https://registry.npmjs.org/laint/-/laint-0.1.0.tgz","fileCount":62,"integrity":"sha512-lwW6T9d4MpC+8alqXXA5Q+etC1pxGJeKyQC1xj5r8CM01fF15eq3a0aB/ODE4MSTFbvany5oKdPHrTIeLi1zYw==","signatures":[{"sig":"MEUCIF7cLLZ9KKyIZ+nAmhW90AT3JJ4rzEae3j6TsnqAyDMVAiEAm2B0M512Vh5tvJ9yPs7Hi9WV2a2eutVzG8ZaMCNXJNo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":86459},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"6a2260a68f44d91d218a7082423579f7bc48ff9b","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest","prepublishOnly":"npm run build && npm test"},"_npmUser":{"name":"daniel1234567890","email":"dchen0@pm.me"},"_npmVersion":"11.6.0","description":"AI Agent Lint Rules SDK - programmatic JSX/TSX linting for AI code generation","directories":{},"_nodeVersion":"24.10.0","dependencies":{"@babel/parser":"^7.24.0","@babel/traverse":"^7.24.0"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^1.3.0","typescript":"^5.3.0","@types/node":"^20.11.0","@babel/types":"^7.24.0","@types/babel__traverse":"^7.20.5"},"_npmOperationalInternal":{"tmp":"tmp/laint_0.1.0_1770171599204_0.32494126986878236","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"laint","version":"0.1.1","keywords":["lint","linter","jsx","tsx","react","react-native","expo","ai","code-generation","ast"],"license":"MIT","_id":"laint@0.1.1","maintainers":[{"name":"daniel1234567890","email":"dchen0@pm.me"}],"dist":{"shasum":"b530ae4c5e84e553acb128526a81e6ddc9889719","tarball":"https://registry.npmjs.org/laint/-/laint-0.1.1.tgz","fileCount":62,"integrity":"sha512-KVP2wYNLVkmfXMuMxZ/2185M7202vj7xcN9IxXSGYcHKToCf3+uQcz3ISrK4wDScFXkZrRjygnXyPqY/JSh1Xg==","signatures":[{"sig":"MEQCIFXhH8Bo8RWbA4O2C8r6MgvQP2T+zEuReibQZpTzDkM8AiAZsAb5F0XIbuB3JJs/ZAL96DPW9T1ksvzHSWpi0FW7Rw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":77396},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","gitHead":"6a2260a68f44d91d218a7082423579f7bc48ff9b","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest","prepublishOnly":"npm run build && npm test"},"_npmUser":{"name":"daniel1234567890","email":"dchen0@pm.me"},"_npmVersion":"11.6.0","description":"AI Agent Lint Rules SDK - programmatic JSX/TSX linting for AI code generation","directories":{},"_nodeVersion":"24.10.0","dependencies":{"@babel/parser":"^7.24.0","@babel/traverse":"^7.24.0"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^1.3.0","typescript":"^5.3.0","@types/node":"^20.11.0","@babel/types":"^7.24.0","@types/babel__traverse":"^7.20.5"},"_npmOperationalInternal":{"tmp":"tmp/laint_0.1.1_1770173245422_0.4237969986407726","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"laint","version":"0.1.2","keywords":["lint","linter","jsx","tsx","react","react-native","expo","ai","code-generation","ast"],"license":"MIT","_id":"laint@0.1.2","maintainers":[{"name":"daniel1234567890","email":"dchen0@pm.me"}],"dist":{"shasum":"d0ff3e6db74b4dc221b3cb76aaab2d4cf76b836f","tarball":"https://registry.npmjs.org/laint/-/laint-0.1.2.tgz","fileCount":62,"integrity":"sha512-v6pVfdZlVjthO9uMwHZIoKViApK5gTuvBdSBjCjsDUcsfJhyyK5C0YHiZ2w+SdaW8sSElV1dnwOVkis7R+1L9A==","signatures":[{"sig":"MEUCIQDDG1uk5LlYHjdcWaAKoKRW9i7t07OLF3GBFmeGtWPy+wIgdt7y9mBc2O5OkAAmhsvcwo2Jcf702IJhZGpw35+K1H0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":78821},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","gitHead":"d9e6e6346be18fac77dc13a444ddd7eed78490e6","scripts":{"test":"vitest run","build":"tsc && node scripts/fix-esm-imports.js","test:watch":"vitest","prepublishOnly":"npm run build && npm test"},"_npmUser":{"name":"daniel1234567890","email":"dchen0@pm.me"},"_npmVersion":"11.6.0","description":"AI Agent Lint Rules SDK - programmatic JSX/TSX linting for AI code generation","directories":{},"_nodeVersion":"24.10.0","dependencies":{"@babel/parser":"^7.24.0","@babel/traverse":"^7.24.0"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^1.3.0","typescript":"^5.3.0","@types/node":"^20.11.0","@babel/types":"^7.24.0","@types/babel__traverse":"^7.20.5"},"_npmOperationalInternal":{"tmp":"tmp/laint_0.1.2_1770173864969_0.13845412499817922","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"laint","version":"0.1.3","keywords":["lint","linter","jsx","tsx","react","react-native","expo","ai","code-generation","ast"],"license":"MIT","_id":"laint@0.1.3","maintainers":[{"name":"daniel1234567890","email":"dchen0@pm.me"}],"homepage":"https://github.com/Create-Inc/laint#readme","bugs":{"url":"https://github.com/Create-Inc/laint/issues"},"dist":{"shasum":"bd2bf972b9cabc5e16b8bcebd907b3c6a78a01f2","tarball":"https://registry.npmjs.org/laint/-/laint-0.1.3.tgz","fileCount":72,"integrity":"sha512-DEjuHprwkT3KVzUAy4baEQHJPWj+jZn4wfzmAYnuaNLVPR0sFEAfZNuO35Z4C81jRWgcUjZ7QNQORZX7YsdqCQ==","signatures":[{"sig":"MEUCIHRUICFsM/y9b2qTfi9G69r2jAqUdRBBIf/RzfMdl78WAiEA/lYQaTn2bFLwNtvbBO7l/LT9GIpfMC9KFAg34Dvfsrk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/laint@0.1.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":94939},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","gitHead":"d226240ef359c9a828718815701a1c8a3afd5f2a","scripts":{"knip":"knip","lint":"eslint . && prettier --check .","test":"vitest run","build":"tsc && node scripts/fix-esm-imports.js","format":"prettier --write .","lint:fix":"eslint --fix . && prettier --write .","test:watch":"vitest","format:check":"prettier --check .","prepublishOnly":"npm run build && npm test"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d94f749c-4507-4054-9d44-5ef42d073c9d"}},"repository":{"url":"git+https://github.com/Create-Inc/laint.git","type":"git"},"_npmVersion":"11.6.2","description":"AI Agent Lint Rules SDK - programmatic JSX/TSX linting for AI code generation","directories":{},"_nodeVersion":"24.13.0","dependencies":{"@babel/parser":"^7.24.0","@babel/traverse":"^7.24.0"},"_hasShrinkwrap":false,"devDependencies":{"knip":"^5.83.1","eslint":"^9.39.2","vitest":"^4.0.18","globals":"^17.3.0","prettier":"^3.8.1","typescript":"^5.3.0","@types/node":"^20.11.0","@babel/types":"^7.24.0","typescript-eslint":"^8.54.0","@types/babel__traverse":"^7.20.5","eslint-config-prettier":"^10.1.8","eslint-plugin-import-x":"^4.16.1","eslint-plugin-security":"^3.0.1"},"_npmOperationalInternal":{"tmp":"tmp/laint_0.1.3_1770662039025_0.4290017134273465","host":"s3://npm-registry-packages-npm-production"}},"0.1.6":{"name":"laint","version":"0.1.6","keywords":["lint","linter","jsx","tsx","react","react-native","expo","ai","code-generation","ast"],"license":"MIT","_id":"laint@0.1.6","maintainers":[{"name":"daniel1234567890","email":"dchen0@pm.me"}],"homepage":"https://github.com/Create-Inc/laint#readme","bugs":{"url":"https://github.com/Create-Inc/laint/issues"},"bin":{"laint":"dist/cli.js"},"dist":{"shasum":"379863bff4ee29c2e666c4c4153cd1b4ba6e9b10","tarball":"https://registry.npmjs.org/laint/-/laint-0.1.6.tgz","fileCount":82,"integrity":"sha512-466NQ6+bg8LGMEMVUg3I8jJ9nKASvISduuq1ZwXTSdi8hhzKtU398M6RxOueGuyxHbvRSNRNWWJwUhNz6NNpaA==","signatures":[{"sig":"MEUCIQDpUN9Iu6nuAz6cd+lVQDuRt92zI/VBmgz96je/ZOZodAIgWcrUBritgEGESOT/MdOE7Yhl7kUEuizPCYGqK0k4740=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/laint@0.1.6","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":106652},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","gitHead":"eb32163231f1993434123d31a015fae0a706900f","scripts":{"knip":"knip","lint":"eslint . && prettier --check .","test":"vitest run","build":"tsc && node scripts/fix-esm-imports.js","format":"prettier --write .","lint:fix":"eslint --fix . && prettier --write .","test:watch":"vitest","format:check":"prettier --check .","prepublishOnly":"npm run build && npm test"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d94f749c-4507-4054-9d44-5ef42d073c9d"}},"repository":{"url":"git+https://github.com/Create-Inc/laint.git","type":"git"},"_npmVersion":"11.6.2","description":"AI Agent Lint Rules SDK - programmatic JSX/TSX linting for AI code generation","directories":{},"_nodeVersion":"24.13.0","dependencies":{"@babel/parser":"^7.24.0","@babel/traverse":"^7.24.0"},"_hasShrinkwrap":false,"devDependencies":{"knip":"^5.83.1","eslint":"^9.39.2","vitest":"^4.0.18","globals":"^17.3.0","prettier":"^3.8.1","typescript":"^5.3.0","@types/node":"^20.11.0","@babel/types":"^7.24.0","typescript-eslint":"^8.54.0","@types/babel__traverse":"^7.20.5","eslint-config-prettier":"^10.1.8","eslint-plugin-import-x":"^4.16.1","eslint-plugin-security":"^3.0.1"},"_npmOperationalInternal":{"tmp":"tmp/laint_0.1.6_1770938002321_0.09598227533687442","host":"s3://npm-registry-packages-npm-production"}},"0.1.7":{"name":"laint","version":"0.1.7","type":"module","description":"AI Agent Lint Rules SDK - programmatic JSX/TSX linting for AI code generation","main":"dist/index.js","types":"dist/index.d.ts","bin":{"laint":"dist/cli.js"},"scripts":{"build":"tsc && node scripts/fix-esm-imports.js","test":"vitest run","test:watch":"vitest","prepublishOnly":"npm run build && npm test","knip":"knip","lint":"eslint . && prettier --check .","lint:fix":"eslint --fix . && prettier --write .","format":"prettier --write .","format:check":"prettier --check ."},"keywords":["lint","linter","jsx","tsx","react","react-native","expo","ai","code-generation","ast"],"repository":{"type":"git","url":"git+https://github.com/Create-Inc/laint.git"},"license":"MIT","dependencies":{"@babel/parser":"^7.24.0","@babel/traverse":"^7.24.0"},"devDependencies":{"@babel/types":"^7.24.0","@types/babel__traverse":"^7.20.5","@types/node":"^20.11.0","eslint":"^9.39.2","eslint-config-prettier":"^10.1.8","eslint-plugin-import-x":"^4.16.1","eslint-plugin-security":"^3.0.1","globals":"^17.3.0","knip":"^5.83.1","prettier":"^3.8.1","typescript":"^5.3.0","typescript-eslint":"^8.54.0","vitest":"^4.0.18"},"gitHead":"41dd792f9c5ca5e64b68b3414accd5ed0436c27c","_id":"laint@0.1.7","bugs":{"url":"https://github.com/Create-Inc/laint/issues"},"homepage":"https://github.com/Create-Inc/laint#readme","_nodeVersion":"24.13.1","_npmVersion":"11.8.0","dist":{"integrity":"sha512-iMlLcOgCxwA2MGGzZZFjZxgOO1fV8gs4J+FGKBKEGIYMhgdwla/9iEErslelR7xuy18HqIEXw1g3J1ToeXLENA==","shasum":"50250128aa7e580be94a40e9fa08a9b1be3ae1ec","tarball":"https://registry.npmjs.org/laint/-/laint-0.1.7.tgz","fileCount":118,"unpackedSize":156432,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/laint@0.1.7","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDCT4jxu3aGtpRGx4rpCtmz/nQ5bmTE6R9jLm7PLhEkRwIhAIz4osf1bOB8EP3tpO0ttRAjpx6tMihIA7aRlhK4waUo"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d94f749c-4507-4054-9d44-5ef42d073c9d"}},"directories":{},"maintainers":[{"name":"daniel1234567890","email":"dchen0@pm.me"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/laint_0.1.7_1772070020618_0.6080053898745685"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-04T02:19:59.203Z","modified":"2026-02-26T01:40:21.110Z","0.1.0":"2026-02-04T02:19:59.348Z","0.1.1":"2026-02-04T02:47:25.574Z","0.1.2":"2026-02-04T02:57:45.125Z","0.1.3":"2026-02-09T18:33:59.203Z","0.1.6":"2026-02-12T23:13:22.468Z","0.1.7":"2026-02-26T01:40:20.810Z"},"bugs":{"url":"https://github.com/Create-Inc/laint/issues"},"license":"MIT","homepage":"https://github.com/Create-Inc/laint#readme","keywords":["lint","linter","jsx","tsx","react","react-native","expo","ai","code-generation","ast"],"repository":{"type":"git","url":"git+https://github.com/Create-Inc/laint.git"},"description":"AI Agent Lint Rules SDK - programmatic JSX/TSX linting for AI code generation","maintainers":[{"name":"daniel1234567890","email":"dchen0@pm.me"}],"readme":"# laint\n\nAI Agent Lint Rules SDK - a simple programmatic API for linting JSX/TSX code.\n\n## Claude Code Integration\n\nThe fastest way to use laint is as a [Claude Code hook](https://docs.anthropic.com/en/docs/claude-code/hooks). After every file edit, Claude sees lint violations and fixes them automatically.\n\n```bash\nnpx laint init\n```\n\nThis writes a `.claude/settings.json` with a `PostToolUse` hook that runs after every `Edit` and `Write` tool call. If the file already exists, it merges without clobbering your other settings.\n\n### Configuring Rules\n\nBy default, all 45 rules run. To customize, create a `laint.config.json` in your project root:\n\n```json\n// Only run these specific rules (include mode)\n{ \"rules\": [\"no-relative-paths\", \"expo-image-import\", \"fetch-response-ok-check\"] }\n```\n\n```json\n// Run all rules except these (exclude mode)\n{ \"rules\": [\"no-tailwind-animation-classes\", \"no-stylesheet-create\"], \"exclude\": true }\n```\n\n```json\n// Run all rules for a platform (platform mode)\n{ \"platform\": \"expo\" }\n```\n\n**Platforms:** `expo`, `web`, `backend`. Platform mode runs all rules tagged for that platform plus universal rules (rules not specific to any platform).\n\n### CLI\n\n```bash\n# Lint a file directly\nnpx laint check src/components/Button.tsx\n\n# Hook mode (used by Claude Code automatically — reads stdin JSON)\nnpx laint check --hook\n```\n\n**Exit codes:**\n\n- `0` — clean (no violations)\n- `1` — violations found (file mode)\n- `2` — violations found (hook mode, stderr output for Claude)\n\n## Installation\n\n```bash\nnpm install laint\n```\n\n## Programmatic Usage\n\n```typescript\nimport { lintJsxCode, getAllRuleNames } from 'laint';\n\nconst code = `\n  <Link href=\"./profile\">Profile</Link>\n  <Button onPress={() => router.navigate('../settings')} />\n`;\n\n// Include mode (default): only run specified rules\nconst results = lintJsxCode(code, {\n  rules: ['no-relative-paths', 'no-stylesheet-create', 'expo-image-import'],\n});\n\n// results:\n// [\n//   { rule: 'no-relative-paths', message: '...', line: 2, column: 14, severity: 'error' },\n//   { rule: 'no-relative-paths', message: '...', line: 3, column: 41, severity: 'error' }\n// ]\n```\n\n### Exclude Mode\n\nRun all rules except specific ones:\n\n```typescript\n// Exclude mode: run ALL rules except those listed\nconst results = lintJsxCode(code, {\n  rules: ['no-stylesheet-create'], // rules to skip\n  exclude: true,\n});\n\n// Run all 45 rules\nconst allResults = lintJsxCode(code, {\n  rules: [],\n  exclude: true,\n});\n\n// Get list of all available rules\nconst ruleNames = getAllRuleNames(); // ['no-relative-paths', 'expo-image-import', ...]\n```\n\n### Platform Mode\n\nRun rules by platform — includes platform-tagged rules plus universal rules:\n\n```typescript\nimport { lintJsxCode, getRulesForPlatform } from 'laint';\n\n// Run all rules for Expo\nconst results = lintJsxCode(code, {\n  rules: [],\n  platform: 'expo',\n});\n\n// Get rule names for a platform\nconst expoRules = getRulesForPlatform('expo'); // expo-tagged + universal rules\nconst webRules = getRulesForPlatform('web');\nconst backendRules = getRulesForPlatform('backend');\n```\n\n## Available Rules (50 total)\n\n### Expo Router Rules\n\n| Rule                 | Severity | Platform  | Description                                              |\n| -------------------- | -------- | --------- | -------------------------------------------------------- |\n| `no-relative-paths`  | error    | expo, web | Use absolute paths in router.navigate/push and Link href |\n| `header-shown-false` | warning  | expo      | (tabs) Screen in root layout needs `headerShown: false`  |\n\n### React Native / Expo Rules\n\n| Rule                               | Severity | Platform | Description                                          |\n| ---------------------------------- | -------- | -------- | ---------------------------------------------------- |\n| `no-stylesheet-create`             | warning  | expo     | Use inline styles instead of StyleSheet.create()     |\n| `no-safeareaview`                  | warning  | expo     | Use useSafeAreaInsets() hook instead of SafeAreaView |\n| `expo-image-import`                | warning  | expo     | Import Image from expo-image, not react-native       |\n| `no-tab-bar-height`                | error    | expo     | Never set explicit height in tabBarStyle             |\n| `scrollview-horizontal-flexgrow`   | warning  | expo     | Horizontal ScrollView needs `flexGrow: 0`            |\n| `expo-font-loaded-check`           | error    | expo     | useFonts() must check loaded before rendering        |\n| `tabs-screen-options-header-shown` | warning  | expo     | Tabs screenOptions should have `headerShown: false`  |\n| `native-tabs-bottom-padding`       | warning  | expo     | NativeTabs screens need 64px bottom padding          |\n| `textinput-keyboard-avoiding`      | warning  | expo     | TextInput should be inside KeyboardAvoidingView      |\n\n### Liquid Glass Rules (expo-glass-effect)\n\n| Rule                         | Severity | Platform | Description                                           |\n| ---------------------------- | -------- | -------- | ----------------------------------------------------- |\n| `no-border-width-on-glass`   | error    | expo     | No borderWidth on GlassView (breaks borderRadius)     |\n| `glass-needs-fallback`       | warning  | expo     | Check isLiquidGlassAvailable() before using GlassView |\n| `glass-interactive-prop`     | warning  | expo     | GlassView in pressables needs `isInteractive={true}`  |\n| `glass-no-opacity-animation` | warning  | expo     | No opacity animations on GlassView                    |\n\n### Next.js Rules\n\n| Rule                         | Severity | Platform | Description                                                       |\n| ---------------------------- | -------- | -------- | ----------------------------------------------------------------- |\n| `require-use-client`         | error    | web      | Files using client-only features must have \"use client\" directive |\n| `no-server-import-in-client` | error    | web      | \"use client\" files must not import server-only modules            |\n\n### React / JSX Rules\n\n| Rule                         | Severity | Platform     | Description                                   |\n| ---------------------------- | -------- | ------------ | --------------------------------------------- |\n| `no-class-components`        | warning  | expo, web    | Use function components with hooks            |\n| `no-inline-script-code`      | error    | web          | Script tags should use template literals      |\n| `no-react-query-missing`     | warning  | expo, web    | Use @tanstack/react-query for data fetching   |\n| `browser-api-in-useeffect`   | warning  | web          | window/localStorage only in useEffect for SSR |\n| `fetch-response-ok-check`    | warning  | web, backend | Check response.ok when using fetch            |\n| `no-complex-jsx-expressions` | warning  | expo, web    | Avoid IIFEs and complex expressions in JSX    |\n\n### Screen Transitions Rules (react-native-screen-transitions)\n\n| Rule                             | Severity | Platform | Description                                                               |\n| -------------------------------- | -------- | -------- | ------------------------------------------------------------------------- |\n| `transition-worklet-directive`   | error    | expo     | screenStyleInterpolator functions must include \"worklet\" directive        |\n| `transition-progress-range`      | warning  | expo     | interpolate() should cover full [0, 1, 2] range including exit phase      |\n| `transition-gesture-scrollview`  | warning  | expo     | Use Transition.ScrollView/FlatList instead of regular versions            |\n| `transition-shared-tag-mismatch` | warning  | expo     | sharedBoundTag on Transition.Pressable must have matching Transition.View |\n| `transition-prefer-blank-stack`  | warning  | expo     | Use Blank Stack instead of enableTransitions on Native Stack              |\n\n### Tailwind CSS Rules\n\n| Rule                            | Severity | Platform | Description                                            |\n| ------------------------------- | -------- | -------- | ------------------------------------------------------ |\n| `no-tailwind-animation-classes` | warning  | web      | Avoid animate-\\* classes, use style jsx global instead |\n| `no-inline-styles`              | warning  | web      | Avoid inline styles, use Tailwind CSS classes instead  |\n\n### Backend / SQL Rules\n\n| Rule                         | Severity | Platform | Description                                                   |\n| ---------------------------- | -------- | -------- | ------------------------------------------------------------- |\n| `no-require-statements`      | error    | backend  | Use ES imports, not CommonJS require                          |\n| `no-response-json-lowercase` | warning  | backend  | Use Response.json() instead of new Response(JSON.stringify()) |\n| `sql-no-nested-calls`        | error    | backend  | Don't nest sql template tags                                  |\n| `no-sync-fs`                 | error    | backend  | Use fs.promises or fs/promises instead of sync fs methods     |\n\n### URL Rules\n\n| Rule                     | Severity | Description                                                    |\n| ------------------------ | -------- | -------------------------------------------------------------- |\n| `url-params-must-encode` | warning  | URL query param values must be wrapped in encodeURIComponent() |\n\n### Error Handling Rules\n\n| Rule                       | Severity | Description                                                        |\n| -------------------------- | -------- | ------------------------------------------------------------------ |\n| `catch-must-log-to-sentry` | warning  | Catch blocks with logger.error/console.error must also call Sentry |\n\n### Code Style Rules\n\n| Rule                     | Severity | Platform  | Description                                                      |\n| ------------------------ | -------- | --------- | ---------------------------------------------------------------- |\n| `prefer-guard-clauses`   | warning  | universal | Use early returns instead of nesting if statements               |\n| `no-type-assertion`      | warning  | universal | Avoid `as` type casts; use type narrowing or proper types        |\n| `safe-json-parse`        | warning  | universal | Wrap JSON.parse in try-catch to handle malformed input           |\n| `no-loose-equality`      | warning  | universal | Use === and !== instead of == and != (except == null)            |\n| `no-magic-env-strings`   | warning  | universal | Use centralized enum for env variable names, not magic strings   |\n| `no-nested-try-catch`    | warning  | universal | Avoid nested try-catch blocks, extract to separate functions     |\n| `no-string-coerce-error` | warning  | universal | Use JSON.stringify instead of String() for unknown caught errors |\n| `logger-error-with-err`  | warning  | universal | logger.error() must include { err: Error } for stack traces      |\n| `no-optional-props`      | warning  | universal | Use `prop: T \\| null` instead of `prop?: T` in interfaces        |\n| `no-silent-skip`         | warning  | universal | Add else branch with logging instead of silently skipping        |\n| `no-manual-retry-loop`   | warning  | universal | Use a retry library instead of manual retry/polling loops        |\n| `no-emoji-icons`         | warning  | universal | Use icons from lucide-react instead of emoji characters          |\n| `prefer-named-params`    | warning  | universal | Use object destructuring instead of positional parameters        |\n\n### General Rules\n\n| Rule                  | Severity | Platform  | Description                                   |\n| --------------------- | -------- | --------- | --------------------------------------------- |\n| `prefer-lucide-icons` | warning  | expo, web | Prefer lucide-react/lucide-react-native icons |\n\n---\n\n## Rule Details\n\n### `no-relative-paths`\n\n```jsx\n// Bad\nrouter.navigate('./profile');\n<Link href=\"../settings\">\n\n// Good\nrouter.navigate('/(tabs)/profile');\n<Link href=\"/settings\">\n```\n\n### `browser-api-in-useeffect`\n\n```jsx\n// Bad - breaks SSR\nfunction Component() {\n  const width = window.innerWidth;\n  return <div>{width}</div>;\n}\n\n// Good\nfunction Component() {\n  const [width, setWidth] = useState(0);\n  useEffect(() => {\n    setWidth(window.innerWidth);\n  }, []);\n  return <div>{width}</div>;\n}\n```\n\n### `fetch-response-ok-check`\n\n```jsx\n// Bad\nconst response = await fetch('/api/data');\nconst data = await response.json();\n\n// Good\nconst response = await fetch('/api/data');\nif (!response.ok) {\n  throw new Error(`HTTP ${response.status}`);\n}\nconst data = await response.json();\n```\n\n### `no-response-json-lowercase`\n\n```jsx\n// Bad\nreturn new Response(JSON.stringify({ data }));\n\n// Good\nreturn Response.json({ data });\n```\n\n### `tabs-screen-options-header-shown`\n\n```jsx\n// Bad\n<Tabs screenOptions={{ tabBarStyle: { ... } }}>\n\n// Good\n<Tabs screenOptions={{ headerShown: false, tabBarStyle: { ... } }}>\n```\n\n### `native-tabs-bottom-padding`\n\nWhen using NativeTabs from expo-router/unstable-native-tabs, each screen needs 64px bottom padding to prevent content overlap with the tab bar.\n\n### `textinput-keyboard-avoiding`\n\n```jsx\n// Bad - keyboard will cover input\n<View>\n  <TextInput placeholder=\"Enter text\" />\n</View>\n\n// Good\n<KeyboardAvoidingView>\n  <TextInput placeholder=\"Enter text\" />\n</KeyboardAvoidingView>\n```\n\n### `glass-no-opacity-animation`\n\n```jsx\n// Bad - opacity animation causes visual glitches on GlassView\n<GlassView style={{ opacity: fadeAnim }} />\n\n// Good - use transform animations instead\n<GlassView style={{ transform: [{ scale: scaleAnim }] }} />\n```\n\n### `require-use-client`\n\n```tsx\n// Bad - uses hooks without \"use client\"\nimport { useState } from 'react';\n\nexport function Counter() {\n  const [count, setCount] = useState(0);\n  return <button onClick={() => setCount(count + 1)}>{count}</button>;\n}\n\n// Good - has \"use client\" directive\n('use client');\n\nimport { useState } from 'react';\n\nexport function Counter() {\n  const [count, setCount] = useState(0);\n  return <button onClick={() => setCount(count + 1)}>{count}</button>;\n}\n```\n\n### `no-server-import-in-client`\n\n```tsx\n// Bad - server-only module in client file\n'use client';\n\nimport { cookies } from 'next/headers';\n\nexport function UserMenu() {\n  return <div>Menu</div>;\n}\n\n// Good - use server-only modules only in server components\nimport { cookies } from 'next/headers';\n\nexport function UserMenu() {\n  const session = cookies().get('session');\n  return <div>Menu</div>;\n}\n```\n\n### `no-complex-jsx-expressions`\n\n```jsx\n// Bad - IIFE in JSX\n<div>\n  {(() => {\n    const x = compute();\n    return x;\n  })()}\n</div>;\n\n// Good - extract to variable\nconst computedValue = compute();\n<div>{computedValue}</div>;\n```\n\n### `no-tailwind-animation-classes`\n\n```jsx\n// Bad - CSS animation classes have issues\n<div className=\"animate-spin\" />\n\n// Good - use style jsx global for animations\n<style jsx global>{`\n  .spinner { animation: spin 1s linear infinite; }\n`}</style>\n<div className=\"spinner\" />\n```\n\n### `transition-worklet-directive`\n\n```jsx\n// Bad - missing worklet directive\nconst options = {\n  screenStyleInterpolator: (progress) => {\n    return { opacity: progress };\n  },\n};\n\n// Good\nconst options = {\n  screenStyleInterpolator: (progress) => {\n    'worklet';\n    return { opacity: progress };\n  },\n};\n```\n\n### `transition-progress-range`\n\n```jsx\n// Bad - only covers [0, 1], missing exit phase\nscreenStyleInterpolator: (progress) => {\n  'worklet';\n  const opacity = interpolate(progress, [0, 1], [0, 1]);\n  return { opacity };\n};\n\n// Good - covers full [0, 1, 2] range\nscreenStyleInterpolator: (progress) => {\n  'worklet';\n  const opacity = interpolate(progress, [0, 1, 2], [0, 1, 0]);\n  return { opacity };\n};\n```\n\n### `transition-gesture-scrollview`\n\n```jsx\n// Bad - regular ScrollView conflicts with transition gestures\nimport { Transition } from 'react-native-screen-transitions';\nimport { ScrollView } from 'react-native';\n<ScrollView>...</ScrollView>;\n\n// Good\nimport { Transition } from 'react-native-screen-transitions';\n<Transition.ScrollView>...</Transition.ScrollView>;\n```\n\n### `transition-shared-tag-mismatch`\n\n```jsx\n// Bad - Pressable tag has no matching View\n<Transition.Pressable sharedBoundTag=\"hero\">\n  <Image source={img} />\n</Transition.Pressable>\n\n// Good - matching tags on both components\n<Transition.Pressable sharedBoundTag=\"hero\">\n  <Image source={img} />\n</Transition.Pressable>\n<Transition.View sharedBoundTag=\"hero\">\n  <Image source={img} />\n</Transition.View>\n```\n\n### `transition-prefer-blank-stack`\n\n```jsx\n// Bad - enableTransitions on Native Stack has edge cases\n<Stack.Screen options={{ enableTransitions: true }} />;\n\n// Good - use Blank Stack from react-native-screen-transitions\nimport { BlankStack } from 'react-native-screen-transitions';\n```\n\n### `sql-no-nested-calls`\n\n```typescript\n// Bad - nested sql causes issues\nsql`UPDATE users SET ${sql`name = ${name}`} WHERE id = ${id}`;\n\n// Good - build query properly\nsql`UPDATE users SET name = ${name} WHERE id = ${id}`;\n```\n\n### `prefer-guard-clauses`\n\n```typescript\n// Bad - entire function body wrapped in if\nfunction handleClick(user) {\n  if (user) {\n    doSomething();\n    doMore();\n  }\n}\n\n// Good - early return\nfunction handleClick(user) {\n  if (!user) return;\n  doSomething();\n  doMore();\n}\n```\n\n### `no-type-assertion`\n\n```typescript\n// Bad - type casting\nconst value = data as string;\nconst user = response.data as User;\n\n// Good - type narrowing\nif (typeof data === 'string') {\n  const value = data;\n}\n\n// Good - proper typing\nconst user: User = response.data;\n```\n\n### `no-loose-equality`\n\n```typescript\n// Bad - loose equality\nif (a == b) {\n}\nif (x != 'hello') {\n}\n\n// Good - strict equality\nif (a === b) {\n}\nif (x !== 'hello') {\n}\n\n// OK - == null is idiomatic for null/undefined check\nif (value == null) {\n}\n```\n\n### `no-magic-env-strings`\n\n````typescript\n// Bad - hardcoded env string\nconst key = process.env.API_KEY;\nconst url = process.env['DATABASE_URL'];\n\n// Good - use centralized enum\nconst key = process.env[EnvVars.API_KEY];\n\n### `url-params-must-encode`\n\n```typescript\n// Bad - unencoded query param\nconst url = `https://api.example.com?q=${query}`;\n\n// Good - encoded query param\nconst url = `https://api.example.com?q=${encodeURIComponent(query)}`;\n````\n\n### `catch-must-log-to-sentry`\n\n```typescript\n// Bad - logs error but no Sentry\ntry {\n  fetchData();\n} catch (error) {\n  logger.error('Failed', error);\n}\n\n// Good - both logging and Sentry\ntry {\n  fetchData();\n} catch (error) {\n  logger.error('Failed', error);\n  Sentry.captureException(error);\n}\n```\n\n### `no-nested-try-catch`\n\n```typescript\n// Bad - nested try-catch\ntry {\n  try {\n    inner();\n  } catch (e) {}\n} catch (e) {}\n\n// Good - extract to separate function\nfunction safeInner() {\n  try {\n    inner();\n  } catch (e) {}\n}\ntry {\n  safeInner();\n} catch (e) {}\n```\n\n### `no-inline-styles`\n\n```tsx\n// Bad - inline style objects\n<div style={{ color: 'red', fontSize: 16 }}>Hello</div>\n\n// Good - Tailwind CSS classes\n<div className=\"text-red-500 text-base\">Hello</div>\n```\n\n### `no-string-coerce-error`\n\n```typescript\n// Bad - String() on a non-Error object produces '[object Object]'\nconst message = error instanceof Error ? error.message : String(error);\n\n// Good - JSON.stringify preserves object structure\nconst message = error instanceof Error ? error.message : JSON.stringify(error);\n```\n\n### `logger-error-with-err`\n\n```typescript\n// Bad - missing err property\nlogger.error({}, 'something failed');\nlogger.error({ userId: 1 }, 'request failed');\nlogger.error('something went wrong');\n\n// Good - includes err for stack traces\nlogger.error({ err: error }, 'something failed');\nlogger.error({ err: new Error('x'), userId: 1 }, 'request failed');\n```\n\n### `no-silent-skip`\n\n```typescript\n// Bad - silently skips when user is falsy\nfunction process(user) {\n  if (user) {\n    sendEmail(user);\n    updateDb(user);\n  }\n}\n\n// Good - logs why the else case was skipped\nfunction process(user) {\n  if (user) {\n    sendEmail(user);\n    updateDb(user);\n  } else {\n    logger.warn('No user provided, skipping processing');\n  }\n}\n\n// Also fine - guard clause with early return\nfunction process(user) {\n  if (!user) return;\n  sendEmail(user);\n  updateDb(user);\n}\n```\n\n### `no-manual-retry-loop`\n\n```typescript\n// Bad - manual retry loop with setTimeout\nfor (let attempt = 0; attempt < 15; attempt++) {\n  const result = await checkStatus(id);\n  if (result.ready) return result;\n  await new Promise((resolve) => setTimeout(resolve, 2000));\n}\n\n// Good - use a retry library\nimport retry from 'async-retry';\nconst result = await retry(\n  async () => {\n    const res = await checkStatus(id);\n    if (!res.ready) throw new Error('not ready');\n    return res;\n  },\n  { retries: 15, minTimeout: 2000 },\n);\n```\n\n---\n\n### `no-emoji-icons`\n\n```typescript\n// Bad - emoji characters as icons\n<Text>✅ Done</Text>\n<Button label=\"🔥 Hot\" />\n\n// Good - use icon library\nimport { Check, Flame } from 'lucide-react';\n<Text><Check /> Done</Text>\n<Button label={<><Flame /> Hot</>} />\n```\n\n---\n\n### `safe-json-parse`\n\n```typescript\n// Bad - JSON.parse without error handling\nconst data = JSON.parse(rawInput);\n\n// Good - wrapped in try-catch\ntry {\n  const data = JSON.parse(rawInput);\n} catch (e) {\n  console.error('Failed to parse JSON', e);\n}\n\n// For JSON.stringify with circular references, consider using fast-safe-stringify\n// import stringify from \"fast-safe-stringify\";\n// const str = stringify(circularObj);\n```\n\n---\n\n### `no-optional-props`\n\n```typescript\n// Bad - optional properties create implicit undefined\ninterface UserProps {\n  name?: string;\n  age?: number;\n}\n\n// Good - explicit null union\ninterface UserProps {\n  name: string | null;\n  age: number | null;\n}\n```\n\n### `no-sync-fs`\n\n```typescript\n// Bad - synchronous fs methods block the event loop\nimport fs from 'fs';\nconst data = fs.readFileSync('file.txt', 'utf-8');\nfs.writeFileSync('output.txt', data);\n\n// Good - use async fs methods\nimport { readFile, writeFile } from 'fs/promises';\nconst data = await readFile('file.txt', 'utf-8');\nawait writeFile('output.txt', data);\n```\n\n### `prefer-named-params`\n\n```typescript\n// Bad - positional parameters\nfunction createUser(name: string, email: string, age: number) {\n  return { name, email, age };\n}\n\n// Good - named parameters via object destructuring\nfunction createUser({ name, email, age }: { name: string; email: string; age: number }) {\n  return { name, email, age };\n}\n```\n\nCallbacks (`.map`, `.filter`, `.reduce`, `.sort`, `.then`, etc.) and `React.forwardRef`/`memo` are excluded.\n\n---\n\n## Adding a New Rule\n\n1. Create a rule file in `src/rules/`:\n\n```typescript\n// src/rules/my-rule.ts\nimport traverse from '@babel/traverse';\nimport type { File } from '@babel/types';\nimport type { LintResult } from '../types';\n\nexport function myRule(ast: File, code: string): LintResult[] {\n  const results: LintResult[] = [];\n\n  traverse(ast, {\n    CallExpression(path) {\n      // Check for violations...\n      results.push({\n        rule: 'my-rule',\n        message: 'Description of the issue',\n        line: path.node.loc?.start.line ?? 0,\n        column: path.node.loc?.start.column ?? 0,\n        severity: 'error', // or 'warning'\n      });\n    },\n  });\n\n  return results;\n}\n```\n\n2. Register in `src/rules/index.ts`\n3. Add tests in `tests/my-rule.test.ts`\n4. Run `npm test`\n\n## API Reference\n\n### `lintJsxCode(code: string, config: LintConfig): LintResult[]`\n\n**Parameters:**\n\n- `code` - JSX/TSX source code to lint\n- `config.rules` - Array of rule names\n- `config.exclude` - (optional) When `true`, runs all rules except those in `rules`. Default: `false`\n- `config.platform` - (optional) `'expo' | 'web' | 'backend'`. When set, runs platform-tagged + universal rules. Takes precedence over `rules`/`exclude`\n\n**Returns:** Array of `LintResult`:\n\n```typescript\ninterface LintResult {\n  rule: string;\n  message: string;\n  line: number; // 1-indexed\n  column: number; // 0-indexed\n  severity: 'error' | 'warning';\n}\n```\n\n### `getAllRuleNames(): string[]`\n\nReturns an array of all available rule names.\n\n### `getRulesForPlatform(platform: Platform): string[]`\n\nReturns rule names for a platform (platform-tagged + universal rules).\n\n**Platforms:** `'expo' | 'web' | 'backend'`\n\n## Development\n\n```bash\nnpm install     # Install dependencies\nnpm test        # Run tests\nnpm run build   # Build TypeScript\nnpm run lint    # ESLint + Prettier\nnpm run knip    # Dead code detection\n```\n","readmeFilename":"README.md"}