Skip to content

Instantly share code, notes, and snippets.

@Werve
Last active May 11, 2026 16:47
Show Gist options
  • Select an option

  • Save Werve/85f17503eb98c1cba4dd5963bc3bfe07 to your computer and use it in GitHub Desktop.

Select an option

Save Werve/85f17503eb98c1cba4dd5963bc3bfe07 to your computer and use it in GitHub Desktop.
Claude jsx to html, works on termux
#!/usr/bin/env python3
"""
JSX → Standalone HTML Converter
Vibe coded using Hermes Agent
Converts single-file React JSX components into self-contained HTML documents.
Works offline — no server required. Handles React hooks, CSS-in-JS, and script escaping.
Usage:
python3 jsx2html.py input.jsx [output.html] [--offline] [--component Name]
./jsx2html.py input.jsx output.html --offline
Dependencies:
npm install react@18 react-dom@18 @babel/core @babel/cli @babel/preset-react
Features:
- Transpiles JSX → JS (Babel)
- Auto-strips ES module imports/exports
- Prefixes all React API calls (hooks, createElement, etc.) with React.
- Extracts CSS-in-JS (const CSS = `...`) into <style>
- Escapes </script> inside inline JS
- Auto-detects component name (default export) or use --component
- Injects React UMD (offline mode) or uses CDN (default)
- Visual error display (no blank pages)
- Platform-agnostic (no Termux-specific paths)
"""
import re
import subprocess
import sys
import shutil
from pathlib import Path
def run(cmd, **kw):
try:
result = subprocess.run(cmd, capture_output=True, text=True, **kw)
except FileNotFoundError as e:
print(f"❌ Command not found: {cmd[0]}")
print(f" Full command: {' '.join(cmd)}")
print(f" CWD: {kw.get('cwd', 'current')}")
sys.exit(1)
except PermissionError as e:
print(f"❌ Permission denied: {cmd[0]}")
print(f" Fix: chmod +x {cmd[0]}")
sys.exit(1)
if result.returncode != 0:
print(f"❌ {' '.join(cmd)}:\\n{result.stderr[:500]}")
sys.exit(1)
return result
def detect_component_name(jsx_path: Path, fallback_code: str) -> str:
"""
Detect the default export component name.
Priority:
1. From original JSX source: 'export default function X' or 'export default X'
2. From transpiled JS: first uppercase function/const/class (fallback)
"""
# 1 — Try original JSX source
try:
source = jsx_path.read_text(encoding='utf-8')
# Pattern: export default function Name( or export default Name
patterns = [
r'export\s+default\s+function\s+([A-Z][A-Za-z0-9_]*)\s*\(', # export default function X(
r'export\s+default\s+([A-Z][A-Za-z0-9_]*)\s*(?:\(|=|;)', # export default X (var/const/ident)
]
for pat in patterns:
m = re.search(pat, source)
if m:
name = m.group(1)
if name not in ('React', 'ReactDOM', 'window', 'document'):
return name
except Exception as e:
print(f" ⚠️ Could not read JSX source for component detection: {e}")
# 2 — Fallback: inspect transpiled JS (first uppercase function/const/class)
return detect_component_name_from_js(fallback_code)
def detect_component_name_from_js(code: str) -> str:
"""Fallback: detect from transpiled JS code."""
patterns = [
r'function\s+([A-Z][A-Za-z0-9_]*)\s*\(',
r'const\s+([A-Z][A-Za-z0-9_]*)\s*=',
r'class\s+([A-Z][A-Za-z0-9_]*)\s*',
r'var\s+([A-Z][A-Za-z0-9_]*)\s*=',
]
for pat in patterns:
m = re.search(pat, code)
if m:
name = m.group(1)
if name not in ('React', 'ReactDOM', 'window', 'document', 'console', 'Error', 'JSON', 'Math', 'Date'):
return name
return 'App'
def main():
if len(sys.argv) < 2:
print("Usage: jsx2html.py input.jsx [output.html] [--offline] [--component Name]")
sys.exit(1)
input_jsx = Path(sys.argv[1]).resolve()
# Parse args
output_html = None
offline_mode = False
component_name_override = None
for arg in sys.argv[2:]:
if arg == '--offline':
offline_mode = True
elif arg.startswith('--component='):
component_name_override = arg.split('=', 1)[1]
elif not arg.startswith('--') and output_html is None:
output_html = Path(arg).resolve()
if output_html is None:
output_html = input_jsx.with_suffix('.html')
workdir = input_jsx.parent
dist = workdir / 'dist_jsx2html'
dist.mkdir(exist_ok=True)
print(f"📄 Input: {input_jsx.name}")
print(f"📁 Workdir: {workdir}")
print(f"⚙️ Mode: {'OFFLINE (React embedded)' if offline_mode else 'CDN (React from unpkg)'}")
# Step 1: Babel transpile
print("🔧 Step 1: Babel transpile...")
app_js = dist / 'app.js'
# Determine Babel command
# Search order for Babel JS (@babel/cli/bin/babel.js):
# 1) workdir/node_modules/@babel/cli/bin/babel.js (project-local)
# 2) ~/workspace/node_modules/@babel/cli/bin/babel.js (common Termux workspace)
# 3) Global npm prefix: $(npm root -g)/@babel/cli/bin/babel.js
# 4) ~/node_modules/@babel/cli/bin/babel.js
# 5) Fallback to babel binary in PATH (global)
babel_js_candidates = [
workdir / 'node_modules' / '@babel' / 'cli' / 'bin' / 'babel.js',
Path.home() / 'workspace' / 'node_modules' / '@babel' / 'cli' / 'bin' / 'babel.js',
]
# Try global npm prefix
try:
np = subprocess.run(['npm', 'root', '-g'], capture_output=True, text=True, timeout=5)
if np.returncode == 0:
global_root = Path(np.stdout.strip())
babel_js_candidates.append(global_root / '@babel' / 'cli' / 'bin' / 'babel.js')
except Exception:
pass
babel_js_candidates.extend([
Path.home() / 'node_modules' / '@babel' / 'cli' / 'bin' / 'babel.js',
])
babel_cmd = None
babel_cwd = workdir
is_global = False
project_root = None # will be set if we find a non-global Babel
for js_path in babel_js_candidates:
print(f"🔍 Checking Babel at: {js_path}")
if js_path.exists():
# Verify that @babel/preset-react is resolvable from the same node_modules root
root = js_path.parent.parent.parent.parent.parent # <root>
preset_path = root / 'node_modules' / '@babel' / 'preset-react' / 'package.json'
if not preset_path.exists():
print(f" ⚠️ Babel found but preset-react missing at {preset_path.parent}")
print(f" Install: (cd {root} && npm install @babel/preset-react)")
continue
print(f" ✅ Found Babel with preset-react")
babel_cmd = ['node', str(js_path)]
babel_cwd = root
project_root = root # save for offline deps
break
else:
print(f" ✘ Not found")
if babel_cmd is None:
print("⚡ Fallback to global Babel binary 'babel' in PATH")
babel_bin = shutil.which('babel')
if not babel_bin:
print("❌ Babel not found.")
print(" Install locally: npm install @babel/core @babel/cli @babel/preset-react")
print(" Or globally: npm install -g @babel/core @babel/cli @babel/preset-react")
sys.exit(1)
# Even with global binary, we need to ensure preset-react is available
# Global Babel typically resolves from global node_modules; but we can't easily verify.
# We'll try and if it fails, give clear guidance.
babel_cmd = [str(babel_bin)]
is_global = True
babel_cwd = workdir
# Run Babel with explicit preset
cmd = babel_cmd + [str(input_jsx), '--out-file', str(app_js), '--presets=@babel/preset-react']
result = run(cmd, cwd=babel_cwd)
# If using global Babel and it fails due to missing preset, give clear guidance
if is_global and result.returncode != 0:
st = result.stderr
if 'Cannot find package' in st or 'Unknown preset' in st or 'The plugin/preset' in st:
print("❌ Babel preset '@babel/preset-react' not found in global installation.")
print(" Fix: npm install -g @babel/preset-react")
print(" Or: copy this file to a project folder with local node_modules and run there:")
print(f" cp {input_jsx} {Path.home()}/workspace/")
print(f" cd {Path.home()}/workspace && jsx2html {input_jsx.name}")
sys.exit(1)
else:
print(f"❌ Babel error: {st[:200]}")
sys.exit(1)
size_kb = app_js.stat().st_size // 1024 if app_js.exists() else 0
print(f" ✓ Babel: {size_kb}KB")
# After Babel transpile, we have babel_cwd (dir where Babel's node_modules lives) and is_global flag.
# For offline mode, we need React UMD files. Search strategy:
# 1) In the same node_modules root as Babel (project_root) if Babel is non-global
# 2) In workdir/node_modules (the JSX's directory) as fallback
if offline_mode:
# Candidate 1: Babel's node_modules root (only if we have a non-global Babel)
react_path = None
react_dom_path = None
if not is_global:
# project_root was set when we found Babel JS
candidate_root = project_root
rp = candidate_root / 'node_modules' / 'react' / 'umd' / 'react.production.min.js'
rd = candidate_root / 'node_modules' / 'react-dom' / 'umd' / 'react-dom.production.min.js'
if rp.exists() and rd.exists():
react_path = rp
react_dom_path = rd
print(f"📦 Offline deps from Babel root: {candidate_root}")
# Candidate 2: workdir (the directory containing the JSX)
if react_path is None:
rp2 = workdir / 'node_modules' / 'react' / 'umd' / 'react.production.min.js'
rd2 = workdir / 'node_modules' / 'react-dom' / 'umd' / 'react-dom.production.min.js'
if rp2.exists() and rd2.exists():
react_path = rp2
react_dom_path = rd2
print(f"⚡ Offline deps from workdir: {workdir}")
if react_path is None:
print("❌ React UMD not found for offline mode.")
print(" Install React in the project directory:")
print(" npm install react@18 react-dom@18")
sys.exit(1)
else:
project_root = None
code = app_js.read_text(encoding='utf-8')
# 2 — Strip imports/exports
print("🔧 Step 2: Strip imports/exports...")
code = re.sub(r'^\s*import\s+.*?;\s*\n?', '', code, flags=re.MULTILINE)
code = re.sub(r'export\s+default\s+', '', code)
# 3 — Substitute React APIs (hooks, createElement, etc.)
print("🔧 Step 3: Substitute React API calls...")
react_apis = [
# Hooks
'useState', 'useEffect', 'useRef', 'useCallback', 'useMemo',
'useReducer', 'useContext', 'useLayoutEffect', 'useImperativeHandle',
'useDebugValue', 'useTransition', 'useDeferredValue', 'useId',
'useSyncExternalStore',
# Creators
'createContext', 'createElement', 'Fragment',
# Components
'StrictMode', 'Suspense', 'lazy',
# Classes / utils
'Component', 'PureComponent', 'memo', 'forwardRef',
'isValidElement', 'cloneElement', 'Children',
# Suspense/Concurrent (newer)
'startTransition', 'useOptimistic', 'use', 'SuspenseList',
]
for api in react_apis:
pattern = r'(?<!React\.)\b' + re.escape(api) + r'\b'
code = re.sub(pattern, 'React.' + api, code)
# Save after substitution for debugging
(dist / 'after_subst.js').write_text(code, encoding='utf-8')
# 4 — GCSS extraction & injection
print("🔧 Step 4: Extract GCSS into <style>...")
gcss_match = re.search(r'const\s+GCSS\s*=\s*`([^`]+)`', code, re.DOTALL)
if gcss_match:
gcss_clean = gcss_match.group(1)
code = re.sub(r'const\s+GCSS\s*=\s*`.*?`;', '', code, flags=re.DOTALL)
code = re.sub(r'\bGCSS\b', "''", code)
print(f" ✓ GCSS extracted ({len(gcss_clean)} chars)")
else:
gcss_clean = ''
print(" ⚠️ No GCSS found")
# 5 — Auto-detect component name
comp_name = component_name_override or detect_component_name(input_jsx, code)
print(f"🔧 Step 5: Component detected: '{comp_name}' (override: {component_name_override or 'auto'})")
# 6 — Mount code (with error display)
mount_code = f"""
(function() {{
try {{
const rootEl = document.getElementById('root');
if (!rootEl) throw new Error('#root element missing');
const root = window.ReactDOM.createRoot(rootEl);
// Use detected component name
const Component = window.{comp_name};
if (!Component) throw new Error('Component "{comp_name}" not found. Check console for available globals.');
root.render(React.createElement(Component));
console.log('✅ App mounted');
}} catch (err) {{
console.error('❌ Mount failed:', err);
const errDiv = document.getElementById('jsx-error') || document.body;
errDiv.innerHTML = '<div style="color:#f88;background:#111;padding:16px;margin:12px;border:1px solid #f44;border-radius:4px;white-space:pre-wrap;font-family:monospace;font-size:12px">' +
'<strong>RENDER ERROR</strong><br>' +
(err.stack || err.message) +
'</div>';
}}
}})();
"""
full_js = code.rstrip() + f"\n\n// Export component to window for mount\nwindow.{comp_name} = {comp_name};\n\n" + mount_code.strip()
# Prepend global error handler BEFORE component definition
full_js = "window.onerror = function(msg, src, line, col, err) { console.error('JS ERROR:', msg, 'line', line, err?.stack); return false; };\n" + full_js
# 7 — Escape </script> in JS to avoid premature tag closing
full_js = full_js.replace('</script>', '<\\/script>')
# 8 — Generate HTML
print("🔧 Step 6: Generate HTML...")
if offline_mode:
# Inline React UMD (React 18)
react_path = project_root / 'node_modules' / 'react' / 'umd' / 'react.production.min.js'
react_dom_path = project_root / 'node_modules' / 'react-dom' / 'umd' / 'react-dom.production.min.js'
if not react_path.exists():
print("❌ React UMD not found. Run: npm install react@18 react-dom@18")
sys.exit(1)
react_js = react_path.read_text()
react_dom_js = react_dom_path.read_text()
html = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>React App</title>
<style>
/* GCSS extracted from component */
{gcss_clean}
/* Reset + full-height */
html,body,#root{{margin:0;padding:0;height:100%;}}
</style>
</head>
<body>
<div id="root"></div>
<div id="jsx-error"></div>
<!-- React UMD (embedded) -->
<script>{react_js}</script>
<script>{react_dom_js}</script>
<!-- Application code -->
<script>
{full_js}
</script>
</body>
</html>"""
else:
# CDN mode — React from unpkg
html = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>React App</title>
<style>
/* GCSS extracted from component */
{gcss_clean}
html,body,#root{{margin:0;padding:0;height:100%;}}
</style>
<!-- React from CDN -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
</head>
<body>
<div id="root"></div>
<div id="jsx-error"></div>
<!-- Application code -->
<script>
{full_js}
</script>
</body>
</html>"""
output_html.parent.mkdir(parents=True, exist_ok=True)
output_html.write_text(html, encoding='utf-8')
size_kb = output_html.stat().st_size // 1024
print(f"✅ Created: {output_html} ({size_kb} KB)")
# Suggest open command
if offline_mode:
print(f"\n🚀 Open: file://{output_html}")
else:
print(f"\n🌐 Open: file://{output_html} (requires internet for React CDN)")
if __name__ == '__main__':
main()
@Werve

Werve commented May 9, 2026

Copy link
Copy Markdown
Author

Created using Hermes agent

JSX to Standalone HTML

Converts single-file React JSX components into self-contained HTML documents.
No server required — works directly from the filesystem.

python3 jsx2html.py input.jsx [output.html] [--offline]
# or globally (if installed):
jsx2html input.jsx output.html --offline

Features

  • Standalone HTML — React embedded (offline) or CDN (online)
  • Auto-detect component — reads export default from JSX source
  • CSS extraction — GCSS (CSS-in-JS) converted to <style>
  • Portable — finds Babel automatically (local, workspace, global npm)
  • Zero config — no .babelrc needed; preset forced via CLI

Installation

Prerequisites

  • Python 3.8+
  • Node.js + npm (for Babel)

Setup (local project)

cd /path/to/your/project
npm install @babel/core @babel/cli @babel/preset-react
# Optional: React for offline mode
npm install react@18 react-dom@18

Setup (global, works anywhere)

npm install -g @babel/core @babel/cli @babel/preset-react
# Optional: React for offline mode
npm install -g react@18 react-dom@18

Termux: install binary

# Binary installs to ~/bin (already in PATH in Termux)
cp jsx2html.py ~/bin/jsx2html
chmod +x ~/bin/jsx2html

Usage

Basic (CDN mode — default)

Uses React from unpkg CDN. Works without local React install.

jsx2html component.jsx output.html
# or
python3 jsx2html.py component.jsx output.html

Generated HTML loads React from CDN:

<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

Offline mode (embedded React)

Bundles React directly into HTML (no internet needed). Requires React UMD files.

How it finds React UMD:

  1. First, in the same node_modules root that provided Babel (if Babel is project-local)
  2. If not found, falls back to the JSX file's directory node_modules (workdir/node_modules)

This lets you keep Babel in a shared location (like ~/workspace) while installing React per-project.

If React UMD is not found, install it in the project directory:

npm install react@18 react-dom@18

How Babel is found (detailed)

The script searches for Babel (@babel/cli/bin/babel.js) in this order:

  1. ./node_modules/@babel/cli/bin/babel.js (current directory)
  2. ~/workspace/node_modules/@babel/cli/bin/babel.js (common Termux workspace)
  3. Global npm prefix: $(npm root -g)/@babel/cli/bin/babel.js
  4. ~/node_modules/@babel/cli/bin/babel.js (home)
  5. babel binary in $PATH

For each candidate, it verifies that @babel/preset-react is also installed in that same node_modules root. If Babel is found but the preset is missing, it skips to the next candidate.

Why this matters: If you see React UMD not found even though ls node_modules/react/umd/react.production.min.js exists, it's because Babel was loaded from a different location that doesn't have React. The script will automatically fall back to your project's node_modules for React UMD. If that also fails, ensure React is installed in the project dir.

Examples

Single component (Counter.jsx)

import { useState } from 'react';

export default function Counter() {
  const [n, setN] = useState(0);
  return (
    <div>
      <h1>Count: {n}</h1>
      <button onClick={() => setN(n+1)}>+</button>
      <style>{`
        div { font: 16px system-ui; padding: 20px; }
        button { padding: 8px 16px; }
      `}</style>
    </div>
  );
}

Convert:

jsx2html Counter.jsx Counter.html --offline

Opens in browser: file:///path/to/Counter.html

GCSS extraction

If your component uses inline styles with nested rules (GCSS), they are extracted:

export default function Card() {
  return (
    <div style={{
      background: white,
      '@media (max-width: 600px)': { padding: 8 }
    }}>
      ...
    </div>
  );
}

→ CSS becomes:

<style>
  ._gcss_1 { background: white; }
  @media (max-width: 600px) { ._gcss_1 { padding: 8; } }
</style>

Limitations

  • Single exported component per file (default export only).
  • No external imports (except React hooks) — all code must be inline.
  • Requires Babel + preset-react (auto-detected).
  • Offline mode requires React UMD files in node_modules (installed via npm install react@18 react-dom@18).
  • Does not support CSS modules, asset imports, or code-splitting.
  • Large dependencies increase HTML size (offline mode embeds React ~150 KB gzipped).

Troubleshooting

❌ Babel not found

Install Babel locally or globally:

npm install @babel/core @babel/cli @babel/preset-react
# or
npm install -g @babel/core @babel/cli @babel/preset-react

❌ React UMD not found (offline mode)

Install React locally:

npm install react@18 react-dom@18

Or use CDN mode (omit --offline) which doesn't embed React.

Error: Unknown preset or Cannot find package '@babel/preset-react'

The Babel you are using doesn't have the preset. Ensure it's installed:

# If using global Babel
npm install -g @babel/preset-react

# If using local Babel
npm install @babel/preset-react

Permissions errors on .bin/babel (Termux)

The script runs Babel via node path/to/babel.js directly, avoiding shebang/permission issues.

License

MIT — use freely.

@SamSimp

SamSimp commented May 10, 2026

Copy link
Copy Markdown

Hi - thanks for this! Looking forward to using it but am having an issue (newbie alert!)

"npm install react@18 react-dom@18" reports: up to date, audited 6 packages in 604ms

but "jsx2html.py abc.jsx --offline" reports "React UMD not found. Run: npm install react@18 react-dom@18" even though "ls ./node_modules/react/umd/react.production.min.js" shows this file is in place.

I'm sure i'm doing something daft (Babel / react are all new to me) - appreciate any pointers?

I've also tried without --offline but the HTML file just shows a blank page 🤦

@Werve

Werve commented May 11, 2026

Copy link
Copy Markdown
Author

Hi - thanks for this! Looking forward to using it but am having an issue (newbie alert!)

"npm install react@18 react-dom@18" reports: up to date, audited 6 packages in 604ms

but "jsx2html.py abc.jsx --offline" reports "React UMD not found. Run: npm install react@18 react-dom@18" even though "ls ./node_modules/react/umd/react.production.min.js" shows this file is in place.

I'm sure i'm doing something daft (Babel / react are all new to me) - appreciate any pointers?

I've also tried without --offline but the HTML file just shows a blank page 🤦

Hi, I have updated the script if you want to try again.

@SamSimp

SamSimp commented May 11, 2026

Copy link
Copy Markdown

Thanks Werve - the process works per the output below, which results in a HTML file, but this is just a blank page in both latest Chrome and Safari?

/jsx2html.py fc-weekly-report-wc-05-may-2026.jsx

📄 Input: fc-weekly-report-wc-05-may-2026.jsx
📁 Workdir: /Users/sam/Downloads
⚙️ Mode: CDN (React from unpkg)
🔧 Step 1: Babel transpile...
🔍 Checking Babel at: /Users/sam/Downloads/node_modules/@babel/cli/bin/babel.js
✘ Not found
🔍 Checking Babel at: /Users/sam/workspace/node_modules/@babel/cli/bin/babel.js
✘ Not found
🔍 Checking Babel at: /opt/homebrew/lib/node_modules/@babel/cli/bin/babel.js
✅ Found Babel with preset-react
✓ Babel: 39KB
🔧 Step 2: Strip imports/exports...
🔧 Step 3: Substitute React API calls...
🔧 Step 4: Extract GCSS into <style>...
⚠️ No GCSS found
🔧 Step 5: Component detected: 'Report' (override: auto)
🔧 Step 6: Generate HTML...
✅ Created: /Users/sam/Downloads/fc-weekly-report-wc-05-may-2026.html (40 KB)

🌐 Open: file:///Users/sam/Downloads/fc-weekly-report-wc-05-may-2026.html (requires internet for React CDN)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment