has-jsx 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elchin Valiyev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # has-jsx
2
+
3
+ Detect JSX in files and strings using AST analysis.
4
+
5
+ ## Why has-jsx?
6
+
7
+ Detecting JSX isn't as simple as checking file extensions. JavaScript and TypeScript files (`.js`, `.ts`) can contain JSX syntax. This tool uses [ast-grep](https://github.com/ast-grep/ast-grep)'s powerful AST parsing to reliably detect JSX node types from the [JSX specification](https://facebook.github.io/jsx/):
8
+
9
+ - `jsx_element` - Standard tags: `<div>...</div>`
10
+ - `jsx_self_closing_element` - Self-closing: `<Component />`
11
+ - `jsx_fragment` - Fragments: `<>...</>`
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install has-jsx
17
+ ```
18
+
19
+ ## CLI Usage
20
+
21
+ ### Check Files
22
+
23
+ Use the `-f` or `--file` flag to check files:
24
+
25
+ ```bash
26
+ has-jsx -f src/Component.tsx
27
+ # Output: JSX detected
28
+ # Exit code: 0
29
+
30
+ has-jsx --file src/utils.ts
31
+ # Output: No JSX detected
32
+ # Exit code: 1
33
+ ```
34
+
35
+ ### Check Strings
36
+
37
+ Pass source code directly without any flags:
38
+
39
+ ```bash
40
+ has-jsx "const App = () => <div>Hello</div>"
41
+ # Output: JSX detected
42
+ # Exit code: 0
43
+
44
+ has-jsx "const x = 5;"
45
+ # Output: No JSX detected
46
+ # Exit code: 1
47
+ ```
48
+
49
+ ### Options
50
+
51
+ | Flag | Description |
52
+ |------|-------------|
53
+ | `-f, --file <path>` | Check a file for JSX |
54
+ | `--verbose` | Output results as JSON |
55
+ | `--quiet` | Silent mode, exit code only |
56
+ | `--version` | Show version number |
57
+ | `--help` | Show help |
58
+
59
+ ### Exit Codes
60
+
61
+ | Code | Meaning |
62
+ |------|---------|
63
+ | `0` | JSX detected |
64
+ | `1` | No JSX detected |
65
+ | `2` | Error (file not found, read error, etc.) |
66
+
67
+ ## Programmatic API
68
+
69
+ ### File-based Detection
70
+
71
+ Check if a file contains JSX:
72
+
73
+ **ESM (recommended):**
74
+ ```javascript
75
+ import hasJSX from 'has-jsx';
76
+
77
+ const result = await hasJSX('src/Component.tsx');
78
+ console.log(result); // true or false
79
+ ```
80
+
81
+ **CommonJS:**
82
+ ```javascript
83
+ const { hasJSX } = require('has-jsx');
84
+
85
+ (async () => {
86
+ const result = await hasJSX('src/Component.tsx');
87
+ console.log(result);
88
+ })();
89
+ ```
90
+
91
+ ### String-based Detection
92
+
93
+ Check if a string contains JSX (synchronous, no file I/O):
94
+
95
+ **ESM:**
96
+ ```javascript
97
+ import { hasJSXInString } from 'has-jsx';
98
+
99
+ const code = `
100
+ function App() {
101
+ return <div>Hello World</div>;
102
+ }
103
+ `;
104
+
105
+ const result = hasJSXInString(code);
106
+ console.log(result); // true
107
+ ```
108
+
109
+ **CommonJS:**
110
+ ```javascript
111
+ const { hasJSXInString } = require('has-jsx');
112
+
113
+ const code = 'const App = () => <div>Hello</div>';
114
+ const result = hasJSXInString(code);
115
+ console.log(result); // true
116
+ ```
117
+
118
+ ### API Reference
119
+
120
+ #### `hasJSX(filepath: string): Promise<boolean>`
121
+
122
+ Analyzes a file to detect JSX syntax.
123
+
124
+ **Parameters:**
125
+ - `filepath` - Path to the file to analyze (relative or absolute)
126
+
127
+ **Returns:**
128
+ - `Promise<boolean>` - `true` if JSX is detected, `false` otherwise
129
+
130
+ **Throws:**
131
+ - `Error` - If file doesn't exist, is not a file, or can't be read
132
+
133
+ **Example:**
134
+
135
+ ```javascript
136
+ import hasJSX from 'has-jsx';
137
+
138
+ try {
139
+ const result = await hasJSX('src/Component.tsx');
140
+ if (result) {
141
+ console.log('This file contains JSX');
142
+ }
143
+ } catch (error) {
144
+ console.error('Error:', error.message);
145
+ }
146
+ ```
147
+
148
+ #### `hasJSXInString(source: string): boolean`
149
+
150
+ Analyzes a source code string to detect JSX syntax (synchronous).
151
+
152
+ **Parameters:**
153
+ - `source` - Source code string to analyze
154
+
155
+ **Returns:**
156
+ - `boolean` - `true` if JSX is detected, `false` otherwise
157
+
158
+ **Throws:**
159
+ - `TypeError` - If source is not a string
160
+
161
+ **Example:**
162
+
163
+ ```javascript
164
+ import { hasJSXInString } from 'has-jsx';
165
+
166
+ const code = 'const Button = () => <button>Click</button>';
167
+ const result = hasJSXInString(code);
168
+ console.log(result); // true
169
+
170
+ // No false positives for TypeScript generics
171
+ const genericCode = 'function identity<T>(arg: T): T { return arg; }';
172
+ console.log(hasJSXInString(genericCode)); // false
173
+ ```
174
+
175
+ ## How It Works
176
+
177
+ 1. **AST Parsing**: Uses [ast-grep](https://github.com/ast-grep/ast-grep) to parse files as TypeScript/TSX
178
+ 2. **Node Detection**: Searches the AST for JSX-specific node types defined in the [JSX specification](https://facebook.github.io/jsx/)
179
+ 3. **Reliable Results**: Avoids false positives from regex-based detection or angle brackets in comparisons
180
+
181
+ ## Use Cases
182
+
183
+ - **Build Tools**: Determine which files need JSX transformation
184
+ - **Linters**: Validate JSX usage against project rules
185
+ - **Code Analysis**: Audit codebase for JSX patterns
186
+ - **CI/CD**: Verify file types in automated pipelines
187
+ - **Migration Tools**: Identify files during framework migrations
188
+
189
+ ## Requirements
190
+
191
+ - Node.js >= 20.0.0
192
+
193
+ ## License
194
+
195
+ MIT
196
+
197
+ ## Contributing
198
+
199
+ Issues and pull requests welcome! Please ensure all tests pass before submitting:
200
+
201
+ ```bash
202
+ npm test
203
+ ```
package/bin/has-jsx.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/cli.js';
4
+
5
+ run(process.argv).catch((error) => {
6
+ console.error(error.message);
7
+ process.exit(1);
8
+ });
package/dist/index.cjs ADDED
@@ -0,0 +1,63 @@
1
+ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } });
2
+ let fs_promises = require("fs/promises");
3
+ let fs = require("fs");
4
+ let _ast_grep_napi = require("@ast-grep/napi");
5
+
6
+ //#region src/utils.js
7
+ /**
8
+ * Reads file content from the filesystem with validation.
9
+ *
10
+ * @param {string} filepath - Absolute or relative path to file
11
+ * @returns {Promise<string>} File content
12
+ * @throws {Error} If file doesn't exist, is not a file, or can't be read
13
+ */
14
+ async function readFileContent(filepath) {
15
+ if (!(0, fs.existsSync)(filepath)) throw new Error(`File not found: ${filepath}`);
16
+ if (!(0, fs.statSync)(filepath).isFile()) throw new Error(`Path is not a file: ${filepath}`);
17
+ return (0, fs_promises.readFile)(filepath, "utf-8");
18
+ }
19
+
20
+ //#endregion
21
+ //#region src/detector.js
22
+ /**
23
+ * Detects if source code contains JSX syntax using AST analysis.
24
+ *
25
+ * @param {string} source - Source code to analyze
26
+ * @returns {boolean} true if JSX is detected, false otherwise
27
+ */
28
+ function containsJSX(source) {
29
+ try {
30
+ return _ast_grep_napi.tsx.parse(source).root().findAll({ rule: { any: [{ kind: "jsx_element" }, { kind: "jsx_self_closing_element" }] } }).length > 0;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ //#endregion
37
+ //#region src/index.js
38
+ /**
39
+ * Checks if a file contains JSX syntax.
40
+ *
41
+ * @param {string} filepath - Path to the file to analyze
42
+ * @returns {Promise<boolean>} true if JSX is detected, false otherwise
43
+ * @throws {Error} If file can't be read
44
+ */
45
+ async function hasJSX(filepath) {
46
+ return containsJSX(await readFileContent(filepath));
47
+ }
48
+ /**
49
+ * Checks if a string contains JSX syntax.
50
+ *
51
+ * @param {string} source - Source code string to analyze
52
+ * @returns {boolean} true if JSX is detected, false otherwise
53
+ * @throws {TypeError} If source is not a string
54
+ */
55
+ function hasJSXInString(source) {
56
+ if (typeof source !== "string") throw new TypeError("Expected source to be a string");
57
+ return containsJSX(source);
58
+ }
59
+
60
+ //#endregion
61
+ exports.default = hasJSX;
62
+ exports.hasJSX = hasJSX;
63
+ exports.hasJSXInString = hasJSXInString;
package/dist/index.mjs ADDED
@@ -0,0 +1,60 @@
1
+ import { readFile } from "fs/promises";
2
+ import { existsSync, statSync } from "fs";
3
+ import { tsx } from "@ast-grep/napi";
4
+
5
+ //#region src/utils.js
6
+ /**
7
+ * Reads file content from the filesystem with validation.
8
+ *
9
+ * @param {string} filepath - Absolute or relative path to file
10
+ * @returns {Promise<string>} File content
11
+ * @throws {Error} If file doesn't exist, is not a file, or can't be read
12
+ */
13
+ async function readFileContent(filepath) {
14
+ if (!existsSync(filepath)) throw new Error(`File not found: ${filepath}`);
15
+ if (!statSync(filepath).isFile()) throw new Error(`Path is not a file: ${filepath}`);
16
+ return readFile(filepath, "utf-8");
17
+ }
18
+
19
+ //#endregion
20
+ //#region src/detector.js
21
+ /**
22
+ * Detects if source code contains JSX syntax using AST analysis.
23
+ *
24
+ * @param {string} source - Source code to analyze
25
+ * @returns {boolean} true if JSX is detected, false otherwise
26
+ */
27
+ function containsJSX(source) {
28
+ try {
29
+ return tsx.parse(source).root().findAll({ rule: { any: [{ kind: "jsx_element" }, { kind: "jsx_self_closing_element" }] } }).length > 0;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ //#endregion
36
+ //#region src/index.js
37
+ /**
38
+ * Checks if a file contains JSX syntax.
39
+ *
40
+ * @param {string} filepath - Path to the file to analyze
41
+ * @returns {Promise<boolean>} true if JSX is detected, false otherwise
42
+ * @throws {Error} If file can't be read
43
+ */
44
+ async function hasJSX(filepath) {
45
+ return containsJSX(await readFileContent(filepath));
46
+ }
47
+ /**
48
+ * Checks if a string contains JSX syntax.
49
+ *
50
+ * @param {string} source - Source code string to analyze
51
+ * @returns {boolean} true if JSX is detected, false otherwise
52
+ * @throws {TypeError} If source is not a string
53
+ */
54
+ function hasJSXInString(source) {
55
+ if (typeof source !== "string") throw new TypeError("Expected source to be a string");
56
+ return containsJSX(source);
57
+ }
58
+
59
+ //#endregion
60
+ export { hasJSX as default, hasJSX, hasJSXInString };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "has-jsx",
3
+ "version": "0.0.1",
4
+ "description": "CLI and programmatic tool to detect JSX in files using AST analysis",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "bin": {
9
+ "has-jsx": "bin/has-jsx.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.mjs",
14
+ "require": "./dist/index.cjs",
15
+ "default": "./dist/index.mjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "bin",
21
+ "src",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "scripts": {
26
+ "build": "npx tsdown",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "test:coverage": "vitest run --coverage",
30
+ "prepublishOnly": "npm run build && npm test"
31
+ },
32
+ "keywords": [
33
+ "jsx",
34
+ "detect",
35
+ "ast",
36
+ "cli",
37
+ "typescript",
38
+ "react"
39
+ ],
40
+ "author": "",
41
+ "license": "MIT",
42
+ "engines": {
43
+ "node": ">=20.0.0"
44
+ },
45
+ "dependencies": {
46
+ "@ast-grep/napi": "^0.40.5",
47
+ "commander": "^14.0.3"
48
+ },
49
+ "devDependencies": {
50
+ "tsdown": "^0.20.3",
51
+ "vitest": "^4.0.18"
52
+ }
53
+ }
package/src/cli.js ADDED
@@ -0,0 +1,100 @@
1
+ import { readFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ import { Command } from 'commander';
6
+
7
+ import { hasJSX, hasJSXInString } from './index.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ /**
13
+ * Runs the CLI with the provided arguments.
14
+ *
15
+ * @param {string[]} argv - Command line arguments (process.argv)
16
+ * @returns {Promise<void>}
17
+ */
18
+ export async function run(argv) {
19
+ const packageJson = JSON.parse(
20
+ readFileSync(join(__dirname, '../package.json'), 'utf-8')
21
+ );
22
+
23
+ const program = new Command();
24
+
25
+ program
26
+ .name('has-jsx')
27
+ .description('Detect JSX in files or strings using AST analysis')
28
+ .version(packageJson.version)
29
+ .argument('[source]', 'Source code string to analyze (use -f for files)')
30
+ .option('-f, --file <filepath>', 'Path to file to analyze')
31
+ .option('-v, --verbose', 'Show detailed JSON output')
32
+ .option('-q, --quiet', 'Silent mode (exit codes only)')
33
+ .exitOverride()
34
+ .configureOutput({
35
+ writeOut: (str) => console.log(str.trimEnd()),
36
+ writeErr: (str) => console.error(str.trimEnd()),
37
+ })
38
+ .action(async (source, options) => {
39
+ try {
40
+ let result;
41
+ let inputType;
42
+ let inputValue;
43
+
44
+ if (options.file) {
45
+ result = await hasJSX(options.file);
46
+ inputType = 'file';
47
+ inputValue = options.file;
48
+ } else if (source) {
49
+ result = hasJSXInString(source);
50
+ inputType = 'string';
51
+ inputValue = source;
52
+ } else {
53
+ if (!options.quiet) {
54
+ console.error('Error: No input provided. Use -f <filepath> or provide source code string.');
55
+ }
56
+ process.exit(2);
57
+ return;
58
+ }
59
+
60
+ const exitCode = result ? 0 : 1;
61
+
62
+ if (options.quiet) {
63
+ process.exit(exitCode);
64
+ } else if (options.verbose) {
65
+ const inputKey = inputType === 'file' ? 'file' : 'source';
66
+ const output = { hasJSX: result, inputType, [inputKey]: inputValue };
67
+ console.log(JSON.stringify(output, null, 2));
68
+ process.exit(exitCode);
69
+ } else {
70
+ console.log(result ? 'JSX detected' : 'No JSX detected');
71
+ process.exit(exitCode);
72
+ }
73
+ } catch (error) {
74
+ if (isProcessExitError(error)) {
75
+ throw error;
76
+ }
77
+ if (!options.quiet) {
78
+ console.error(`Error: ${error.message}`);
79
+ }
80
+ process.exit(2);
81
+ }
82
+ });
83
+
84
+ try {
85
+ await program.parseAsync(argv);
86
+ } catch (error) {
87
+ if (isProcessExitError(error) || isCommanderError(error)) {
88
+ return;
89
+ }
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ function isProcessExitError(error) {
95
+ return error.message?.startsWith('process.exit(');
96
+ }
97
+
98
+ function isCommanderError(error) {
99
+ return error.code?.startsWith('commander.');
100
+ }
@@ -0,0 +1,27 @@
1
+ import { tsx } from '@ast-grep/napi';
2
+
3
+ /**
4
+ * Detects if source code contains JSX syntax using AST analysis.
5
+ *
6
+ * @param {string} source - Source code to analyze
7
+ * @returns {boolean} true if JSX is detected, false otherwise
8
+ */
9
+ export function containsJSX(source) {
10
+ try {
11
+ const root = tsx.parse(source).root();
12
+
13
+ // jsx_element covers standard elements and fragments (<>...</>)
14
+ const jsxNodes = root.findAll({
15
+ rule: {
16
+ any: [
17
+ { kind: 'jsx_element' },
18
+ { kind: 'jsx_self_closing_element' },
19
+ ],
20
+ },
21
+ });
22
+
23
+ return jsxNodes.length > 0;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
package/src/index.js ADDED
@@ -0,0 +1,30 @@
1
+ import { readFileContent } from './utils.js';
2
+ import { containsJSX } from './detector.js';
3
+
4
+ /**
5
+ * Checks if a file contains JSX syntax.
6
+ *
7
+ * @param {string} filepath - Path to the file to analyze
8
+ * @returns {Promise<boolean>} true if JSX is detected, false otherwise
9
+ * @throws {Error} If file can't be read
10
+ */
11
+ export async function hasJSX(filepath) {
12
+ const content = await readFileContent(filepath);
13
+ return containsJSX(content);
14
+ }
15
+
16
+ /**
17
+ * Checks if a string contains JSX syntax.
18
+ *
19
+ * @param {string} source - Source code string to analyze
20
+ * @returns {boolean} true if JSX is detected, false otherwise
21
+ * @throws {TypeError} If source is not a string
22
+ */
23
+ export function hasJSXInString(source) {
24
+ if (typeof source !== 'string') {
25
+ throw new TypeError('Expected source to be a string');
26
+ }
27
+ return containsJSX(source);
28
+ }
29
+
30
+ export default hasJSX;
package/src/utils.js ADDED
@@ -0,0 +1,22 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { existsSync, statSync } from 'fs';
3
+
4
+ /**
5
+ * Reads file content from the filesystem with validation.
6
+ *
7
+ * @param {string} filepath - Absolute or relative path to file
8
+ * @returns {Promise<string>} File content
9
+ * @throws {Error} If file doesn't exist, is not a file, or can't be read
10
+ */
11
+ export async function readFileContent(filepath) {
12
+ if (!existsSync(filepath)) {
13
+ throw new Error(`File not found: ${filepath}`);
14
+ }
15
+
16
+ const stats = statSync(filepath);
17
+ if (!stats.isFile()) {
18
+ throw new Error(`Path is not a file: ${filepath}`);
19
+ }
20
+
21
+ return readFile(filepath, 'utf-8');
22
+ }