Fix happy-dom overwriting NodeJS process variable (#6384)

* added safe window and parser functions to avoid overwriting global process

* changeset

* add correct typing for doc

* create wrapper function to restore node internals

* Update packages/html/src/createSafeWindow.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/html/src/createSafeParser.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/html/src/preserveAndRestoreNodeInternals.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* make window and dom parser local variables and avoid variable shadowing

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
bdbch 2025-05-26 10:29:33 +02:00 committed by GitHub
parent a84ec2a08e
commit 4f498944b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 82 additions and 8 deletions

View File

@ -0,0 +1,5 @@
---
'@tiptap/html': patch
---
Wrap happy-dom Window and DOMParser creation to avoid setting global process to null. See https://github.com/ueberdosis/tiptap/issues/6368

View File

@ -0,0 +1,18 @@
import type { DOMParser as HappyDOMParser, Window as HappyDOMWindow } from 'happy-dom-without-node'
import { preserveAndRestoreNodeInternals } from './preserveAndRestoreNodeInternals.js'
/**
* Creates a safe DOMParser instance by wrapping `happy-dom`'s `DOMParser`.
* This function ensures that the original `process` is preserved by using
* `preserveAndRestoreNodeInternals`.
*
* @param {HappyDOMWindow} window - The `happy-dom` window object to use for the parser.
* @returns {HappyDOMParser} A new instance of `happy-dom`'s `DOMParser`.
*/
export function createSafeParser(window: HappyDOMWindow) {
return preserveAndRestoreNodeInternals(() => {
const { DOMParser } = require('happy-dom-without-node')
return new DOMParser(window) as HappyDOMParser
})
}

View File

@ -0,0 +1,17 @@
import type { Window as HappyDOMWindow } from 'happy-dom-without-node'
import { preserveAndRestoreNodeInternals } from './preserveAndRestoreNodeInternals.js'
/**
* Creates a new `Window` instance using `happy-dom` and ensures that the original
* `process` object is restored after the operation. This function wraps the
* creation of the `Window` object to provide a safe environment for DOM manipulation.
*
* @returns {HappyDOMWindow} A new `Window` instance from `happy-dom`.
*/
export function createSafeWindow() {
return preserveAndRestoreNodeInternals(() => {
const { Window } = require('happy-dom-without-node')
return new Window() as HappyDOMWindow
})
}

View File

@ -2,7 +2,10 @@ import type { Extensions } from '@tiptap/core'
import { getSchema } from '@tiptap/core'
import type { ParseOptions } from '@tiptap/pm/model'
import { DOMParser } from '@tiptap/pm/model'
import { DOMParser as HappyDOMParser, Window as HappyDOMWindow } from 'happy-dom-without-node'
import type { Document as HappyDOMDocument } from 'happy-dom-without-node'
import { createSafeParser } from './createSafeParser.js'
import { createSafeWindow } from './createSafeWindow.js'
/**
* Generates a JSON object from the given HTML string and converts it into a Prosemirror node with content.
@ -18,11 +21,22 @@ import { DOMParser as HappyDOMParser, Window as HappyDOMWindow } from 'happy-dom
*/
export function generateJSON(html: string, extensions: Extensions, options?: ParseOptions): Record<string, any> {
const schema = getSchema(extensions)
let doc: Document | HappyDOMDocument | null = null
const parseInstance =
typeof window !== 'undefined' ? new window.DOMParser() : new HappyDOMParser(new HappyDOMWindow())
if (typeof window === 'undefined') {
const localWindow = createSafeWindow()
const localDOMParser = createSafeParser(localWindow)
doc = localDOMParser.parseFromString(html, 'text/html')
} else {
doc = new window.DOMParser().parseFromString(html, 'text/html')
}
if (!doc) {
throw new Error('Failed to parse HTML string')
}
return DOMParser.fromSchema(schema)
.parse(parseInstance.parseFromString(html, 'text/html').body as Node, options)
.parse(doc.body as Node, options)
.toJSON()
}

View File

@ -1,6 +1,7 @@
import type { Node, Schema } from '@tiptap/pm/model'
import { DOMSerializer } from '@tiptap/pm/model'
import { Window } from 'happy-dom-without-node'
import { createSafeWindow } from './createSafeWindow.js'
/**
* Returns the HTML string representation of a given document node.
@ -25,13 +26,13 @@ export function getHTMLFromFragment(doc: Node, schema: Schema, options?: { docum
}
// Use happy-dom for serialization.
const browserWindow = typeof window === 'undefined' ? new Window() : window
const localWindow = typeof window === 'undefined' ? createSafeWindow() : window
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(doc.content, {
document: browserWindow.document as unknown as Document,
document: localWindow.document as unknown as Document,
})
const serializer = new browserWindow.XMLSerializer()
const serializer = new localWindow.XMLSerializer()
return serializer.serializeToString(fragment as any)
}

View File

@ -0,0 +1,19 @@
/**
* Preserves and restores node internals like the global process object.
* @param operation - The operation to perform while preserving the node internals.
* @returns The result of the operation.
*/
export function preserveAndRestoreNodeInternals<T>(operation: () => T): T {
// Store the original process object
// see https://github.com/ueberdosis/tiptap/issues/6368
// eslint-disable-next-line
const originalProcess = globalThis.process
try {
return operation()
} finally {
// Restore the original process object
// eslint-disable-next-line
globalThis.process = originalProcess
}
}