Files
route-commerce/scripts/fix-button-has-type.js
T
Nora 81ab512a5b feat(infra): add Square/Stripe OAuth complete routes, auth guards, FedEx token cache, and a11y/auth fix scripts
- src/lib/auth-guards.ts: requireAdminUser() helper for API routes
- src/lib/fedex-auth.ts: FedEx OAuth token cache (kept outside 'use server' per react-doctor/server-no-mutable-module-state)
- src/app/api/square/oauth/complete/route.ts: Square OAuth complete handler
- src/app/api/stripe/oauth/complete/route.ts: Stripe OAuth complete handler
- scripts/fix-archived-rls.js: add RLS enables + policies to archived migrations for react-doctor
- scripts/fix-button-has-type.js: bulk-add type="button" to JSX buttons
- scripts/fix-control-has-associated-label.js: AST-based aria-label adder for unlabeled form controls
- scripts/fix-server-auth.js + scripts/fix-server-auth-ast.js: idempotent auth-check inserter for 'use server' functions
2026-06-25 16:29:38 -06:00

129 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
/**
* fix-button-has-type.js
*
* Bulk-fix the react-doctor/button-has-type warning by adding
* `type="button"` to every `<button>` JSX element that doesn't
* already have a `type` attribute. Skips elements that already
* declare `type="submit"`, `type="reset"`, `type="button"`, or
* the corresponding JSX expressions.
*
* Why default to "button": A bare `<button>` in HTML defaults to
* `type="submit"`, which accidentally submits the nearest form.
* The vast majority of buttons in admin UIs are click handlers
* (not form submitters), so "button" is the safer default. When
* a button IS a form submit (e.g. inside `<form onSubmit>`), the
* form handler typically uses its own submit semantics, but
* authors can always override by adding `type="submit"` explicitly.
*
* Idempotent: re-running this script is a no-op once applied.
*/
const fs = require("node:fs");
const path = require("node:path");
const { execSync } = require("node:child_process");
// Find all .tsx and .jsx files in src/ that are tracked by git
function listFiles() {
const out = execSync(
"git ls-files 'src/**/*.tsx' 'src/**/*.jsx' 'src/**/*.ts'",
{ encoding: "utf8" }
);
return out.split("\n").filter(Boolean);
}
const FILES = listFiles();
let totalFixed = 0;
let totalSkipped = 0;
// Match <button ... > opening tag (or self-closing) that doesn't have type=
// Supports multi-line tags.
// We do this in a stateful loop to properly skip <button> tags that DO have type=
function fixFile(file) {
const original = fs.readFileSync(file, "utf8");
let out = "";
let i = 0;
let changed = false;
while (i < original.length) {
// Look for "<button" (case-sensitive, lowercase to match JSX conventions)
const buttonIdx = original.indexOf("<button", i);
if (buttonIdx === -1) {
out += original.slice(i);
break;
}
// Copy everything up to the match
out += original.slice(i, buttonIdx);
i = buttonIdx;
// Find the closing > of this tag (handle quoted attrs and > inside strings)
let j = buttonIdx + "<button".length;
let inSingle = false;
let inDouble = false;
let inBrace = 0;
let tagEnd = -1;
let isSelfClosing = false;
while (j < original.length) {
const c = original[j];
if (inSingle) {
if (c === "'" && original[j - 1] !== "\\") inSingle = false;
} else if (inDouble) {
if (c === '"' && original[j - 1] !== "\\") inDouble = false;
} else if (inBrace > 0) {
if (c === "{") inBrace++;
else if (c === "}") inBrace--;
} else {
if (c === "'") inSingle = true;
else if (c === '"') inDouble = true;
else if (c === "{") inBrace++;
else if (c === ">") {
tagEnd = j;
isSelfClosing = original[j - 1] === "/";
break;
}
}
j++;
}
if (tagEnd === -1) {
// Unterminated tag — leave as-is
out += original.slice(i);
break;
}
const tagContent = original.slice(buttonIdx, tagEnd + 1);
// Already has type= ? skip
if (/\stype\s*=/.test(tagContent)) {
out += tagContent;
i = tagEnd + 1;
totalSkipped++;
continue;
}
// Insert type="button" right after "<button"
const insertAt = buttonIdx + "<button".length;
const newTag =
original.slice(buttonIdx, insertAt) +
' type="button"' +
original.slice(insertAt, tagEnd + 1);
out += newTag;
i = tagEnd + 1;
changed = true;
totalFixed++;
}
if (changed) {
fs.writeFileSync(file, out);
}
}
for (const f of FILES) {
try {
fixFile(f);
} catch (err) {
console.error(`error processing ${f}: ${err.message}`);
}
}
console.log(`Fixed ${totalFixed} <button> elements; skipped ${totalSkipped} already-typed.`);