Skip to content

Commit

Permalink
chore [#320] Continues on implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
capricorn86 committed Feb 5, 2025
1 parent 926550d commit 6906f53
Show file tree
Hide file tree
Showing 29 changed files with 2,973 additions and 1,195 deletions.
2,451 changes: 1,368 additions & 1,083 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions packages/happy-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,7 @@
"@webref/css": "6.6.2",
"prettier": "^2.6.0",
"typescript": "^5.0.4",
"vitest": "^2.1.4",
"express": "^4.20.0"
"vitest": "^2.1.4"
},
"engines": {
"node": ">=18.0.0"
Expand Down
1 change: 1 addition & 0 deletions packages/happy-dom/src/PropertySymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,4 @@ export const credentials = Symbol('credentials');
export const blocking = Symbol('blocking');
export const moduleImportMap = Symbol('moduleImportMap');
export const dispatchError = Symbol('dispatchError');
export const supports = Symbol('supports');
49 changes: 30 additions & 19 deletions packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ export default class AsyncTaskManager {
private runningImmediates: NodeJS.Immediate[] = [];
private debugTrace: Map<number | NodeJS.Timeout | NodeJS.Immediate, string> = new Map();
private waitUntilCompleteTimer: NodeJS.Timeout | null = null;
private waitUntilCompleteResolvers: Array<() => void> = [];
private waitUntilCompleteResolvers: Array<{
resolve: () => void;
reject: (error: Error) => void;
}> = [];
private aborted = false;
private destroyed = false;
#browserFrame: IBrowserFrame;
Expand All @@ -39,8 +42,8 @@ export default class AsyncTaskManager {
* @returns Promise.
*/
public waitUntilComplete(): Promise<void> {
return new Promise((resolve) => {
this.waitUntilCompleteResolvers.push(resolve);
return new Promise((resolve, reject) => {
this.waitUntilCompleteResolvers.push({ resolve, reject });
this.resolveWhenComplete();
});
}
Expand All @@ -50,8 +53,8 @@ export default class AsyncTaskManager {
*/
public abort(): Promise<void> {
if (this.aborted) {
return new Promise((resolve) => {
this.waitUntilCompleteResolvers.push(resolve);
return new Promise((resolve, reject) => {
this.waitUntilCompleteResolvers.push({ resolve, reject });
this.resolveWhenComplete();
});
}
Expand All @@ -63,8 +66,8 @@ export default class AsyncTaskManager {
*/
public destroy(): Promise<void> {
if (this.aborted) {
return new Promise((resolve) => {
this.waitUntilCompleteResolvers.push(resolve);
return new Promise((resolve, reject) => {
this.waitUntilCompleteResolvers.push({ resolve, reject });
this.resolveWhenComplete();
});
}
Expand Down Expand Up @@ -221,15 +224,12 @@ export default class AsyncTaskManager {
* Resolves when complete.
*/
private resolveWhenComplete(): void {
this.applyDebugging();

if (this.runningTaskCount || this.runningTimers.length || this.runningImmediates.length) {
this.applyDebugging();
return;
}

if (this.#debugTimeout) {
TIMER.clearTimeout(this.#debugTimeout);
}

if (this.waitUntilCompleteTimer) {
TIMER.clearTimeout(this.waitUntilCompleteTimer);
this.waitUntilCompleteTimer = null;
Expand All @@ -241,10 +241,13 @@ export default class AsyncTaskManager {
this.waitUntilCompleteTimer = TIMER.setTimeout(() => {
this.waitUntilCompleteTimer = null;
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
if (this.#debugTimeout) {
TIMER.clearTimeout(this.#debugTimeout);
}
const resolvers = this.waitUntilCompleteResolvers;
this.waitUntilCompleteResolvers = [];
for (const resolver of resolvers) {
resolver();
resolver.resolve();
}
this.aborted = false;
} else {
Expand All @@ -262,23 +265,31 @@ export default class AsyncTaskManager {
return;
}
if (this.#debugTimeout) {
TIMER.clearTimeout(this.#debugTimeout);
return;
}
this.#debugTimeout = TIMER.setTimeout(() => {
this.#debugTimeout = null;

let error = `The maximum time was reached for "waitUntilComplete()".\n\n${
let errorMessage = `The maximum time was reached for "waitUntilComplete()".\n\n${
this.debugTrace.size
} task${
this.debugTrace.size === 1 ? '' : 's'
} did not end in time.\n\nThe following traces were recorded:\n\n`;

for (const [key, value] of this.debugTrace.entries()) {
const type = typeof key === 'number' ? 'Task' : 'Timer';
error += `${type} #${key}\n______________________________________\n${value}\n\n`;
errorMessage += `${type} #${key}\n‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾${value
.replace(/Error:/, '')
.replace(/\s+at /gm, '\n> ')}\n\n`;
}

const error = new Error(errorMessage);

for (const resolver of this.waitUntilCompleteResolvers) {
resolver.reject(error);
}

throw new Error(error);
this.abortAll(true);
}, debug.traceWaitUntilComplete);
}

Expand Down Expand Up @@ -318,8 +329,8 @@ export default class AsyncTaskManager {
}

// We need to wait for microtasks to complete before resolving.
return new Promise((resolve) => {
this.waitUntilCompleteResolvers.push(resolve);
return new Promise((resolve, reject) => {
this.waitUntilCompleteResolvers.push({ resolve, reject });
this.resolveWhenComplete();
});
}
Expand Down
4 changes: 4 additions & 0 deletions packages/happy-dom/src/browser/BrowserSettingsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export default class BrowserSettingsFactory {
device: {
...DefaultBrowserSettings.device,
...settings?.device
},
debug: {
...DefaultBrowserSettings.debug,
...settings?.debug
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import IBrowserFrame from '../types/IBrowserFrame.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import IGoToOptions from '../types/IGoToOptions.js';
import Response from '../../fetch/Response.js';
import DocumentReadyStateManager from '../../nodes/document/DocumentReadyStateManager.js';
import BrowserWindow from '../../window/BrowserWindow.js';
import BrowserFrameFactory from './BrowserFrameFactory.js';
import BrowserFrameURL from './BrowserFrameURL.js';
Expand Down
16 changes: 12 additions & 4 deletions packages/happy-dom/src/dom/DOMTokenList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,29 @@ export default class DOMTokenList {
items: [],
attributeValue: ''
};
private [PropertySymbol.supports]: string[];

/**
* Constructor.
*
* @param illegalConstructorSymbol Illegal constructor symbol.
* @param ownerElement Owner element.
* @param attributeName Attribute name.
* @param [supports] Supports.
*/
constructor(illegalConstructorSymbol: symbol, ownerElement: Element, attributeName: string) {
constructor(
illegalConstructorSymbol: symbol,
ownerElement: Element,
attributeName: string,
supports?: string[]
) {
if (illegalConstructorSymbol !== PropertySymbol.illegalConstructor) {
throw new TypeError('Illegal constructor');
}

this[PropertySymbol.ownerElement] = ownerElement;
this[PropertySymbol.attributeName] = attributeName;
this[PropertySymbol.supports] = supports || [];

const methodBinder = new ClassMethodBinder(this, [DOMTokenList]);

Expand Down Expand Up @@ -189,10 +197,10 @@ export default class DOMTokenList {
/**
* Supports.
*
* @param _token Token.
* @param token Token.
*/
public supports(_token: string): boolean {
return false;
public supports(token: string): boolean {
return this[PropertySymbol.supports].includes(token);
}

/**
Expand Down
11 changes: 6 additions & 5 deletions packages/happy-dom/src/fetch/Request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,21 @@ export default class Request implements Request {
if (init?.mode) {
switch (init.mode) {
case 'navigate':
throw new window.DOMException(
`Failed to construct 'Request': Cannot construct a Request with a RequestInit whose mode member is set as 'navigate'.`,
DOMExceptionNameEnum.securityError
);
case 'websocket':
throw new window.DOMException(
`Failed to construct 'Request': Cannot construct a Request with a RequestInit whose mode member is set as 'websocket'.`,
`Failed to construct 'Request': Cannot construct a Request with a RequestInit whose mode member is set as '${init.mode}'.`,
DOMExceptionNameEnum.securityError
);
case 'same-origin':
case 'no-cors':
case 'cors':
this[PropertySymbol.mode] = init.mode;
break;
default:
throw new window.DOMException(
`Failed to construct 'Request': The provided value '${init.mode}' is not a valid enum value of type RequestMode.`,
DOMExceptionNameEnum.syntaxError
);
}
} else if (input instanceof Request) {
this[PropertySymbol.mode] = input.mode;
Expand Down
32 changes: 18 additions & 14 deletions packages/happy-dom/src/module/ECMAScriptModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import WindowBrowserContext from '../window/WindowBrowserContext.js';
import IECMAScriptModuleCompiledResult from './IECMAScriptModuleCompiledResult.js';
import ModuleFactory from './ModuleFactory.js';

const EMPTY_COMPILED_RESULT = { imports: [], execute: () => {} };

/**
* ECMAScript module.
*/
export default class ECMAScriptModule implements IModule {
public readonly url: URL;
public readonly [PropertySymbol.window]: BrowserWindow;
readonly #source: string;
#preloaded: boolean = false;
#compiled: IECMAScriptModuleCompiledResult | null = null;
#exports: { [k: string]: any } | null = null;

Expand All @@ -36,7 +39,7 @@ export default class ECMAScriptModule implements IModule {
*
* @returns Module exports.
*/
public async evaluate(): Promise<{ [key: string]: any }> {
public async evaluate(): Promise<{ [key: string]: any } | null> {
if (this.#exports) {
return this.#exports;
}
Expand All @@ -50,9 +53,6 @@ export default class ECMAScriptModule implements IModule {
return {};
}

const asyncTaskManager = browserFrame[PropertySymbol.asyncTaskManager];
const taskID = asyncTaskManager.startTask();

for (const moduleImport of compiled.imports) {
modulePromises.push(
ModuleFactory.getModule(window, this.url, moduleImport.url, {
Expand All @@ -62,24 +62,24 @@ export default class ECMAScriptModule implements IModule {
}

const modules = await Promise.all(modulePromises);

const imports = new Map<string, { [key: string]: any }>();

for (const module of modules) {
imports.set(module.url.href, await module.evaluate());
}

asyncTaskManager.endTask(taskID);

const exports = {};

compiled.execute.call(window, {
this.#exports = exports;

compiled.execute({
dispatchError: window[PropertySymbol.dispatchError].bind(window),
dynamicImport: this.#import.bind(this),
imports,
exports
});

this.#exports = exports;

return exports;
}

Expand All @@ -89,6 +89,12 @@ export default class ECMAScriptModule implements IModule {
* @returns Promise.
*/
public async preload(): Promise<void> {
if (this.#preloaded) {
return;
}

this.#preloaded = true;

const compiled = this.#compile();
const modulePromises: Promise<IModule>[] = [];
const window = this[PropertySymbol.window];
Expand All @@ -98,9 +104,6 @@ export default class ECMAScriptModule implements IModule {
return;
}

const asyncTaskManager = browserFrame[PropertySymbol.asyncTaskManager];
const taskID = asyncTaskManager?.startTask();

for (const moduleImport of compiled.imports) {
modulePromises.push(
ModuleFactory.getModule(window, this.url, moduleImport.url, {
Expand All @@ -117,8 +120,6 @@ export default class ECMAScriptModule implements IModule {
}

await Promise.all(promises);

asyncTaskManager.endTask(taskID);
}

/**
Expand All @@ -129,6 +130,9 @@ export default class ECMAScriptModule implements IModule {
return this.#compiled;
}

// In case of an error, the compiled module will be empty.
this.#compiled = EMPTY_COMPILED_RESULT;

const compiler = new ECMAScriptModuleCompiler(this[PropertySymbol.window]);

this.#compiled = compiler.compile(this.url.href, this.#source);
Expand Down
Loading

0 comments on commit 6906f53

Please sign in to comment.