Metadata-Version: 2.4
Name: static-site-audit
Version: 0.1.0
Summary: Find dead buttons, duplicate ids and global-scope collisions in no-build HTML/JS sites
Author: GALLA Labs
License: MIT
Project-URL: Homepage, https://github.com/gallakorea/static-site-audit
Keywords: html,javascript,lint,audit,static-site,qa
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# static-site-audit

[![test](https://github.com/gallakorea/static-site-audit/actions/workflows/test.yml/badge.svg)](https://github.com/gallakorea/static-site-audit/actions/workflows/test.yml)

Find the bugs that make buttons silently do nothing in **no-build HTML/JS sites** —
plain `.html` pages with classic `<script>` tags, no React, no bundler.

Linters for that world mostly don't exist, so these checks came out of real bugs
in a production site with 80+ pages and ~2,000 buttons:

| Check | Catches | Why it's hard to see |
| --- | --- | --- |
| `dead-buttons` | Buttons no script can reach | With event delegation there's no listener on the button to look for |
| `dup-ids` | Duplicate ids, and ids your scripts inject that already exist in the HTML | `getElementById` returns the first one; the other never updates |
| `global-collisions` | Two classic scripts on one page declaring the same top-level `let`/`const`/`class` | The later script throws `Identifier 'x' has already been declared` and none of it runs — the page just looks empty |
| `SSA.reachability` (browser) | Same as `dead-buttons`, for buttons rendered by JS | They don't exist in the source |
| `SSA.clicks` (browser) | Controls that throw, log errors, or change nothing when pressed — with writes blocked | |
| `SSA.occlusion` (browser) | Controls covered by a fixed overlay, so a tap hits the overlay instead | `el.click()` skips hit-testing, so click tests pass |

## Install

```bash
pip install git+https://github.com/gallakorea/static-site-audit
```

Python 3.8+, no dependencies.

## Static checks (CLI)

```bash
static-site-audit path/to/site
static-site-audit . --check dup-ids --skip 'admin|-test\.html'
static-site-audit . --json > report.json
```

```
[dead-buttons] index.html
  ✗ "Nobody listens" — handles orphan-btn not found in any script

[global-collisions] index.html
  ✗ `store` declared at top level in js/a.js, js/b.js — js/b.js will fail to run

2 error(s), 0 warning(s)
```

| Option | |
| --- | --- |
| `--check NAME` | Run only this check (repeatable) |
| `--skip REGEX` | Ignore pages/scripts whose path matches |
| `--exclude-dir NAME` | Skip a directory (`node_modules`, `vendor`, `dist`, `build` are always skipped) |
| `--one-scope` | `global-collisions`: treat all scripts as one document, for SPA shells that inject page scripts |
| `--strict` | Exit 1 on warnings too |
| `--json` | Machine-readable output |

Exit code is 1 when there are errors, so it drops into CI as is.

### GitHub Actions

```yaml
- uses: actions/checkout@v4
- uses: gallakorea/static-site-audit@v0
  with:
    path: public          # site root, default "."
    args: --skip admin    # any CLI options
```

### pre-commit

```yaml
- repo: https://github.com/gallakorea/static-site-audit
  rev: v0
  hooks:
    - id: static-site-audit
      args: [--skip, admin]
```

### How `dead-buttons` decides

Many sites wire buttons like this:

```js
document.addEventListener("click", e => { if (e.target.closest(".buy")) buy(); });
```

so "does the button have a listener?" can't be answered. Instead, every handle the
button carries — `id`, classes, `data-*` names and values — is searched for in all
scripts, written the way a selector or lookup would write it (`"#buy"`, `'.buy'`,
`[data-buy]`). If none is found, the four nearest ancestors get the same test (a
parent may delegate). Buttons with `onclick`, `type="submit"`/`"reset"` and hidden
layout placeholders are skipped.

It's a heuristic: a class name that appears in a script for an unrelated reason
counts as "reachable". It errs on the side of fewer false alarms.

## Runtime checks (browser)

Open your site, sign in if the pages need it, paste
[`browser/static-site-audit.js`](browser/static-site-audit.js) into the console, then:

```js
await SSA.reachability(["index", "search", "item.html?id=3"])
await SSA.clicks("settings")
await SSA.occlusion("checkout")
```

Each page is loaded in a hidden same-origin iframe. Options go last:
`{ wait: 2000, width: 420, height: 900, skip: "nav,.tabbar" }`.

**`SSA.clicks` is safe to run signed in.** Non-GET `fetch`/XHR requests never reach
the network, `confirm()` always answers no, and the iframe can't navigate your tab.
Because writes are faked, APIs that *read* through POST (RPC endpoints, GraphQL)
get odd responses and may cause errors your real app never has — check before filing
a bug. `noEffect` is a candidate list, not a verdict.

## Contributing

Issues and PRs are welcome — especially false positives and false negatives from
real sites. Please include a minimal HTML/JS snippet that reproduces it; it can go
straight into `tests/fixtures/`. Run the tests with:

```bash
pip install -e . && python -m unittest discover -s tests
```

## License

MIT
