The Tech Stack Behind HTML Table Exporter: No Framework, No Build Step (Almost)

When I started building HTML Table Exporter, I had a decision to make: React/Vue/Svelte, or vanilla JavaScript?
I chose vanilla. No framework. No bundler in development. Just JavaScript files that run directly in the browser.
Here's the full stack and why each choice made sense for a browser extension.
The Architecture Overview
HTML Table Exporter
├── Manifest V3 (Chrome Extension API)
├── Content Script (table detection, DOM interaction)
├── Popup UI (vanilla JS + CSS)
├── Background Service Worker (minimal, for messaging)
└── Shared Core (pure functions, testable)
No React. No Webpack in development. No node_modules with 847 dependencies.
Why Vanilla JavaScript?
1. Browser Extensions Are Already Sandboxed
React's value proposition includes component isolation and state management. But Chrome extensions already have natural boundaries:
Content script: runs in the page context
Popup: separate window, separate DOM
Background: service worker, no DOM at all
Each piece is already isolated. I don't need a framework to manage boundaries that the browser enforces.
2. Bundle Size Matters
Every KB in a Chrome extension affects:
Install size (users see this in the store)
Load time (popup should open instantly)
Review time (Google reviews the code)
React + ReactDOM minified is ~40KB. For a popup that shows a table list and some buttons, that's overhead I don't need.
3. The UI Isn't That Complex
The popup has:
A list of detected tables
Export buttons per table
A format dropdown
Some settings toggles
This is a form with buttons. Vanilla JS handles this fine with querySelector, event listeners, and template literals for dynamic HTML.
The Shared Core: Pure Functions for Business Logic
The interesting architectural decision was creating a "shared core", pure JavaScript functions with no DOM dependencies:
// tableExtraction.js
function extractTableMatrix(table) {
const rows = Array.from(table.rows);
const grid = [];
rows.forEach((rowEl, rowIndex) => {
// Handle rowspan/colspan
// Build normalized grid
});
return grid; // 2D array of strings
}
// cleaningPresets.js
function normalizeNumber(value, locale) {
// Parse "1.234,56" or "1,234.56" → 1234.56
}
// exporters.js
function toCSV(matrix, delimiter) {
// Convert 2D array to CSV string
}
These functions:
Take data in, return data out
Have no side effects
Work in Node.js for testing
Work in the browser for production
This separation is the architectural win. The UI is simple vanilla JS. The logic is testable pure functions.
Client-Side XLSX Generation
One non-obvious challenge: generating Excel files in the browser.
I use SheetJS (xlsx) for this. The minified library is ~200KB, which is significant, but:
It runs entirely client-side: no server roundtrip
Users' data never leaves their browser: privacy win
It handles the full Excel format correctly: cell types, column widths, etc.
The integration:
import * as XLSX from './xlsx.full.min.js';
function exportToXLSX(matrix, filename) {
const worksheet = XLSX.utils.aoa_to_sheet(matrix);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Table");
const xlsxData = XLSX.write(workbook, {
bookType: 'xlsx',
type: 'array'
});
downloadBlob(xlsxData, filename, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
}
ZIP Generation for Bulk Export
When users export all tables from a page, they get a ZIP file. For this, I use JSZip:
import JSZip from './jszip.min.js';
async function exportAllTables(tables, format) {
const zip = new JSZip();
tables.forEach((table, i) => {
const content = convertTable(table.matrix, format);
const filename = `table-${i + 1}.${format}`;
zip.file(filename, content);
});
const blob = await zip.generateAsync({ type: 'blob' });
downloadBlob(blob, 'tables.zip', 'application/zip');
}
Both SheetJS and JSZip are vendored (committed to the repo) rather than npm-installed. This keeps the dependency tree flat and auditable.
The Build Process (Minimal)
For development, there's no build step. I edit JS files and reload the extension.
For production, I have a single Node.js script that:
Concatenates the shared core files (to reduce HTTP requests in older contexts)
Sets feature flags for FREE vs PRO builds
Copies everything to a
dist/folderGenerates the ZIP for Chrome Web Store upload
// build.js (simplified)
const FREE_BUILD = process.argv.includes('--production');
let content = fs.readFileSync('featureFlags.js', 'utf8');
content = content.replace(
'const _hte_cfg_0x1 = true',
`const _hte_cfg_0x1 = ${!FREE_BUILD}`
);
fs.writeFileSync('dist/featureFlags.js', content);
The entire build script is ~300 lines. No Webpack config. No Babel. No transpilation.
Testing Without a Framework
The shared core runs in Node.js, so testing is straightforward:
// tableExtraction_test.js
function runTableExtractionTests() {
let passed = 0, failed = 0;
// Test: Basic table
const html = '<table><tr><td>A</td><td>B</td></tr></table>';
const result = extractTableMatrix(parseHTML(html));
if (result[0][0] === 'A' && result[0][1] === 'B') {
passed++;
} else {
failed++;
console.log('FAIL: Basic table extraction');
}
return { passed, failed, total: passed + failed };
}
No Jest. No Mocha. Just functions that return pass/fail counts.
I have ~60 tests across 4 test suites:
Table extraction (rowspan, colspan, nested tables, Wikipedia edge cases)
Cleaning presets (number normalization, date parsing)
Exporters (CSV escaping, JSON structure, SQL syntax)
Review prompt system (timing logic)
Running node runAllTests.js executes everything in ~200ms.
Would I Use a Framework Next Time?
For a browser extension popup? Probably not. The constraints (small UI, fast load, isolated context) favor vanilla JS.
For a complex web app? Yes, frameworks provide real value for large teams and complex state.
The takeaway isn't "frameworks bad." It's "match the tool to the problem." A Chrome extension popup isn't a React problem.
The Full Stack Summary
| Layer | Technology | Why |
| Extension API | Manifest V3 | Required by Chrome |
| UI | Vanilla JS + CSS | Small, fast, no build needed |
| Business Logic | Pure JS functions | Testable, portable |
| XLSX Export | SheetJS (vendored) | Full Excel support, client-side |
| ZIP Export | JSZip (vendored) | Client-side archive creation |
| Licensing | LemonSqueezy API | Handles payments, validation |
| Build | Custom Node script | Simple, auditable |
| Testing | Custom test runner | No dependencies, fast |
Total external dependencies in production: 2 (SheetJS, JSZip).
Sometimes the boring stack is the right stack.
HTML Table Exporter is available at gauchogrid.com. Try it free on the Chrome Web Store. Built with vanilla JS and zero regrets.




