Skip to content

fix: make module importable in DOM-free runtimes (SSR)#49

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

fix: make module importable in DOM-free runtimes (SSR)#49
dmchaledev wants to merge 1 commit into
mainfrom
claude/sleepy-rubin-h1uz7i

Conversation

@dmchaledev

Copy link
Copy Markdown
Contributor

Problem

The README advertises the pure calculate() export as a "pure DOM-free calculator":

// Or import the pure DOM-free calculator:
import { calculate } from '@hailbytes/vulnerability-calculator';

But importing the module in any non-DOM runtime — Node SSR, Next.js server components, Nuxt, Astro — crashes at load time. Three operations run at module top level and assume a browser:

  1. class HailbytesVulnCalculator extends HTMLElementHTMLElement is undefined off-DOM, so extends undefined throws a TypeError
  2. document.createElement('template') — builds the component markup eagerly
  3. customElements.define(...) — registers the custom element eagerly

Reproduction (before this change), in a plain Node process:

$ node --input-type=module -e "await import('./hailbytes-vuln-calculator.js')"
ReferenceError: document is not defined

The existing tests never caught this because smoke.test.mjs installs a full DOM shim before importing. The documented server-side use case was silently broken.

Fix

Guard all three so evaluating the module never touches DOM globals unless they exist:

  • Base class — fall back to a stub class {} when HTMLElement is absent. The stub is never instantiated off-DOM because registration is guarded.
  • Template — keep the markup as a plain string (TEMPLATE_HTML) and build the <template> lazily on first component construction (browser only).
  • Registration — call customElements.define only when customElements exists and the tag isn't already registered. The !customElements.get(...) guard also fixes a double-registration throw if the module is loaded twice.

No behavior change in the browser — the component builds and registers exactly as before, just lazily. The only observable difference is that import { calculate } now works in a plain Node process, as documented.

Tests

  • All 65 existing tests still pass.
  • Adds test/ssr.test.mjs (2 tests): imports the module in a process with no document / HTMLElement / customElements defined, and exercises calculate() end to end. node:test runs each file in its own process, so no other test's DOM shim leaks in. This test fails against the old code and passes with the fix.
# tests 67
# pass 67
# fail 0

🤖 Generated with Claude Code


Generated by Claude Code

The README advertises `import { calculate }` as a "pure DOM-free
calculator", but importing the module in any non-DOM runtime (Node SSR,
Next.js server components, Nuxt, Astro) crashed at load time because
three operations ran at module top level and assumed a browser:

  1. `class ... extends HTMLElement` — HTMLElement is undefined off-DOM
  2. `document.createElement('template')` for the component markup
  3. `customElements.define(...)`

Guard all three so evaluating the module never touches DOM globals
unless they exist:

  - fall back to a stub base class when HTMLElement is absent
  - keep the template markup as a string and build the <template>
    lazily on first component construction (browser only)
  - register the element only in a browser, and only once (also fixes a
    double-registration throw if the module loads twice)

The pure `calculate()` export now works in a plain Node process with no
DOM shim. Adds test/ssr.test.mjs, which imports the module with no DOM
globals defined and exercises calculate() end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xv8XRbA9DRzE1t8RtJpNAa
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