Skip to main content

Command Palette

Search for a command to run...

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

Published
5 min read
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:

  1. It runs entirely client-side: no server roundtrip

  2. Users' data never leaves their browser: privacy win

  3. 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:

  1. Concatenates the shared core files (to reduce HTTP requests in older contexts)

  2. Sets feature flags for FREE vs PRO builds

  3. Copies everything to a dist/ folder

  4. Generates 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

LayerTechnologyWhy
Extension APIManifest V3Required by Chrome
UIVanilla JS + CSSSmall, fast, no build needed
Business LogicPure JS functionsTestable, portable
XLSX ExportSheetJS (vendored)Full Excel support, client-side
ZIP ExportJSZip (vendored)Client-side archive creation
LicensingLemonSqueezy APIHandles payments, validation
BuildCustom Node scriptSimple, auditable
TestingCustom test runnerNo 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.