diff --git a/hailbytes-vuln-calculator.js b/hailbytes-vuln-calculator.js index 1d2c2aa..dedec17 100644 --- a/hailbytes-vuln-calculator.js +++ b/hailbytes-vuln-calculator.js @@ -550,8 +550,7 @@ const STYLES = ` // ─── Template ───────────────────────────────────────────────────────────────── -const TMPL = document.createElement('template'); -TMPL.innerHTML = ` +const TEMPLATE_HTML = `

🔍 Vulnerability Scanner Infrastructure Calculator HailBytes

Size your scanning infrastructure — estimate VM requirements, cloud costs, and ROI entirely in the browser

@@ -642,9 +641,23 @@ TMPL.innerHTML = `
`; +// 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() { @@ -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 }; diff --git a/test/dom-free.test.mjs b/test/dom-free.test.mjs new file mode 100644 index 0000000..1401c8e --- /dev/null +++ b/test/dom-free.test.mjs @@ -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)); +});