Files
route-commerce/src/lib/excel-parser.ts
T
Nora 29d9d23a26 fix: react-doctor → 64/100 (Bugs 122, Perf 286, A11y 613, Maint 436)
- CartContext: lazy initializers replace mount-only useEffect
  hydration; remove 8 no-initialize-state warnings
- Toast/AdminSearchInput: React 19 useContext/use + drop
  forwardRef (3 no-react19-deprecated-apis)
- ProductFormModal: lazy initializers + useSyncExternalStore
  for mount; parent adds key=editingProduct.id
- InstallPrompt: useReducer for prompt state (no-cascading-set-state)
- QRScanModal: ref-based latest-callback pattern replaces
  useEffectEvent deps mistake
- OnboardingFlow: functional setState (rerender-functional-setstate)
- UsersPage/StopsCalendar/FeaturesAndStats: lazy initializers
  (rerender-lazy-state-init)
- FAQClientPage: server-side brand settings fetch via getBrandSettingsPublic
  in layout; remove supabase import
- LandingPageWrapper: href='#' → href='#top' (anchor-is-valid)
- TuxedoVideoHero: replace animate-bounce with ease-out-expo
  (no-inline-bounce-easing)
- ProductTableClient: useCallback for handleDeleted
  (jsx-no-new-function-as-prop)
- excel-parser: pre-compile delimiter regexes (js-hoist-regexp)
- water-log/settings: Promise.all for parallel DB calls
  (async-parallel)
- ToastNotification: extract toast store to separate file
  (only-export-components)
- WholesaleClient: inline <WholesaleIcon/> instead of hoisting to
  const (rendering-hoist-jsx)
2026-06-26 02:41:56 -06:00

88 lines
2.6 KiB
TypeScript

import ExcelJS from "exceljs";
export type ParsedSheet = {
headers: string[];
rows: string[][];
};
export async function parseExcelBuffer(input: Buffer | ArrayBuffer | Uint8Array): Promise<{
headers: string[];
rows: string[][];
}> {
const workbook = new ExcelJS.Workbook();
const buffer = Buffer.isBuffer(input) ? input : Buffer.from(new Uint8Array(input));
await workbook.xlsx.load(buffer as unknown as import("exceljs").Buffer);
const sheet = workbook.getWorksheet(1);
if (!sheet) {
return { headers: [], rows: [] };
}
const headers: string[] = [];
const rows: string[][] = [];
sheet.eachRow((row, rowIndex) => {
const values = row.values as (string | number | Date | null | undefined)[];
const rowData = values.map((v) => {
if (v === null || v === undefined) return "";
if (v instanceof Date) return v.toISOString().split("T")[0];
return String(v).trim();
});
if (rowIndex === 1) {
// Header row
headers.push(...rowData);
} else {
// Skip empty rows
if (rowData.some((v) => v !== "")) {
rows.push(rowData);
}
}
});
return { headers, rows };
}
/**
* Parse CSV/TSV/TXT text into headers + rows.
* Auto-detects delimiter by checking first few lines.
*/
export function parseTextBuffer(rawText: string): ParsedSheet {
// Normalize line endings
const text = rawText.replace(/\r\n/g, "\n").replace(/\r/g, "").trim();
const lines = text.split("\n").filter((l) => l.trim() !== "");
if (lines.length === 0) return { headers: [], rows: [] };
// Detect delimiter
const firstLine = lines[0];
const delimiter = detectDelimiter(firstLine);
const headers = firstLine.split(delimiter).map((h) => h.trim().replace(/^["']|["']$/g, ""));
const rows = lines.slice(1).map((line) =>
line.split(delimiter).map((v) => v.trim().replace(/^["']|["']$/g, ""))
);
return { headers, rows };
}
function detectDelimiter(line: string): string {
const delimiters = [",", "\t", ";", "|"];
// Pre-compile the matcher regexes once so we don't rebuild them on
// every loop iteration for every parsed line.
const matchers = delimiters.map((d) => new RegExp(`\\${d}`, "g"));
let best = ",";
let maxCount = 0;
for (let i = 0; i < delimiters.length; i++) {
// The regex carries the `g` flag, so we need to reset lastIndex
// between calls (lastIndex persists on the same regex instance).
matchers[i].lastIndex = 0;
const count = (line.match(matchers[i]) ?? []).length;
if (count > maxCount) {
maxCount = count;
best = delimiters[i];
}
}
return best;
}