/
githubmirror
/
webpack
Обзор
Документация
Войти
/
githubmirror
/
webpack
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
v5.105.3
test/helpers/FakeDocument.js
563 строки
13 KB
Xiao
fix: prevent empty JS file generation for CSS-only entry points (#20454)
16 фев 2026, 16:15
Не верифицирован
16 фев 2026, 16:15
6dec682
Код
Авторство
О чём код?
"use strict"; const fs = require("fs"); const path = require("path"); /** * @this {FakeDocument} * @param {string} property property * @returns {EXPECTED_ANY} value */ function getPropertyValue(property) { return this[property]; } class FakeDocument { constructor(basePath) { this.head = this.createElement("head"); this.body = this.createElement("body"); this.baseURI = "https://test.cases/path/index.html"; this._elementsByTagName = new Map([ ["head", [this.head]], ["body", [this.body]] ]); this._basePath = basePath; } createElement(type) { return new FakeElement(this, type, this._basePath); } _onElementAttached(element) { const type = element._type; let list = this._elementsByTagName.get(type); if (list === undefined) { list = []; this._elementsByTagName.set(type, list); } list.push(element); } _onElementRemoved(element) { const type = element._type; const list = this._elementsByTagName.get(type); const idx = list.indexOf(element); list.splice(idx, 1); } getElementsByTagName(name) { return this._elementsByTagName.get(name) || []; } querySelectorAll(selector) { // Simple selector support for common cases // Tag selector: "link", "script", etc. if (/^[a-zA-Z][a-zA-Z0-9-]*$/.test(selector)) { return this.getElementsByTagName(selector); } // Class selector: ".class" if (selector.startsWith(".")) { const className = selector.slice(1); const allElements = []; for (const elements of this._elementsByTagName.values()) { for (const element of elements) { if (element.getAttribute("class") === className) { allElements.push(element); } } } return allElements; } // ID selector: "#id" if (selector.startsWith("#")) { const id = selector.slice(1); for (const elements of this._elementsByTagName.values()) { for (const element of elements) { if (element.getAttribute("id") === id) { return [element]; } } } return []; } // Attribute selector: "[attr]", "[attr=value]" if (selector.startsWith("[") && selector.endsWith("]")) { const attrSelector = selector.slice(1, -1); const allElements = []; if (attrSelector.includes("=")) { const [attr, value] = attrSelector .split("=") .map((s) => s.trim().replace(/^["']|["']$/g, "")); for (const elements of this._elementsByTagName.values()) { for (const element of elements) { if (element.getAttribute(attr) === value) { allElements.push(element); } } } } else { for (const elements of this._elementsByTagName.values()) { for (const element of elements) { if (element.getAttribute(attrSelector) !== undefined) { allElements.push(element); } } } } return allElements; } // Default: return empty array for unsupported selectors return []; } getComputedStyle(element) { const style = { getPropertyValue }; const links = this.getElementsByTagName("link"); for (const link of links) { for (const rule of link.sheet.cssRules) { if (rule.selectorText === element._type) { Object.assign(style, rule.style); } } } return style; } } class FakeElement { constructor(document, type, basePath) { this._document = document; this._type = type; this._children = []; this._attributes = Object.create(null); this._src = undefined; this._href = undefined; this.parentNode = undefined; this.sheet = type === "link" ? new FakeSheet(this, basePath) : undefined; } _attach(node) { this._document._onElementAttached(node); this._children.push(node); node.parentNode = this; } _load(node) { if (node._type === "link") { const timer = setTimeout(() => { clearTimeout(timer); const loadEvent = { type: "load", target: node }; if (node.onload) node.onload(loadEvent); node._dispatchEvent(loadEvent); }, 100); } else if (node._type === "script" && this._document.onScript) { Promise.resolve().then(() => { this._document.onScript(node.src); }); } } insertBefore(node) { this._attach(node); this._load(node); } appendChild(node) { this._attach(node); this._load(node); } removeChild(node) { const idx = this._children.indexOf(node); if (idx >= 0) { this._children.splice(idx, 1); this._document._onElementRemoved(node); node.parentNode = undefined; } } setAttribute(name, value) { if (this._type === "link" && name === "href") { this.href(value); } else { this._attributes[name] = value; } } removeAttribute(name) { delete this._attributes[name]; } getAttribute(name) { if (this._type === "link" && name === "href") { return this.href; } return this._attributes[name]; } _toRealUrl(value) { if (/^\//.test(value)) { return `https://test.cases${value}`; } else if (/^\.\.\//.test(value)) { return `https://test.cases${value.slice(2)}`; } else if (/^\.\//.test(value)) { return `https://test.cases/path${value.slice(1)}`; } else if (/^\w+:\/\//.test(value)) { return value; } else if (/^\/\//.test(value)) { return `https:${value}`; } return `https://test.cases/path/${value}`; } set src(value) { if (this._type === "script") { this._src = this._toRealUrl(value); } } get src() { return this._src; } set href(value) { if (this._type === "link") { this._href = this._toRealUrl(value); try { this.sheet._css = this.sheet.css; this.sheet._cssRules = this.sheet.cssRules; } catch (_error) { // Ignore error } } } get href() { return this._href; } get rel() { if (this._type === "link") { return this._attributes.rel || "stylesheet"; } return this._attributes.rel; } set rel(value) { this._attributes.rel = value; } addEventListener(event, handler) { if (!this._eventListeners) { this._eventListeners = new Map(); } if (!this._eventListeners.has(event)) { this._eventListeners.set(event, []); } this._eventListeners.get(event).push(handler); } removeEventListener(event, handler) { if (!this._eventListeners) return; const handlers = this._eventListeners.get(event); if (!handlers) return; const index = handlers.indexOf(handler); if (index >= 0) { handlers.splice(index, 1); } } _dispatchEvent(event) { if (!this._eventListeners) return; const handlers = this._eventListeners.get(event.type); if (handlers) { for (const handler of handlers) { handler(event); } } } cloneNode(deep = false) { const cloned = new FakeElement( this._document, this._type, this._document._basePath ); // Copy attributes cloned._attributes = { ...this._attributes }; // Copy src and href cloned._src = this._src; cloned._href = this._href; // For link elements, create a new sheet with the same href if (this._type === "link" && this.sheet) { cloned.sheet = new FakeSheet(cloned, this._document._basePath); if (this._href) { cloned.href = this._href; } } // Copy event handlers if they exist if (this.onload) { cloned.onload = this.onload; } // Copy event listeners if (this._eventListeners) { cloned._eventListeners = new Map(); for (const [event, handlers] of this._eventListeners.entries()) { cloned._eventListeners.set(event, [...handlers]); } } // Deep clone children if requested if (deep) { for (const child of this._children) { const clonedChild = child.cloneNode(true); cloned.appendChild(clonedChild); } } return cloned; } } class FakeSheet { constructor(element, basePath) { this._element = element; this._basePath = basePath; // We cannot lazily load file content in getter because in HMR scenarios, // the file path will have ?hmr=timestamp appended. If we load lazily, // we can only get the latest file content, and the previous content will be lost. this._css = undefined; this._cssRules = undefined; } get css() { if (this._css) return this._css; let css = fs.readFileSync( path.resolve( this._basePath, this._element.href .replace(/^https:\/\/test\.cases\/path\//, "") .replace(/^https:\/\/example\.com\//, "") .split("?")[0] // Remove query parameters (e.g., ?hmr=timestamp) ), "utf8" ); css = css.replace(/@import url\("([^"]+)"\);/g, (match, url) => { if (!/^https:\/\/test\.cases\/path\//.test(url)) { return `@import url("${url}");`; } if (url.startsWith("#")) { return url; } return fs.readFileSync( path.resolve( this._basePath, url.replace(/^https:\/\/test\.cases\/path\//, "") ), "utf8" ); }); return css; } get cssRules() { if (this._cssRules) return this._cssRules; const walkCssTokens = require("../../lib/css/walkCssTokens"); const rules = []; let currentRule = { getPropertyValue }; let selector; let last = 0; let ruleStart = 0; // Track the start of the current rule const processDeclaration = (str) => { const colon = str.indexOf(":"); if (colon > 0) { const property = str.slice(0, colon).trim(); const value = str.slice(colon + 1); currentRule[property] = value; } }; const href = this._element.href.split("?")[0]; // Remove query parameters (e.g., ?hmr=timestamp) const filepath = /file:\/\//.test(href) ? new URL(href) : path.resolve( this._basePath, href .replace(/^https:\/\/test\.cases\/path\//, "") .replace(/^https:\/\/example\.com\/public\/path\//, "") .replace(/^https:\/\/example\.com\//, "") ); let css = fs.readFileSync(filepath, "utf8"); css = css // Remove comments // @ts-expect-error we use es2018 for such tests .replace(/\/\*.*?\*\//gs, "") .replace(/@import url\("([^"]+)"\);/g, (match, url) => { if (!/^https:\/\/test\.cases\/path\//.test(url)) { return url; } if (url.startsWith("#")) { return url; } return fs.readFileSync( path.resolve( this._basePath, url.replace(/^https:\/\/test\.cases\/path\//, "") ), "utf8" ); }); walkCssTokens(css, 0, { leftCurlyBracket(source, start, end) { if (selector === undefined) { ruleStart = last; // Record the start of the rule (before the selector) selector = source.slice(last, start).trim(); last = end; } return end; }, rightCurlyBracket(source, start, end) { processDeclaration(source.slice(last, start)); rules.push({ selectorText: selector, style: currentRule, cssText: source.slice(ruleStart, end).trim() }); selector = undefined; currentRule = { getPropertyValue }; last = end; return end; }, semicolon(source, start, end) { processDeclaration(source.slice(last, start)); last = end; return end; } }); return rules; } } /** * Fake CSSStyleSheet implementation for testing * Supports the Constructable Stylesheets API */ class CSSStyleSheet { constructor() { this._cssText = ""; this._cssRules = []; } /** * Synchronously replace the stylesheet content * @param {string} cssText CSS text content */ replaceSync(cssText) { this._cssText = cssText; this._parseCssRules(cssText); } /** * Asynchronously replace the stylesheet content * @param {string} cssText CSS text content * @returns {Promise<CSSStyleSheet>} Promise that resolves to this stylesheet */ replace(cssText) { return Promise.resolve().then(() => { this.replaceSync(cssText); return this; }); } /** * Get the parsed CSS rules * @returns {Array} Array of CSS rules */ get cssRules() { return this._cssRules; } /** * Parse CSS text into rules * @param {string} cssText CSS text to parse */ _parseCssRules(cssText) { const walkCssTokens = require("../../lib/css/walkCssTokens"); const rules = []; let currentRule = { getPropertyValue }; let selector; let last = 0; const processDeclaration = (str) => { const colon = str.indexOf(":"); if (colon > 0) { const property = str.slice(0, colon).trim(); const value = str.slice(colon + 1).trim(); currentRule[property] = value; } }; // Remove comments const cleanCss = cssText // @ts-expect-error we use es2018 for such tests .replace(/\/\*.*?\*\//gs, ""); let ruleStart = 0; walkCssTokens(cleanCss, 0, { leftCurlyBracket(source, start, end) { if (selector === undefined) { selector = source.slice(last, start).trim(); ruleStart = last; last = end; } return end; }, rightCurlyBracket(source, start, end) { processDeclaration(source.slice(last, start)); last = end; // Generate cssText for the entire rule const ruleText = cleanCss.slice(ruleStart, end).trim(); const cssText = `${selector} ${ruleText.slice(ruleText.indexOf("{"))}`; rules.push({ selectorText: selector, style: currentRule, cssText }); selector = undefined; currentRule = { getPropertyValue }; return end; }, semicolon(source, start, end) { processDeclaration(source.slice(last, start)); last = end; return end; } }); this._cssRules = rules; } } FakeDocument.FakeSheet = FakeSheet; FakeDocument.FakeElement = FakeDocument; FakeDocument.CSSStyleSheet = CSSStyleSheet; module.exports = FakeDocument;