Skip to content

fix: make module importable without a DOM (Node/SSR) so calculate() works#17

Open
dmchaledev wants to merge 1 commit into
mainfrom
claude/sleepy-rubin-u4bjq0
Open

fix: make module importable without a DOM (Node/SSR) so calculate() works#17
dmchaledev wants to merge 1 commit into
mainfrom
claude/sleepy-rubin-u4bjq0

Conversation

@dmchaledev

Copy link
Copy Markdown
Contributor

Problem

The package exports a pure calculate() function, documented in index.d.ts as:

Pure infrastructure-sizing calculation — no DOM required.

But the module cannot actually be imported without a DOM. Three things run at module-load time and reference browser globals:

  1. const TMPL = document.createElement('template') (top level) → ReferenceError: document is not defined
  2. class HailbytesVulnCalculator extends HTMLElementHTMLElement is undefined in Node
  3. customElements.define(...) (top level) → customElements is undefined in Node

Reproduced against main:

$ node --input-type=module -e "import { calculate } from './hailbytes-vuln-calculator.js'"
const TMPL = document.createElement('template');
             ^
ReferenceError: document is not defined

Impact

  • The headline DOM-free calculate() API is unusable in Node, scripts, or tests without manually constructing a fake DOM.
  • Server-side rendering crashes at build/render time — Next.js, Astro, Remix, etc. evaluate imported modules on the server, so even a browser-only consumer that just imports the package breaks the build.
  • The existing test suite masks the bug: both smoke.test.mjs and calculate.test.mjs install a full DOM shim on globalThis before importing, so CI never exercises the real-world "no DOM" import path.

This is distinct from the input-robustness work in #14 (which handles omitted inputs); here the module can't even be loaded.

Fix

Defer all DOM access out of module-load time — the web component still behaves identically in the browser:

Change Detail
Lazy <template> Build the <template> on first component construction via getTemplate() instead of at top level.
Base-class fallback extends (typeof HTMLElement !== 'undefined' ? HTMLElement : class {}) so the class declaration doesn't throw in Node.
Guard registration customElements.define(...) only runs when customElements/HTMLElement exist.

No calculation logic changed; the browser code path is unchanged (template is built the first time the element is constructed, before connectedCallback).

Verification

$ node --input-type=module -e "import { calculate } from './hailbytes-vuln-calculator.js'; \
    console.log(calculate({target_hosts:1000,scan_intensity:'medium',scan_frequency:'weekly', \
    scan_window:8,scanning_tools:['hailbytes_asm'],compliance_needs:[]}).vm_resources.cpu_cores)"
6

Regression test

Added a guard to smoke.test.mjs that spawns a fresh, unshimmed Node process to import the module and run calculate() with no DOM globals present. Because it runs in a separate process, the DOM shim used by the other tests can't hide a regression — and it's part of the default npm test (which currently runs only the smoke suite).

$ npm test          # CI default (smoke suite)
# tests 7
# pass 7
# fail 0

$ node --test test/*.test.mjs   # full suite
# tests 66
# pass 66
# fail 0

Self-contained: source + one regression test, no API or behavior changes for existing consumers.

https://claude.ai/code/session_01YGoUBT4BGcWuGqYdVcQRHT


Generated by Claude Code

The module ran document.createElement() at the top level and extended
HTMLElement / called customElements.define() unconditionally, so
importing it threw 'ReferenceError: document is not defined' in any
non-browser environment. This broke the exported calculate() function —
documented as 'no DOM required' — for Node consumers and crashed
server-side rendering (Next.js/Astro/etc.) at build time.

- Build the <template> lazily on first component construction instead of
  at module load.
- Extend a plain base class when HTMLElement is undefined.
- Guard customElements.define() behind a browser-environment check.

Adds a regression test that spawns a fresh, unshimmed Node process to
verify the module imports and calculate() runs without any DOM globals.
The existing DOM-shimmed tests masked this, so the guard runs in a
separate process and is included in the default npm test run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants