Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reduce node specific dependencies #1158

Closed
wants to merge 7 commits into from
7 changes: 3 additions & 4 deletions packages/happy-dom/src/console/VirtualConsole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import IVirtualConsolePrinter from './types/IVirtualConsolePrinter.js';
import VirtualConsoleLogLevelEnum from './enums/VirtualConsoleLogLevelEnum.js';
import VirtualConsoleLogTypeEnum from './enums/VirtualConsoleLogTypeEnum.js';
import IVirtualConsoleLogGroup from './types/IVirtualConsoleLogGroup.js';
import * as PerfHooks from 'perf_hooks';
import { ConsoleConstructor } from 'console';

/**
Expand Down Expand Up @@ -276,7 +275,7 @@ export default class VirtualConsole implements Console {
* @param [label=default] Label.
*/
public time(label = 'default'): void {
this._time[label] = PerfHooks.performance.now();
this._time[label] = performance.now();
}

/**
Expand All @@ -288,7 +287,7 @@ export default class VirtualConsole implements Console {
public timeEnd(label = 'default'): void {
const time = this._time[label];
if (time) {
const duration = PerfHooks.performance.now() - time;
const duration = performance.now() - time;
this._printer.print({
type: VirtualConsoleLogTypeEnum.timeEnd,
level: VirtualConsoleLogLevelEnum.info,
Expand All @@ -308,7 +307,7 @@ export default class VirtualConsole implements Console {
public timeLog(label = 'default', ...args: Array<object | string>): void {
const time = this._time[label];
if (time) {
const duration = PerfHooks.performance.now() - time;
const duration = performance.now() - time;
this._printer.print({
type: VirtualConsoleLogTypeEnum.timeLog,
level: VirtualConsoleLogLevelEnum.info,
Expand Down
1 change: 0 additions & 1 deletion packages/happy-dom/src/event/Event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import IWindow from '../window/IWindow.js';
import IShadowRoot from '../nodes/shadow-root/IShadowRoot.js';
import IEventTarget from './IEventTarget.js';
import NodeTypeEnum from '../nodes/node/NodeTypeEnum.js';
import { performance } from 'perf_hooks';
import EventPhaseEnum from './EventPhaseEnum.js';
import IDocument from '../nodes/document/IDocument.js';

Expand Down
1 change: 0 additions & 1 deletion packages/happy-dom/src/fetch/Request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import FetchBodyUtility from './utilities/FetchBodyUtility.js';
import AbortSignal from './AbortSignal.js';
import Stream from 'stream';
import Blob from '../file/Blob.js';
import { TextDecoder } from 'util';
import FetchRequestValidationUtility from './utilities/FetchRequestValidationUtility.js';
import IRequestReferrerPolicy from './types/IRequestReferrerPolicy.js';
import IRequestRedirect from './types/IRequestRedirect.js';
Expand Down
1 change: 0 additions & 1 deletion packages/happy-dom/src/fetch/Response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import FormData from '../form-data/FormData.js';
import FetchBodyUtility from './utilities/FetchBodyUtility.js';
import DOMException from '../exception/DOMException.js';
import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js';
import { TextDecoder } from 'util';
import MultipartFormDataParser from './multipart/MultipartFormDataParser.js';

const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import URL from '../../url/URL.js';
import IRequest from '../types/IRequest.js';
import IDocument from '../../nodes/document/IDocument.js';
import { isIP } from 'net';
import Headers from '../Headers.js';
import IRequestReferrerPolicy from '../types/IRequestReferrerPolicy.js';

Expand Down Expand Up @@ -207,13 +206,32 @@ export default class FetchRequestReferrerUtility {

// 4. If origin's host component matches one of the CIDR notations 127.0.0.0/8 or ::1/128 [RFC4632], return "Potentially Trustworthy".
const hostIp = url.host.replace(/(^\[)|(]$)/g, '');
const hostIPVersion = isIP(hostIp);

if (hostIPVersion === 4 && /^127\./.test(hostIp)) {
// IPv4 addr test pattern
const v4Seg = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])';
const v4Str = `(?:${v4Seg}\\.){3}${v4Seg}`;
const IPv4Reg = new RegExp(`^${v4Str}$`);

// IPv6 addr test pattern
const v6Seg = '(?:[0-9a-fA-F]{1,4})';
const IPv6Reg = new RegExp(
'^(?:' +
`(?:${v6Seg}:){7}(?:${v6Seg}|:)|` +
`(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` +
`(?:${v6Seg}:){5}(?::${v4Str}|(?::${v6Seg}){1,2}|:)|` +
`(?:${v6Seg}:){4}(?:(?::${v6Seg}){0,1}:${v4Str}|(?::${v6Seg}){1,3}|:)|` +
`(?:${v6Seg}:){3}(?:(?::${v6Seg}){0,2}:${v4Str}|(?::${v6Seg}){1,4}|:)|` +
`(?:${v6Seg}:){2}(?:(?::${v6Seg}){0,3}:${v4Str}|(?::${v6Seg}){1,5}|:)|` +
`(?:${v6Seg}:){1}(?:(?::${v6Seg}){0,4}:${v4Str}|(?::${v6Seg}){1,6}|:)|` +
`(?::(?:(?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` +
')(?:%[0-9a-zA-Z-.:]{1,})?$'
);

if (IPv4Reg.test(hostIp) && /^127\./.test(hostIp)) {
return true;
}

if (hostIPVersion === 6 && /^(((0+:){7})|(::(0+:){0,6}))0*1$/.test(hostIp)) {
if (IPv6Reg.test(hostIp) && /^(((0+:){7})|(::(0+:){0,6}))0*1$/.test(hostIp)) {
return true;
}

Expand Down
10 changes: 8 additions & 2 deletions packages/happy-dom/src/window/IWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ import IHappyDOMSettings from './IHappyDOMSettings.js';
import RequestInfo from '../fetch/types/IRequestInfo.js';
import FileList from '../nodes/html-input-element/FileList.js';
import Stream from 'stream';
import { webcrypto } from 'crypto';
import FormData from '../form-data/FormData.js';
import AbortController from '../fetch/AbortController.js';
import AbortSignal from '../fetch/AbortSignal.js';
Expand All @@ -133,6 +132,13 @@ import Clipboard from '../clipboard/Clipboard.js';
import ClipboardItem from '../clipboard/ClipboardItem.js';
import ClipboardEvent from '../event/events/ClipboardEvent.js';

if (
!('crypto' in globalThis) ||
Object.getPrototypeOf(globalThis.crypto) === Object.getPrototypeOf({})
) {
globalThis['crypto'] = import('crypto').then((c) => c.webcrypto);
}

Copy link

@shirakaba shirakaba Dec 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid dynamic imports as it creates new problems.

Also in any case, we'd need to await this Promise, as right now the value of globalThis.crypto will be Promise {status: "resolved", result: ()} (i.e. a resolved Promise wrapping the value of webcrypto).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this feedback!
I'll look for a better replacement...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's now solved in a slightly modified manner:

async function webcryptoImportFallback() {
	if (
		!('crypto' in globalThis) ||
		Object.getPrototypeOf(globalThis.crypto) === Object.getPrototypeOf({})
	) {
		// Import required only on node < 19
		globalThis['crypto'] = (await import('crypto')).webcrypto;
	}
}
webcryptoImportFallback();

A rather verbose variant to work around the prohibited top-level-await but still using dynamic import.
But the import should be only required for node < 19. In this particular case it will just have the same effect as in the original source code. But on most JS runtimes (more actual node releases / deno / browsers) webcrypto is already accessible as globalThis.crypto without any explicit import.

We could avoid the dynamic import fallback by always importing * from 'crypto' and handle the conditional redirection later if needed, but it again wouldn't work with commonly used polyfill solutions out of the box.

Copy link

@shirakaba shirakaba Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed for Node, it'll only be a problem on older environments, but even then, I'm not sure about this workaround. This will queue a microtask to polyfill globalThis.cypto, but as the surrounding code is synchronous, something could still try to access that property before it's ready.

To avoid this weak point, I think ideally polyfilling crypto should be a responsibility of the user rather than the library, so that they can safely coordinate the polyfill flow (if needed at all) in their userland code.

  • If using a bundler: use resolve.alias (or similar) to alias crypto to a local polyfill of webcrypto.
  • If not using a bundler: in package.json, alias the Node SDK dependency to a local polyfill of webcrypto:
      // ...
      "dependencies": {
        "crypto": "file:./lib/web-crypto-polyfill@^1.0.0",
      }
    }
    ... where web-crypto-polyfill would be an npm package that pretty much just does this in its entrypoint JS file:
    import { webcrypto } from 'crypto';
    globalThis.crypto = webcrypto;

Copy link
Contributor Author

@mash-graz mash-graz Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really appreciate your critical review of the suggested code changes, but honestly I can't agree with your point of view in some significant aspects.

The webcrypto functions are in fact not needed by any internal processing in happy-dom. They are just reexported for WebAPI completeness as requested by #1050.

Right now it's handling this job in a strict node-centric manner, which unfortunately isn't compatible with other JS runtimes.

For example in deno the node:crypt module isn't available at all and any import attempt throws an error. And in many common tools for automatic polyfilling node specific interfaces, like the vite-plugin-node-polyfills used in my particular case, crypto imports just get shimmed by empty objects. The documentation of these helpers often explicitly advice to avoid the incomplete dummy replacement in this particular case (for example: here).

As a consequence of this lack of support under many circumstances, we are just shifting the issue from one level to the next one as long as we don't try to avoid this node specific imports wherever possible resp. replace them by alternative solutions and dynamic imports, which will be only activated in case of actually running on older node environments.

If we can't figure out a more satisfying and user-friendly solution, I would even suggest removing the crypto reexport again, which would at least eliminate the need of additional polyfills as long as this interface isn't actually required by an application.

Nevertheless, I think, my suggested workaround could be seen as an acceptable compromise to minimize these compatibility issues in a more desirable manner.

But if you see any further improvements to process the async dynamic import in more proper fashion (e.g., in regard to the type definition in IWindow), I would be really happy to listen to your advice.

/**
* Browser window.
*/
Expand Down Expand Up @@ -414,7 +420,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal {
readonly pageYOffset: number;
readonly scrollX: number;
readonly scrollY: number;
readonly crypto: typeof webcrypto;
readonly crypto: typeof globalThis.crypto;

/**
* Returns an object containing the values of all CSS properties of an element.
Expand Down
20 changes: 15 additions & 5 deletions packages/happy-dom/src/window/Window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,8 @@ import Fetch from '../fetch/Fetch.js';
import RangeImplementation from '../range/Range.js';
import DOMRect from '../nodes/element/DOMRect.js';
import VMGlobalPropertyScript from './VMGlobalPropertyScript.js';
import * as PerfHooks from 'perf_hooks';
import VM from 'vm';
import { Buffer } from 'buffer';
import { webcrypto } from 'crypto';
import XMLHttpRequestImplementation from '../xml-http-request/XMLHttpRequest.js';
import XMLHttpRequestUpload from '../xml-http-request/XMLHttpRequestUpload.js';
import XMLHttpRequestEventTarget from '../xml-http-request/XMLHttpRequestEventTarget.js';
Expand Down Expand Up @@ -147,6 +145,13 @@ import Clipboard from '../clipboard/Clipboard.js';
import ClipboardItem from '../clipboard/ClipboardItem.js';
import ClipboardEvent from '../event/events/ClipboardEvent.js';

if (
!('crypto' in globalThis) ||
Object.getPrototypeOf(globalThis.crypto) === Object.getPrototypeOf({})
) {
globalThis['crypto'] = import('crypto').then((c) => c.webcrypto);
}

const ORIGINAL_SET_TIMEOUT = setTimeout;
const ORIGINAL_CLEAR_TIMEOUT = clearTimeout;
const ORIGINAL_SET_INTERVAL = setInterval;
Expand Down Expand Up @@ -219,7 +224,12 @@ export default class Window extends EventTarget implements IWindow {
enableFileSystemHttpRequests: false,
navigator: {
userAgent: `Mozilla/5.0 (X11; ${
process.platform.charAt(0).toUpperCase() + process.platform.slice(1) + ' ' + process.arch
process?.platform
? process.platform.charAt(0).toUpperCase() +
process.platform.slice(1) +
' ' +
process.arch
: 'Unknown'
}) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}`
},
device: {
Expand Down Expand Up @@ -478,12 +488,12 @@ export default class Window extends EventTarget implements IWindow {
public readonly devicePixelRatio = 1;
public readonly sessionStorage: Storage;
public readonly localStorage: Storage;
public readonly performance = PerfHooks.performance;
public readonly performance = performance;
public readonly innerWidth: number = 1024;
public readonly innerHeight: number = 768;
public readonly outerWidth: number = 1024;
public readonly outerHeight: number = 768;
public readonly crypto = webcrypto;
public readonly crypto = globalThis.crypto;

// Node.js Globals
public Array: typeof Array;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import XMLHttpRequestSyncRequestScriptBuilder from '../../src/xml-http-request/u
import XMLHttpRequestCertificate from '../../src/xml-http-request/XMLHttpRequestCertificate.js';
import ProgressEvent from '../../src/event/events/ProgressEvent.js';
import HTTP from 'http';
import { TextDecoder } from 'util';
import Blob from '../../src/file/Blob.js';
import IDocument from '../../src/nodes/document/IDocument.js';
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest';
Expand Down