diff --git a/packages/happy-dom/src/dynamic-import/DynamicImport.ts b/packages/happy-dom/src/dynamic-import/DynamicImport.ts deleted file mode 100644 index f0be654a1..000000000 --- a/packages/happy-dom/src/dynamic-import/DynamicImport.ts +++ /dev/null @@ -1,41 +0,0 @@ -import BrowserWindow from '../window/BrowserWindow.js'; -import { URL } from 'url'; -import Module from './Module.js'; -import * as PropertySymbol from '../PropertySymbol.js'; - -/** - * Dynamic import. - * - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import - */ -export default class DynamicImport { - private window: BrowserWindow; - private parentModule: Module; - - /** - * Constructor. - * - * @param window Window. - * @param [parentModule] Parent module. - */ - constructor(window: BrowserWindow, parentModule?: Module) { - this.window = window; - this.parentModule = parentModule || new Module(window, new URL(this.window.location.href)); - } - - /** - * Import a module dynamically. - * - * @param url URL of the module. - * @returns Promise. - */ - public async import(url: string): Promise { - const absoluteURL = new URL(url, this.parentModule.url.href); - const modules = this.window[PropertySymbol.modules]; - const module = modules.get(absoluteURL.href); - - if (module) { - return module; - } - } -} diff --git a/packages/happy-dom/src/dynamic-import/ModuleParser.ts b/packages/happy-dom/src/dynamic-import/ModuleParser.ts deleted file mode 100644 index ce5a7a226..000000000 --- a/packages/happy-dom/src/dynamic-import/ModuleParser.ts +++ /dev/null @@ -1,284 +0,0 @@ -import BrowserWindow from '../window/BrowserWindow.js'; - -/** - * Code regexp. - * - * Group 1: Import exported variables. - * Group 2: Import exported url. - * Group 3: Import without name part. - * Group 4: Modules in export from module statement. - * Group 5: Import in export from module statement. - * Group 6: Export default statement. - * Group 7: Export function or class type. - * Group 8: Export function or class name. - * Group 9: Export object. - * Group 10: Export variable type (var, let or const). - * Group 11: Export variable name. - * Group 12: Export variable name end character (= or ;). - * Group 13: Single line comment. - * Group 14: Multi line comment. - * Group 15: Slash (RegExp). - * Group 16: Parentheses. - * Group 17: Curly braces. - * Group 18: Square brackets. - * Group 19: String apostrophe (', " or `). - */ -const CODE_REGEXP = /(import\s+)|\sfrom\s["']([^"']+)["']|export\s(.+)\sfrom\s["']([^"']+)["'];|(export\sdefault\s)|export\s(function\*{0,1}|class)\s([^({\s]+)|export\s{([^}]+)}|export\s(var|let|const)\s+([^=;]+)(=|;)|(\/\/.*$)|(\/\*|\*\/)|(\/)|(\(|\))|({|})|(\[|\])|('|"|`)/gm; - -/** - * Import regexp. - * - * Group 1: Import braces. - * Group 2: Import all as. - * Group 3: Import default. - */ -const IMPORT_REGEXP = /{([^}]+)}|\*\s+as\s+([a-zA-Z0-9-_$]+)|([a-zA-Z0-9-_$]+)/gm; - -/** - * Module parser. - * - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import - */ -export default class ModuleParser { - public readonly window: BrowserWindow; - - /** - * Constructor. - * - * @param window Window. - * @param url Module URL. - */ - constructor(window: BrowserWindow) { - this.window = window; - } - - /** - * Parse a module. - * - * @param moduleURL Module URL. - * @param code Code. - * @returns Result. - */ - public parse(moduleURL: string, code: string): { imports: string[]; code: string } { - const regExp = new RegExp(CODE_REGEXP); - const imports: string[] = []; - const count = { - comment: 0, - parantheses: 0, - curlyBraces: 0, - squareBrackets: 0, - regExp: 0 - }; - const exportSpreadVariables: Array> = []; - let newCode = ''; - let match: RegExpExecArray; - let precedingToken: string; - let stringCharacter: string | null = null; - let lastIndex = 0; - let importStartIndex = -1; - let skipCode = false; - - while ((match = regExp.exec(code))) { - precedingToken = code[match.index - 1]; - if (match[11]) { - // Ignore single line comment - } else if (match[12]) { - // Multi line comment - if (match[12] === '/*') { - count.comment++; - } else if (match[12] === '*/' && count.comment > 0) { - count.comment--; - } - } else if (match[13]) { - // Slash (RegExp) - if (precedingToken !== '\\') { - count.regExp = count.regExp === 0 ? 1 : 0; - } - } else if (match[14]) { - // Parentheses - if (match[14] === '(') { - count.parantheses++; - } - if (match[14] === ')' && count.parantheses > 0) { - count.parantheses--; - } - } else if (match[15]) { - // Curly braces - if (match[15] === '{') { - count.curlyBraces++; - } - if (match[15] === '}' && count.curlyBraces > 0) { - count.curlyBraces--; - } - } else if (match[16]) { - // Square brackets - if (match[16] === '[') { - count.squareBrackets++; - } - if (match[16] === ']' && count.squareBrackets > 0) { - count.squareBrackets--; - } - } else if (match[17]) { - // String - if (precedingToken !== '\\') { - if (stringCharacter === null) { - stringCharacter = match[7]; - } else if (stringCharacter === match[7]) { - stringCharacter = null; - } - } - } else if (count.curlyBraces === 0 && count.squareBrackets === 0) { - /** - * Code regexp. - * - * Group 1: Import name. - * Group 2: Import URL. - * Group 3: Import without name. - * Group 4: Modules in export from module statement. - * Group 5: Import in export from module statement. - * Group 6: Export default statement. - * Group 7: Export function or class type. - * Group 8: Export function or class name. - * Group 9: Export object. - * Group 10: Export variable type (var, let or const). - * Group 11: Export variable name. - * Group 12: Export variable name end character (= or ;). - * Group 13: Single line comment. - * Group 14: Multi line comment. - * Group 15: Slash (RegExp). - * Group 16: Parentheses. - * Group 17: Curly braces. - * Group 18: Square brackets. - * Group 19: String apostrophe (', " or `). - */ - if (match[1]) { - // Import statement start - importStartIndex = match.index + match[0].length; - skipCode = true; - } else if(match[2]) { - // Import statement end - if(importStartIndex !== -1) { - const url = new URL(match[2], moduleURL).href; - const variables = code.substring(importStartIndex, match.index); - const importRegExp = new RegExp(IMPORT_REGEXP); - let importMatch: RegExpExecArray; - while((importMatch = importRegExp.exec(variables))) { - if(importMatch[1]) { - // Import braces - newCode += `const { ${importMatch[1].replace(/\s+as\s+/gm, ': ')} } = __happy_dom_imports__.get('${url}');\n`; - } else if(importMatch[2]) { - // Import all as - newCode += `const ${importMatch[2]} = __happy_dom_imports__.get('${url}');\n`; - } - } - skipCode = true; - } - } else if (match[9] && match[10]) { - const url = new URL(match[10], moduleURL).href; - const imported = match[9]; - - newCode += code.substring(lastIndex, match.index); - - // Export from module statement - if (imported === '*') { - newCode += `Object.assign(__happy_dom_exports__, __happy_dom_imports__.get('${url}'))`; - imports.push(url); - } else if (imported[0] === '*') { - const parts = imported.split(/\s+as\s+/); - if (parts.length === 2) { - const exportName = parts[1].replace(/["']/g, ''); - newCode += `__happy_dom_exports__['${exportName}'] = __happy_dom_imports__.get('${url}')`; - imports.push(url); - } - } else if (imported[0] === '{' && imported[imported.length - 1] === '}') { - const parts = imported.slice(1, -1).split(/\s*,\s*/); - for (const part of parts) { - const nameParts = part.split(/\s+as\s+/); - const exportName = (nameParts[1] || nameParts[0]).replace(/["']/g, ''); - const importName = nameParts[0].replace(/["']/g, ''); - newCode += `__happy_dom_exports__['${exportName}'] = __happy_dom_imports__.get('${url}')?.['${importName}'];\n`; - } - imports.push(url); - } - skipCode = true; - } else if (match[11]) { - // Export default statement - newCode += - code.substring(lastIndex, match.index) + '__happy_dom_exports__.default = '; - skipCode = true; - } else if (match[12] && match[13]) { - // Export function or class type - newCode += - code.substring(lastIndex, match.index) + - `__happy_dom_exports__['${match[13]}'] = ${match[12]}`; - skipCode = true; - } else if (match[14]) { - // Export object - newCode += code.substring(lastIndex, match.index); - const parts = match[14].split(/\s*,\s*/); - for (const part of parts) { - const nameParts = part.split(/\s+as\s+/); - const exportName = (nameParts[1] || nameParts[0]).replace(/["']/g, ''); - const importName = nameParts[0].replace(/["']/g, ''); - newCode += `__happy_dom_exports__['${exportName}'] = ${importName};\n`; - } - skipCode = true; - } else if (match[15]) { - // Export variable - if (match[17] === '=') { - const exportName = match[16].trim(); - if ( - (exportName[0] === '{' && exportName[exportName.length - 1] === '}') || - (exportName[0] === '[' && exportName[exportName.length - 1] === ']') - ) { - newCode += code.substring(lastIndex, match.index); - const parts = match[16].split(/\s*,\s*/); - const variableObject: Map = new Map(); - - for (const part of parts) { - const nameParts = part.split(/\s+:\s+/); - const exportName = (nameParts[1] || nameParts[0]).replace(/["']/g, ''); - const importName = nameParts[0].replace(/["']/g, ''); - variableObject.set(exportName, importName); - } - - newCode += `const __happy_dom_variable_spread_${exportSpreadVariables.length}__ =`; - exportSpreadVariables.push(variableObject); - } else { - newCode += - code.substring(lastIndex, match.index) + - `__happy_dom_exports__['${exportName}'] =`; - } - } else { - // TODO: If there is no =, we should ignore until we know what is is useful for - // Example: export let name1, name2, name3; - newCode += - code.substring(lastIndex, match.index) + `/*Unknown export: ${match[0]}*/`; - this.window.console.warn(`Unknown export in "${moduleURL}": ${match[0]}`); - } - skipCode = true; - } - } - } - - if (!skipCode) { - newCode += code.substring(lastIndex, match.index + match[0].length); - } - - skipCode = false; - lastIndex = regExp.lastIndex; - } - - if (exportSpreadVariables.length > 0) { - for (let i = 0; i < exportSpreadVariables.length; i++) { - for (const [exportName, importName] of exportSpreadVariables[i]) { - newCode += `__happy_dom_exports__['${exportName}'] = __happy_dom_variable_spread_${i}__['${importName}'];\n`; - } - } - } - - newCode += code.substring(lastIndex); - - return { imports, code: newCode }; - } -} diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts index c42ac982c..005aaaa41 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -9,6 +9,7 @@ import Attr from '../attr/Attr.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import ResourceFetch from '../../fetch/ResourceFetch.js'; import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; +import DynamicImport from '../../dynamic-import/DynamicImport.js'; /** * HTML Script Element. @@ -195,29 +196,34 @@ export default class HTMLScriptElement extends HTMLElement { } else if (browserSettings && !browserSettings.disableJavaScriptEvaluation) { const textContent = this.textContent; const type = this.getAttribute('type'); - if ( - textContent && - (type === null || + + if (textContent) { + if (type === 'module') { + this.#parseModule(new URL(this[PropertySymbol.window].location.href), textContent); + } else if ( + type === null || type === 'application/x-ecmascript' || type === 'application/x-javascript' || - type.startsWith('text/javascript')) - ) { - this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = this; - - const code = `//# sourceURL=${this[PropertySymbol.window].location.href}\n` + textContent; - - if ( - browserSettings.disableErrorCapturing || - browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch + type.startsWith('text/javascript') ) { - this[PropertySymbol.window].eval(code); - } else { - WindowErrorUtility.captureError(this[PropertySymbol.window], () => - this[PropertySymbol.window].eval(code) - ); + this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = this; + + const code = + `//# sourceURL=${this[PropertySymbol.window].location.href}\n` + textContent; + + if ( + browserSettings.disableErrorCapturing || + browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch + ) { + this[PropertySymbol.window].eval(code); + } else { + WindowErrorUtility.captureError(this[PropertySymbol.window], () => + this[PropertySymbol.window].eval(code) + ); + } + + this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = null; } - - this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = null; } } } @@ -241,6 +247,38 @@ export default class HTMLScriptElement extends HTMLElement { } } + /** + * Parses a module. + * + * @param url URL. + * @param source Source. + */ + async #parseModule(url: URL, source: string): Promise { + const browserSettings = new WindowBrowserContext(this[PropertySymbol.window]).getSettings(); + const dynamicImport = new DynamicImport(this[PropertySymbol.window]); + const readyStateManager = (<{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }>( + (this[PropertySymbol.window]) + ))[PropertySymbol.readyStateManager]; + readyStateManager.startTask(); + + if ( + browserSettings.disableErrorCapturing || + browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch + ) { + await dynamicImport.parseModule(url, 'javascript', source); + } else { + try { + await dynamicImport.parseModule(url, 'javascript', source); + } catch (error) { + WindowErrorUtility.dispatchError(this, error); + } + } + + readyStateManager.endTask(); + + this.dispatchEvent(new Event('load')); + } + /** * Returns a URL relative to the given Location object. * @@ -249,10 +287,6 @@ export default class HTMLScriptElement extends HTMLElement { async #loadScript(url: string): Promise { const window = this[PropertySymbol.window]; const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); - const async = - this.getAttribute('async') !== null || - this.getAttribute('defer') !== null || - this.getAttribute('type') === 'module'; if (!browserFrame) { return; @@ -293,15 +327,43 @@ export default class HTMLScriptElement extends HTMLElement { return; } + this.#loadedScriptURL = absoluteURL; + + if (this.getAttribute('type') === 'module') { + const readyStateManager = (< + { [PropertySymbol.readyStateManager]: DocumentReadyStateManager } + >(this[PropertySymbol.window]))[PropertySymbol.readyStateManager]; + const dynamicImport = new DynamicImport(this[PropertySymbol.window]); + + readyStateManager.startTask(); + + if ( + browserSettings.disableErrorCapturing || + browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch + ) { + await dynamicImport.import(absoluteURL); + } else { + try { + await dynamicImport.import(absoluteURL); + } catch (error) { + WindowErrorUtility.dispatchError(this, error); + } + } + + readyStateManager.endTask(); + + this.dispatchEvent(new Event('load')); + return; + } + const resourceFetch = new ResourceFetch({ browserFrame, window: this[PropertySymbol.window] }); + const async = this.getAttribute('async') !== null || this.getAttribute('defer') !== null; let code: string | null = null; let error: Error | null = null; - this.#loadedScriptURL = absoluteURL; - if (async) { const readyStateManager = (< { [PropertySymbol.readyStateManager]: DocumentReadyStateManager } diff --git a/packages/happy-dom/src/nodes/html-script-element/module/DynamicImport.ts b/packages/happy-dom/src/nodes/html-script-element/module/DynamicImport.ts new file mode 100644 index 000000000..6ad2342e1 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-script-element/module/DynamicImport.ts @@ -0,0 +1,153 @@ +import BrowserWindow from '../../../window/BrowserWindow.js'; +import { URL } from 'url'; +import Module from './Module.js'; +import * as PropertySymbol from '../../../PropertySymbol.js'; +import EcmaScriptModuleParser from './EcmaScriptModuleParser.js'; + +/** + * Dynamic import. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import + */ +export default class DynamicImport { + private window: BrowserWindow; + private parentModuleURL: URL | null; + + /** + * Constructor. + * + * @param window Window. + * @param [parentModuleURL] Parent module URL. + */ + constructor(window: BrowserWindow, parentModuleURL: URL | null = null) { + this.window = window; + this.parentModuleURL = parentModuleURL; + } + + /** + * Import a module dynamically. + * + * @param url URL of the module. + * @param [options] Options. + * @param [options.with] With. + * @param [options.with.type] Type. + * @returns Promise. + */ + public async import(url: string, options?: { with?: { type?: string } }): Promise { + const absoluteURL = this.parentModuleURL ? new URL(url, this.parentModuleURL) : new URL(url); + const modules = this.window[PropertySymbol.modules]; + const type = options?.with?.type || 'ecmascript'; + + if (type !== 'ecmascript' && type !== 'json' && type !== 'css') { + throw new this.window.TypeError( + `Failed to parse module "${absoluteURL.href}": Unkown type "${type}"` + ); + } + + const module = modules[type].get(absoluteURL.href); + + if (module) { + return module; + } + + const response = await this.window.fetch(absoluteURL); + const source = await response.text(); + const newModule = await this.parseModule(absoluteURL, type, source); + + modules[type].set(absoluteURL.href, newModule); + + return newModule; + } + + /** + * Parse a module. + * + * @param url URL of the module. + * @param type Type of the module. + * @param source Source code of the module. + * @returns Module. + */ + public async parseModule(url: URL, type: string, source: string): Promise { + switch (type) { + case 'json': + const jsonModule = new Module(this.window, url); + jsonModule.type = 'json'; + try { + jsonModule.exports.default = JSON.parse(source); + } catch (error) { + throw new this.window.TypeError( + `Failed to parse "json" module "${url.href}": ${error.message}` + ); + } + return jsonModule; + case 'css': + const cssModule = new Module(this.window, url); + const stylesheet = new this.window.CSSStyleSheet(); + stylesheet.replaceSync(source); + cssModule.type = 'css'; + cssModule.exports.default = stylesheet; + return cssModule; + case 'ecmascript': + const parser = new EcmaScriptModuleParser(this.window); + const result = parser.parse(url.href, source); + const module = new Module(this.window, url); + const importMap = new Map(); + + for (const moduleImport of result.imports) { + const module = await this.import(moduleImport.url, { with: { type: moduleImport.type } }); + importMap.set(moduleImport.url, module.exports); + module.imports.set(moduleImport.url, module); + } + + const dynamicImport = new DynamicImport(this.window, module.url); + const dynamicImportFunction = async ( + url: string, + options?: { with?: { type?: string } } + ): Promise<{ [key: string]: any }> => { + const module = await dynamicImport.import(url, options); + return module.exports; + }; + + module.exports = this.evaluateEcmascriptModule({ + dynamicImport: dynamicImportFunction, + code: result.code, + imports: importMap, + exports: {} + }); + + return module; + default: + throw new this.window.TypeError( + `Failed to parse module "${url.href}": Unkown type "${type}"` + ); + } + } + + /** + * Evaluate a JavaScript module. + * + * @param $happy_dom Happy DOM object. + * @param $happy_dom.dynamicImport Function to import a module. + * @param $happy_dom.code Code. + * @param $happy_dom.imports Imports. + * @param $happy_dom.exports Exports. + * @returns Exports. + */ + private evaluateEcmascriptModule( + // eslint-disable-next-line + $happy_dom: { + dynamicImport: ( + url: string, + options?: { with?: { type?: string } } + ) => Promise<{ [key: string]: any }>; + code: string; + imports: Map; + exports: { [key: string]: any }; + } + ): { [key: string]: any } { + // eslint-disable-next-line + this.window.eval.call({}, $happy_dom.code); + // eslint-disable-next-line + return $happy_dom.exports; + } +} diff --git a/packages/happy-dom/src/nodes/html-script-element/module/EcmaScriptModuleParser.ts b/packages/happy-dom/src/nodes/html-script-element/module/EcmaScriptModuleParser.ts new file mode 100644 index 000000000..7e12b8c31 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-script-element/module/EcmaScriptModuleParser.ts @@ -0,0 +1,382 @@ +import BrowserWindow from '../../../window/BrowserWindow.js'; + +/** + * Code regexp. + * + * Group 1: Import without exported properties. + * Group 2: Dynamic import function call. + * Group 3: Import exported variables. + * Group 4: Import exported url. + * Group 5: Import with group. + * Group 6: Import with type. + * Group 7: Modules in export from module statement. + * Group 8: Import in export from module statement. + * Group 9: Export default statement. + * Group 10: Export function or class type. + * Group 11: Export function or class name. + * Group 12: Export object. + * Group 13: Export variable type (var, let or const). + * Group 14: Export variable name. + * Group 15: Export variable name end character (= or ;). + * Group 16: Single line comment. + * Group 17: Multi line comment. + * Group 18: Slash (RegExp). + * Group 19: Parentheses. + * Group 20: Curly braces. + * Group 21: Square brackets. + * Group 22: String apostrophe (', " or `). + */ +const CODE_REGEXP = + /import\s["']([^"']+)["'];{0,1}|import\s*\(([^)]+)\)|(import\s+)|\sfrom\s["']([^"']+)["'](\s+with\s*{\s*type\s*:\s*["']([^"']+)["']\s*}){0,1}|export\s([a-zA-Z0-9-_$]+|\*|\*\s+as\s+["'a-zA-Z0-9-_$]+|{[^}]+})\sfrom\s["']([^"']+)["']|(export\sdefault\s)|export\s(function\*{0,1}|class)\s([^({\s]+)|export\s{([^}]+)}|export\s(var|let|const)\s+([^=;]+)(=|;)|(\/\/.*$)|(\/\*|\*\/)|(\/)|(\(|\))|({|})|(\[|\])|('|"|`)/gm; + +/** + * Import regexp. + * + * Group 1: Import braces. + * Group 2: Import all as. + * Group 3: Import default. + */ +const IMPORT_REGEXP = /{([^}]+)}|\*\s+as\s+([a-zA-Z0-9-_$]+)|([a-zA-Z0-9-_$]+)/gm; + +/** + * Valid preceding token before a statement regexp. + */ +const PRECEDING_STATEMENT_TOKEN_REGEXP = /['"`(){}\s;]/; + +/** + * Multiline comment regexp. + */ +const MULTILINE_COMMENT_REGEXP = /\/\*|\*\//gm; + +interface IModuleImport { + url: string; + type: string; +} + +/** + * Module parser. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/export + */ +export default class EcmaScriptModuleParser { + public readonly window: BrowserWindow; + + /** + * Constructor. + * + * @param window Window. + * @param url Module URL. + */ + constructor(window: BrowserWindow) { + this.window = window; + } + + /** + * Parse a module. + * + * @param moduleURL Module URL. + * @param code Code. + * @returns Result. + */ + public parse(moduleURL: string, code: string): { imports: IModuleImport[]; code: string } { + const regExp = new RegExp(CODE_REGEXP); + const imports: IModuleImport[] = []; + const count = { + comment: 0, + parantheses: 0, + curlyBraces: 0, + squareBrackets: 0, + regExp: 0 + }; + const exportSpreadVariables: Array> = []; + let newCode = `//# sourceURL=${moduleURL}\n`; + let match: RegExpExecArray; + let precedingToken: string; + let stringCharacter: string | null = null; + let lastIndex = 0; + let importStartIndex = -1; + let skipMatchedCode = false; + + while ((match = regExp.exec(code))) { + if (importStartIndex === -1) { + newCode += code.substring(lastIndex, match.index); + } + precedingToken = code[match.index - 1] || ' '; + + // Imports and exports are only valid outside any statement, string or comment at the top level + if ( + count.comment === 0 && + count.parantheses === 0 && + count.curlyBraces === 0 && + count.squareBrackets === 0 && + count.regExp === 0 && + !stringCharacter + ) { + if (match[1] && PRECEDING_STATEMENT_TOKEN_REGEXP.test(precedingToken)) { + // Import without exported properties + imports.push({ url: new URL(match[1], moduleURL).href, type: 'ecmascript' }); + skipMatchedCode = true; + } else if (match[3] && PRECEDING_STATEMENT_TOKEN_REGEXP.test(precedingToken)) { + // Import statement start + if (importStartIndex !== -1) { + throw new this.window.TypeError( + `Failed to parse module: Unexpected import statement in "${moduleURL}"` + ); + } + importStartIndex = match.index + match[0].length; + skipMatchedCode = true; + } else if (match[4]) { + // Import statement end + if (importStartIndex !== -1) { + const url = new URL(match[4], moduleURL).href; + const variables = code.substring(importStartIndex, match.index); + const importRegExp = new RegExp(IMPORT_REGEXP); + const importCode: string[] = []; + let importMatch: RegExpExecArray; + while ((importMatch = importRegExp.exec(variables))) { + if (importMatch[1]) { + // Import braces + importCode.push( + `const {${importMatch[1].replace( + /\s+as\s+/gm, + ': ' + )}} = $happy_dom.imports.get('${url}')` + ); + } else if (importMatch[2]) { + // Import all as + importCode.push(`const ${importMatch[2]} = $happy_dom.imports.get('${url}')`); + } else if (importMatch[3]) { + // Import default + importCode.push( + `const ${importMatch[3]} = $happy_dom.imports.get('${url}').default` + ); + } + } + newCode += importCode.join(';\n'); + importStartIndex = -1; + imports.push({ url, type: match[6] || 'ecmascript' }); + skipMatchedCode = true; + } + } else if (match[7] && match[8] && PRECEDING_STATEMENT_TOKEN_REGEXP.test(precedingToken)) { + // Export from module statement + + const url = new URL(match[8], moduleURL).href; + const imported = match[7]; + + if (imported === '*') { + newCode += `Object.assign($happy_dom.exports, $happy_dom.imports.get('${url}'))`; + imports.push({ url, type: 'ecmascript' }); + } else if (imported[0] === '*') { + const parts = imported.split(/\s+as\s+/); + if (parts.length === 2) { + const exportName = parts[1].replace(/["']/g, ''); + newCode += `$happy_dom.exports['${exportName}'] = $happy_dom.imports.get('${url}')`; + imports.push({ url, type: 'ecmascript' }); + } + } else if (imported[0] === '{') { + const parts = this.removeMultilineComments(imported) + .slice(1, -1) + .split(/\s*,\s*/); + const exportCode: string[] = []; + for (const part of parts) { + const nameParts = part.trim().split(/\s+as\s+/); + const exportName = (nameParts[1] || nameParts[0]).replace(/["']/g, ''); + const importName = nameParts[0].replace(/["']/g, ''); + if (exportName && importName) { + exportCode.push( + `$happy_dom.exports['${exportName}'] = $happy_dom.imports.get('${url}')['${importName}']` + ); + } + } + newCode += exportCode.join(';\n'); + imports.push({ url, type: 'ecmascript' }); + } + skipMatchedCode = true; + } else if (match[9] && PRECEDING_STATEMENT_TOKEN_REGEXP.test(precedingToken)) { + // Export default statement + newCode += '$happy_dom.exports.default = '; + skipMatchedCode = true; + } else if ( + match[10] && + match[11] && + PRECEDING_STATEMENT_TOKEN_REGEXP.test(precedingToken) + ) { + // Export function or class type + newCode += `$happy_dom.exports['${match[11]}'] = ${match[10]} ${match[11]}`; + skipMatchedCode = true; + } else if (match[12] && PRECEDING_STATEMENT_TOKEN_REGEXP.test(precedingToken)) { + // Export object + const parts = this.removeMultilineComments(match[12]).split(/\s*,\s*/); + const exportCode: string[] = []; + for (const part of parts) { + const nameParts = part.trim().split(/\s+as\s+/); + const exportName = (nameParts[1] || nameParts[0]).replace(/["']/g, ''); + const importName = nameParts[0].replace(/["']/g, ''); + if (exportName && importName) { + exportCode.push(`$happy_dom.exports['${exportName}'] = ${importName}`); + } + } + newCode += exportCode.join(';\n'); + skipMatchedCode = true; + } else if (match[13] && PRECEDING_STATEMENT_TOKEN_REGEXP.test(precedingToken)) { + // Export variable + if (match[15] === '=') { + const exportName = this.removeMultilineComments(match[14]).trim(); + if ( + (exportName[0] === '{' && exportName[exportName.length - 1] === '}') || + (exportName[0] === '[' && exportName[exportName.length - 1] === ']') + ) { + const parts = exportName.slice(1, -1).split(/\s*,\s*/); + const variableObject: Map = new Map(); + + for (const part of parts) { + const nameParts = part.trim().split(/\s*:\s*/); + const exportName = (nameParts[1] || nameParts[0]).replace(/["']/g, ''); + const importName = nameParts[0].replace(/["']/g, ''); + if (exportName && importName) { + variableObject.set(exportName, importName); + } + } + + newCode += `const $happy_dom_export_${exportSpreadVariables.length} =`; + exportSpreadVariables.push(variableObject); + } else { + newCode += `$happy_dom.exports['${exportName}'] =`; + } + } else { + // TODO: If there is no =, we should ignore until we know what is is useful for + // Example: export let name1, name2, name3; + newCode += `/*Unknown export: ${match[0]}*/`; + this.window.console.warn(`Unknown export in "${moduleURL}": ${match[0]}`); + } + skipMatchedCode = true; + } + } + + if (stringCharacter === null && count.comment === 0 && count.regExp === 0) { + if ( + match[2] && + count.comment === 0 && + count.regExp === 0 && + PRECEDING_STATEMENT_TOKEN_REGEXP.test(precedingToken) + ) { + // Dynamic import function call + newCode += `$happy_dom.dynamicImport(${match[2]})`; + skipMatchedCode = true; + } else if (match[19] && count.regExp === 0 && count.comment === 0) { + // Parentheses + if (match[19] === '(') { + count.parantheses++; + } + if (match[19] === ')' && count.parantheses > 0) { + count.parantheses--; + } + } else if (match[20] && count.regExp === 0 && count.comment === 0) { + // Curly braces + if (match[20] === '{') { + count.curlyBraces++; + } + if (match[20] === '}' && count.curlyBraces > 0) { + count.curlyBraces--; + } + } else if (match[21] && count.regExp === 0 && count.comment === 0) { + // Square brackets + if (match[21] === '[') { + count.squareBrackets++; + } + if (match[21] === ']' && count.squareBrackets > 0) { + count.squareBrackets--; + } + } + } else if (stringCharacter === null && count.comment === 0) { + if (match[18]) { + // Slash (RegExp) + if (precedingToken !== '\\') { + count.regExp = count.regExp === 0 ? 1 : 0; + } + } + } else if (stringCharacter === null && count.regExp === 0) { + if (match[16]) { + // Ignore single line comment + } else if (match[17]) { + // Multi line comment + if (match[17] === '/*') { + count.comment++; + } else if (match[17] === '*/' && count.comment > 0) { + count.comment--; + } + } + } else if (match[22] && count.regExp === 0) { + // String + if (precedingToken !== '\\') { + if (stringCharacter === null) { + stringCharacter = match[22]; + } else if (stringCharacter === match[22]) { + stringCharacter = null; + } + } + } + + // Unless the code has been handled by transforming imports or exports, we add it to the new code + if (!skipMatchedCode && importStartIndex === -1) { + newCode += match[0]; + } + + skipMatchedCode = false; + lastIndex = regExp.lastIndex; + } + + if (importStartIndex !== -1) { + throw new this.window.TypeError( + `Failed to parse module: Unexpected import statement in "${moduleURL}"` + ); + } + + newCode += code.substring(lastIndex); + + if (exportSpreadVariables.length > 0) { + newCode += '\n\n'; + + for (let i = 0; i < exportSpreadVariables.length; i++) { + for (const [exportName, importName] of exportSpreadVariables[i]) { + newCode += `$happy_dom.exports['${exportName}'] = $happy_dom_export_${i}['${importName}'];\n`; + } + } + } + + return { imports, code: newCode }; + } + + /** + * Remove multiline comments. + * + * @param code Code. + * @returns Code without multiline comments. + */ + private removeMultilineComments(code: string): string { + const regexp = new RegExp(MULTILINE_COMMENT_REGEXP); + let match: RegExpExecArray; + let count = 0; + let lastIndex = 0; + let newCode = ''; + + while ((match = regexp.exec(code))) { + if (count === 0) { + newCode += code.substring(lastIndex, match.index); + } + + if (match[0] === '/*') { + count++; + } else if (match[0] === '*/' && count > 0) { + count--; + } + + lastIndex = regexp.lastIndex; + } + + newCode += code.substring(lastIndex); + + return newCode; + } +} diff --git a/packages/happy-dom/src/dynamic-import/Module.ts b/packages/happy-dom/src/nodes/html-script-element/module/Module.ts similarity index 64% rename from packages/happy-dom/src/dynamic-import/Module.ts rename to packages/happy-dom/src/nodes/html-script-element/module/Module.ts index c36983a87..b3c64e13e 100644 --- a/packages/happy-dom/src/dynamic-import/Module.ts +++ b/packages/happy-dom/src/nodes/html-script-element/module/Module.ts @@ -1,12 +1,15 @@ -import BrowserWindow from '../window/BrowserWindow.js'; +import BrowserWindow from '../../../window/BrowserWindow.js'; import { URL } from 'url'; /** - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import + * Module. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import */ export default class Module { public readonly window: BrowserWindow; public readonly url: URL; + public type: 'javascript' | 'json' | 'css' = 'javascript'; public exports: { [k: string]: any } = {}; public imports: Map = new Map(); diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index ce1151ad5..5a9dd5e10 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -796,7 +796,15 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public [PropertySymbol.window]: BrowserWindow = this; public [PropertySymbol.internalId]: number = -1; public [PropertySymbol.customElementReactionStack] = new CustomElementReactionStack(this); - public [PropertySymbol.modules]: Map = new Map(); + public [PropertySymbol.modules]: { + json: Map; + css: Map; + javascript: Map; + } = { + json: new Map(), + css: new Map(), + javascript: new Map() + }; // Private properties #browserFrame: IBrowserFrame; diff --git a/packages/happy-dom/test/dynamic-import/DynamicImport.ts b/packages/happy-dom/test/dynamic-import/DynamicImport.ts deleted file mode 100644 index bae78f74f..000000000 --- a/packages/happy-dom/test/dynamic-import/DynamicImport.ts +++ /dev/null @@ -1,68 +0,0 @@ -const CODE = `import defaultExport from "module-name"; -import * as name from "module-name"; -import { export1 } from "module-name"; -import { export1 as alias1 } from "module-name"; -import { default as alias } from "module-name"; -import { export1, export2 } from "module-name"; -import { export1, export2 as alias2, /* … */ } from "module-name"; -import { "string name" as alias } from "module-name"; -import defaultExport, { export1, /* … */ } from "module-name"; -import defaultExport, * as name from "module-name"; -import "module-name"; - -// Comment -/* Comment */ -/** - *Comment - */ -const regexp = /import \/data from 'data'/gm; -export default class TestClass { - constructor() { - console.log('export const variable = \'\';'); - } -} - -if(bajs === 'export default class') { - const test = new TestClass(); - test.print("import data from 'data'"); -} - -export const variable = 'hello'; -export const variable2 = "he\"ll\"o"; -export const variable2 = \`hello\`; -export const arr = ['hello', "he\"ll\"o", \`hello\`]; - -// Exporting declarations -export let name1, name2/*, … */; // also var -export const name1 = 1, name2 = 2/*, … */; // also var, let -export function functionName() { /* … */ } -export class ClassName { /* … */ } -export function* generatorFunctionName() { /* … */ } -export const { name1, name2: bar } = o; -export const [ name1, name2 ] = array; - -// Export list -export { name1, /* …, */ nameN }; -export { variable1 as name1, variable2 as name2, /* …, */ nameN }; -export { variable1 as "string name" }; -export { name1 as default /*, … */ }; - -// Default exports -export default expression; -export default { - test -}; -export default function functionName() { /* … */ } -export default class ClassName { /* … */ } -export default function* generatorFunctionName() { /* … */ } -export default function () { /* … */ } -export default class { /* … */ } -export default function* () { /* … */ } - -// Aggregating modules -export * from "module-name"; -export * as name1 from "module-name"; -export { name1, /* …, */ nameN } from "module-name"; -export { import1 as name1, import2 as name2, /* …, */ nameN } from "module-name"; -export { default, /* …, */ } from "module-name"; -export { default as name1 } from "module-name";`; diff --git a/packages/happy-dom/test/dynamic-import/ModuleParser.test.ts b/packages/happy-dom/test/dynamic-import/ModuleParser.test.ts new file mode 100644 index 000000000..2c5d3e5bf --- /dev/null +++ b/packages/happy-dom/test/dynamic-import/ModuleParser.test.ts @@ -0,0 +1,384 @@ +import { beforeEach, describe, it, expect } from 'vitest'; +import ModuleParser from '../../src/dynamic-import/ModuleParser.js'; +import BrowserWindow from '../../src/window/BrowserWindow.js'; +import Window from '../../src/window/Window.js'; + +describe('ModuleParser', () => { + let window: BrowserWindow; + + beforeEach(() => { + window = new Window(); + }); + + describe('parse()', () => { + it('Parses imports and exports of a basic module.', () => { + const code = ` + import StringUtility from "../utilities/StringUtility.js"; + import { default as DefaultImageUtility } from "../utilities/ImageUtility.js"; + import * as NumberUtility from "../utilities/NumberUtility.js"; + + const result = await import('http://localhost:8080/js/utilities/StringUtility.js'); + + export const variable = 'hello'; + + export default class TestClass { + constructor() { + console.log('Hello World'); + } + } + `; + const parser = new ModuleParser(window); + const result = parser.parse('http://localhost:8080/js/app/main.js', code); + + expect(result.imports).toEqual([ + { url: 'http://localhost:8080/js/utilities/StringUtility.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/utilities/ImageUtility.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/utilities/NumberUtility.js', type: 'javascript' } + ]); + expect(result.code).toBe(` + const StringUtility = $happy_dom.imports.get('http://localhost:8080/js/utilities/StringUtility.js').default; + const { default: DefaultImageUtility } = $happy_dom.imports.get('http://localhost:8080/js/utilities/ImageUtility.js'); + const NumberUtility = $happy_dom.imports.get('http://localhost:8080/js/utilities/NumberUtility.js'); + + const result = await $happy_dom.dynamicImport('http://localhost:8080/js/utilities/StringUtility.js'); + + $happy_dom.exports['variable'] = 'hello'; + + $happy_dom.exports.default = class TestClass { + constructor() { + console.log('Hello World'); + } + } + `); + }); + + it('Ignores statements ending with "import" or "export".', () => { + const code = ` + testImport StringUtility from "../utilities/StringUtility.js"; + testImport { default as DefaultImageUtility } from "../utilities/ImageUtility.js"; + testImport * as NumberUtility from "../utilities/NumberUtility.js"; + + const result = await testImport('http://localhost:8080/js/utilities/StringUtility.js'); + + testExport const variable = 'hello'; + + testExport default class TestClass { + constructor() { + console.log('Hello World'); + } + } + `; + const parser = new ModuleParser(window); + const result = parser.parse('http://localhost:8080/js/app/main.js', code); + + expect(result.imports).toEqual([]); + + expect(result.code).toBe(` + testImport StringUtility from "../utilities/StringUtility.js"; + testImport { default as DefaultImageUtility } from "../utilities/ImageUtility.js"; + testImport * as NumberUtility from "../utilities/NumberUtility.js"; + + const result = await testImport('http://localhost:8080/js/utilities/StringUtility.js'); + + testExport const variable = 'hello'; + + testExport default class TestClass { + constructor() { + console.log('Hello World'); + } + } + `); + }); + + it('Handles import and export with a various combinations.', () => { + const code = ` + import defaultExport from "stuff/defaultExport.js"; + import * as name from "stuff/name.js"; + import { export1 } from "stuff/export1.js"; + import { export2 as alias1 } from "stuff/export2.js"; + import { default as alias2 } from "stuff/default.js"; + import { export3, export4 } from "stuff/export3.js"; + import { export5, export6 as alias3, /* … */ } from "stuff/export4.js"; + import { "string name" as alias } from "stuff/stringName.js"; + import defaultExport, { export7, /* … */ } from "stuff/defaultExport2.js"; + import defaultExport, * as name from "stuff/defaultExport3.js"; + import JSON from 'json/data.json' with { type: "json" }; + import CSS from '../css/data.css' with { type: "css" }; + import "../run.js"; + import { export8, + export9 } from "stuff/export5.js"; + + // Comment + /* Comment */ + /** + *Comment import data from 'data' + */ + const regexp = /import \\/data from 'data'/gm; + export default class TestClass { + constructor() { + console.log('export const variable = "\\'";'); + } + + print() { + const data = await import('data/data.json', { with: { type: 'json' } }); + console.log(data); + + const data2 = await import('data/data.js'); + console.log(data2); + } + } + + if(test === 'export default class') { + const test = new TestClass(); + test.print("import data from 'data'"); + } + + export const variable = 'hello'; + export const variable2 = "he\"ll\"o"; + export const variable3 = \`export const variable = 'hello';\`; + export const arr = ['hello', "he\"ll\"o", \`hello\`]; + + // Exporting declarations + export let name1, name2; // also var + export const name3 = 1, name4 = 2; // also var, let + export function functionName() { /* … */ } + export class ClassName { /* … */ } + export function* generatorFunctionName() { /* … */ } + export const { name5, name6: bar } = o; + export const [ name7, name8 ] = array; + + // Export list + export { name9, name10 /* , !*/ }; + export { variable1 as name11, variable2 as name12, nameN }; + export { variable1 as "string name" }; + export { name1 as default }; + + // Aggregating modules + export * from "../aggregated1.js"; + export * as name1 from "../aggregated2.js"; + export { name1, /* …, */ nameN } from "../aggregated3.js"; + export { import1 as name1, import2 as name2, /* …, */ nameN } from "../aggregated4.js"; + export { default, /* …, */ } from "../aggregated5.js"; + export { default as name1 } from "../aggregated6.js"; + `; + + const parser = new ModuleParser(window); + const result = parser.parse('http://localhost:8080/js/app/main.js', code); + + expect(result.imports).toEqual([ + { url: 'http://localhost:8080/js/app/stuff/defaultExport.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/app/stuff/name.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/app/stuff/export1.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/app/stuff/export2.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/app/stuff/default.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/app/stuff/export3.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/app/stuff/export4.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/app/stuff/stringName.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/app/stuff/defaultExport2.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/app/stuff/defaultExport3.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/app/json/data.json', type: 'json' }, + { url: 'http://localhost:8080/js/css/data.css', type: 'css' }, + { url: 'http://localhost:8080/js/run.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/app/stuff/export5.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/aggregated1.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/aggregated2.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/aggregated3.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/aggregated4.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/aggregated5.js', type: 'javascript' }, + { url: 'http://localhost:8080/js/aggregated6.js', type: 'javascript' } + ]); + + expect(result.code).toBe(` + const defaultExport = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/defaultExport.js').default; + const name = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/name.js'); + const { export1 } = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/export1.js'); + const { export2: alias1 } = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/export2.js'); + const { default: alias2 } = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/default.js'); + const { export3, export4 } = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/export3.js'); + const { export5, export6: alias3, /* … */ } = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/export4.js'); + const { "string name": alias } = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/stringName.js'); + const defaultExport = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/defaultExport2.js').default; +const { export7, /* … */ } = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/defaultExport2.js'); + const defaultExport = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/defaultExport3.js').default; +const name = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/defaultExport3.js'); + const JSON = $happy_dom.imports.get('http://localhost:8080/js/app/json/data.json').default; + const CSS = $happy_dom.imports.get('http://localhost:8080/js/css/data.css').default; + + const { export8, + export9 } = $happy_dom.imports.get('http://localhost:8080/js/app/stuff/export5.js'); + + // Comment + /* Comment */ + /** + *Comment import data from 'data' + */ + const regexp = /import \\/data from 'data'/gm; + $happy_dom.exports.default = class TestClass { + constructor() { + console.log('export const variable = "\\'";'); + } + + print() { + const data = await $happy_dom.dynamicImport('data/data.json', { with: { type: 'json' } }); + console.log(data); + + const data2 = await $happy_dom.dynamicImport('data/data.js'); + console.log(data2); + } + } + + if(test === 'export default class') { + const test = new TestClass(); + test.print("import data from 'data'"); + } + + $happy_dom.exports['variable'] = 'hello'; + $happy_dom.exports['variable2'] = "he"ll"o"; + $happy_dom.exports['variable3'] = \`export const variable = 'hello';\`; + $happy_dom.exports['arr'] = ['hello', "he"ll"o", \`hello\`]; + + // Exporting declarations + /*Unknown export: export let name1, name2;*/ // also var + $happy_dom.exports['name3'] = 1, name4 = 2; // also var, let + $happy_dom.exports['functionName'] = function functionName() { /* … */ } + $happy_dom.exports['ClassName'] = class ClassName { /* … */ } + $happy_dom.exports['generatorFunctionName'] = function* generatorFunctionName() { /* … */ } + const $happy_dom_export_0 = o; + const $happy_dom_export_1 = array; + + // Export list + $happy_dom.exports['name9'] = name9; +$happy_dom.exports['name10'] = name10; + $happy_dom.exports['name11'] = variable1; +$happy_dom.exports['name12'] = variable2; +$happy_dom.exports['nameN'] = nameN; + $happy_dom.exports['string name'] = variable1; + $happy_dom.exports['default'] = name1; + + // Aggregating modules + Object.assign($happy_dom.exports, $happy_dom.imports.get('http://localhost:8080/js/aggregated1.js')); + $happy_dom.exports['name1'] = $happy_dom.imports.get('http://localhost:8080/js/aggregated2.js'); + $happy_dom.exports['name1'] = $happy_dom.imports.get('http://localhost:8080/js/aggregated3.js')['name1']; +$happy_dom.exports['nameN'] = $happy_dom.imports.get('http://localhost:8080/js/aggregated3.js')['nameN']; + $happy_dom.exports['name1'] = $happy_dom.imports.get('http://localhost:8080/js/aggregated4.js')['import1']; +$happy_dom.exports['name2'] = $happy_dom.imports.get('http://localhost:8080/js/aggregated4.js')['import2']; +$happy_dom.exports['nameN'] = $happy_dom.imports.get('http://localhost:8080/js/aggregated4.js')['nameN']; + $happy_dom.exports['default'] = $happy_dom.imports.get('http://localhost:8080/js/aggregated5.js')['default']; + $happy_dom.exports['name1'] = $happy_dom.imports.get('http://localhost:8080/js/aggregated6.js')['default']; + + +$happy_dom.exports['name5'] = $happy_dom_export_0['name5']; +$happy_dom.exports['bar'] = $happy_dom_export_0['name6']; +$happy_dom.exports['name7'] = $happy_dom_export_1['name7']; +$happy_dom.exports['name8'] = $happy_dom_export_1['name8']; +`); + }); + + it('Handles export default function.', () => { + const code = ` + export const variable = /my-regexp/; + export default function () { + console.log('Hello World'); + } + `; + + const parser = new ModuleParser(window); + const result = parser.parse('http://localhost:8080/js/app/main.js', code); + + expect(result.imports).toEqual([]); + + expect(result.code).toBe(` + $happy_dom.exports['variable'] = /my-regexp/; + $happy_dom.exports.default = function () { + console.log('Hello World'); + } + `); + }); + + it('Handles export default class.', () => { + const code = ` + export default class TestClass { + constructor() { + console.log('Hello World'); + } + } + `; + + const parser = new ModuleParser(window); + const result = parser.parse('http://localhost:8080/js/app/main.js', code); + + expect(result.imports).toEqual([]); + + expect(result.code).toBe(` + $happy_dom.exports.default = class TestClass { + constructor() { + console.log('Hello World'); + } + } + `); + }); + + it('Handles export default generator function.', () => { + const code = ` + export default function* () { + yield i; + yield i + 10; + } + `; + + const parser = new ModuleParser(window); + const result = parser.parse('http://localhost:8080/js/app/main.js', code); + + expect(result.imports).toEqual([]); + + expect(result.code).toBe(` + $happy_dom.exports.default = function* () { + yield i; + yield i + 10; + } + `); + }); + + it('Handles export default object.', () => { + const code = ` + export default { + test: 'test' + }; + `; + + const parser = new ModuleParser(window); + const result = parser.parse('http://localhost:8080/js/app/main.js', code); + + expect(result.imports).toEqual([]); + + expect(result.code).toBe(` + $happy_dom.exports.default = { + test: 'test' + }; + `); + }); + + it('Handles export default expression.', () => { + const code = ` + export default (function () { + return { + test: 'test' + } + })(); + `; + + const parser = new ModuleParser(window); + const result = parser.parse('http://localhost:8080/js/app/main.js', code); + + expect(result.imports).toEqual([]); + + expect(result.code).toBe(` + $happy_dom.exports.default = (function () { + return { + test: 'test' + } + })(); + `); + }); + }); +});