Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions hailbytes-vuln-calculator.js
Original file line number Diff line number Diff line change
Expand Up @@ -550,8 +550,7 @@ const STYLES = `

// ─── Template ─────────────────────────────────────────────────────────────────

const TMPL = document.createElement('template');
TMPL.innerHTML = `<style>${STYLES}</style>
const TEMPLATE_HTML = `<style>${STYLES}</style>
<div class="header">
<h1><span class="icon">🔍</span> Vulnerability Scanner Infrastructure Calculator <span class="hailbytes-badge">HailBytes</span></h1>
<p>Size your scanning infrastructure — estimate VM requirements, cloud costs, and ROI entirely in the browser</p>
Expand Down Expand Up @@ -642,9 +641,23 @@ TMPL.innerHTML = `<style>${STYLES}</style>
</div>
</div>`;

// Only touch the DOM when it exists. This keeps the module importable in
// DOM-free environments (Node, SSR, Next.js server components) so the pure
// `calculate` export can be used without a browser or DOM shim.
const TMPL = typeof document !== 'undefined'
? document.createElement('template')
: null;
if (TMPL) TMPL.innerHTML = TEMPLATE_HTML;

// ─── Web Component ────────────────────────────────────────────────────────────

class HailbytesVulnCalculator extends HTMLElement {
// Fall back to a plain base class off-DOM so the class declaration itself
// doesn't throw when `HTMLElement` is undefined. The element is only ever
// instantiated by the browser's custom-element machinery, so the DOM methods
// used in the constructor are never reached off-DOM.
const ComponentBase = typeof HTMLElement !== 'undefined' ? HTMLElement : class {};

class HailbytesVulnCalculator extends ComponentBase {
static get observedAttributes() { return ['theme', 'branding']; }

constructor() {
Expand Down Expand Up @@ -856,7 +869,10 @@ class HailbytesVulnCalculator extends HTMLElement {
}
}

customElements.define('hailbytes-vuln-calculator', HailbytesVulnCalculator);
// Register the custom element only in a browser context.
if (typeof customElements !== 'undefined') {
customElements.define('hailbytes-vuln-calculator', HailbytesVulnCalculator);
}

export default HailbytesVulnCalculator;
export { HailbytesVulnCalculator, calculateResources as calculate };
35 changes: 35 additions & 0 deletions test/dom-free.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Regression test: the module must be importable in a DOM-free environment
// (Node, SSR, Next.js server components) so the pure `calculate` export works
// without a browser or a DOM shim. NOTE: this file intentionally does NOT set
// up any `document`/`HTMLElement`/`customElements` globals — `node --test` runs
// each test file in its own process, so importing here exercises the real
// off-DOM code path.

import { test } from 'node:test';
import assert from 'node:assert/strict';

test('module imports without a DOM and exposes calculate()', async () => {
assert.equal(typeof globalThis.document, 'undefined');
assert.equal(typeof globalThis.HTMLElement, 'undefined');
assert.equal(typeof globalThis.customElements, 'undefined');

const mod = await import('../hailbytes-vuln-calculator.js');
assert.equal(typeof mod.calculate, 'function');
});

test('calculate() runs off-DOM and returns a sizing result', async () => {
const { calculate } = await import('../hailbytes-vuln-calculator.js');

const result = calculate({
target_hosts: 1000,
scan_intensity: 'medium',
scan_frequency: 'weekly',
scan_window: 8,
scanning_tools: ['openvas'],
compliance_needs: [],
});

assert.ok(result.vm_resources.cpu_cores >= 2);
assert.ok(result.costs.total_monthly_aws > 0);
assert.ok(Array.isArray(result.recommendations));
});
Loading