0ac4beaaa8
- Add requireAuth() to admin-permissions.ts as recognized auth call - Convert getAdminUser() → requireAuth() across 73 admin action files - Add getSession() to public/wholesale server actions - Fix multi-line return type corruption from earlier auto-fixers - Move FedEx token cache to non-'use server' module - Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD, EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS - Update Stripe API version 2026-05-27 → 2026-06-24 - Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient - Fix 51 TypeScript errors (return type corruption, missing imports)
113 lines
3.5 KiB
JavaScript
113 lines
3.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Replace <a href="/internal"> with <Link href="/internal"> in JSX.
|
|
* Skips <a> tags that already opt out of navigation (target, download,
|
|
* rel, mailto, tel, external, hash-only) and any non-string-literal
|
|
* href expressions.
|
|
*
|
|
* Idempotent: re-running on an already-converted file is a no-op.
|
|
*
|
|
* Mirrors the react-doctor rule
|
|
* `react-doctor/nextjs-no-a-element`.
|
|
*/
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const ts = require("typescript");
|
|
|
|
function findInternalAnchorOpeningElements(src) {
|
|
const sf = ts.createSourceFile("x.tsx", src, ts.ScriptTarget.Latest, true);
|
|
/** @type {Array<{pos:number,end:number,href:string}>} */
|
|
const matches = [];
|
|
function visit(node) {
|
|
if (
|
|
ts.isJsxOpeningElement(node) &&
|
|
ts.isIdentifier(node.tagName) &&
|
|
node.tagName.text === "a"
|
|
) {
|
|
let hrefValue = null;
|
|
let isInternalLiteral = false;
|
|
let isOptOut = false;
|
|
for (const attr of node.attributes.properties) {
|
|
if (!ts.isJsxAttribute(attr) || !attr.name) continue;
|
|
const name = ts.isIdentifier(attr.name) ? attr.name.text : attr.name.text;
|
|
if (name === "href" && attr.initializer && ts.isStringLiteral(attr.initializer)) {
|
|
hrefValue = attr.initializer.text;
|
|
if (hrefValue.startsWith("/")) {
|
|
isInternalLiteral = true;
|
|
}
|
|
}
|
|
if (name === "target" || name === "download" || name === "rel" || name === "onClick") {
|
|
isOptOut = true;
|
|
}
|
|
}
|
|
if (isInternalLiteral && !isOptOut) {
|
|
matches.push({
|
|
pos: node.getStart(sf),
|
|
end: node.getEnd(),
|
|
href: hrefValue,
|
|
});
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
visit(sf);
|
|
return matches;
|
|
}
|
|
|
|
function ensureLinkImport(src) {
|
|
if (/from\s+["']next\/link["']/.test(src)) return src;
|
|
// Insert after the last existing import line.
|
|
const sf = ts.createSourceFile("x.tsx", src, ts.ScriptTarget.Latest, true);
|
|
let lastImportEnd = 0;
|
|
for (const stmt of sf.statements) {
|
|
if (ts.isImportDeclaration(stmt)) {
|
|
lastImportEnd = Math.max(lastImportEnd, stmt.getEnd());
|
|
}
|
|
}
|
|
if (lastImportEnd === 0) {
|
|
return `import Link from "next/link";\n${src}`;
|
|
}
|
|
return `${src.slice(0, lastImportEnd)}\nimport Link from "next/link";${src.slice(lastImportEnd)}`;
|
|
}
|
|
|
|
function processFile(filePath) {
|
|
const abs = path.resolve(filePath);
|
|
if (!fs.existsSync(abs)) {
|
|
console.error(`skip (not found): ${abs}`);
|
|
return 0;
|
|
}
|
|
const original = fs.readFileSync(abs, "utf8");
|
|
const matches = findInternalAnchorOpeningElements(original);
|
|
if (matches.length === 0) return 0;
|
|
|
|
let out = original;
|
|
// Apply edits in reverse so positions don't shift.
|
|
matches.sort((a, b) => b.pos - a.pos);
|
|
for (const m of matches) {
|
|
out = out.slice(0, m.pos) + `<Link href="${m.href}"` + out.slice(m.end);
|
|
}
|
|
// Replace any closing </a> with </Link> in the same file. Safer
|
|
// to do this as a single global swap after the opening-tag edits
|
|
// since the file is the scope.
|
|
out = out.replace(/<\/a>/g, "</Link>");
|
|
|
|
out = ensureLinkImport(out);
|
|
if (out !== original) {
|
|
fs.writeFileSync(abs, out, "utf8");
|
|
console.log(`patched (${matches.length} anchors): ${abs}`);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
const files = process.argv.slice(2);
|
|
if (files.length === 0) {
|
|
console.error("usage: node scripts/fix-next-anchor.js <file>...");
|
|
process.exit(2);
|
|
}
|
|
let n = 0;
|
|
for (const f of files) {
|
|
n += processFile(f);
|
|
}
|
|
console.log(`done — ${n} files patched`);
|