From 3ed754d6c55b7c4c9bb0430474ea4827e1f12af8 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Thu, 5 Dec 2024 18:37:36 +0300 Subject: [PATCH 1/9] fix: speed up initial client bundling --- .cspell.json | 1 + client-src/overlay.js | 382 ++++++++++++++++++++++++++-- client-src/overlay/fsm.js | 64 ----- client-src/overlay/runtime-error.js | 50 ---- client-src/overlay/state-machine.js | 104 -------- client-src/overlay/styles.js | 89 ------- client-src/webpack.config.js | 3 - package.json | 1 - 8 files changed, 365 insertions(+), 329 deletions(-) delete mode 100644 client-src/overlay/fsm.js delete mode 100644 client-src/overlay/runtime-error.js delete mode 100644 client-src/overlay/state-machine.js delete mode 100644 client-src/overlay/styles.js diff --git a/.cspell.json b/.cspell.json index 75ccb46d8d..817c0ee483 100644 --- a/.cspell.json +++ b/.cspell.json @@ -2,6 +2,7 @@ "version": "0.2", "language": "en,en-gb", "words": [ + "apos", "camelcase", "tapable", "sockjs", diff --git a/client-src/overlay.js b/client-src/overlay.js index 04670df2c0..8443fd8ced 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -2,22 +2,368 @@ // They, in turn, got inspired by webpack-hot-middleware (https://github.com/glenjamin/webpack-hot-middleware). import ansiHTML from "ansi-html-community"; -import { encode } from "html-entities"; -import { - listenToRuntimeError, - listenToUnhandledRejection, - parseErrorToStacks, -} from "./overlay/runtime-error.js"; -import createOverlayMachine from "./overlay/state-machine.js"; -import { - containerStyle, - dismissButtonStyle, - headerStyle, - iframeStyle, - msgStyles, - msgTextStyle, - msgTypeStyle, -} from "./overlay/styles.js"; + +/** + * @type {(input: string, position: number) => string} + */ +const getCodePoint = String.prototype.codePointAt + ? (input, position) => input.codePointAt(position) + : (input, position) => + (input.charCodeAt(position) - 0xd800) * 0x400 + + input.charCodeAt(position + 1) - + 0xdc00 + + 0x10000; + +/** + * @param {string} macroText + * @param {RegExp} macroRegExp + * @param {(input: string) => string} macroReplacer + * @returns {string} + */ +const replaceUsingRegExp = (macroText, macroRegExp, macroReplacer) => { + macroRegExp.lastIndex = 0; + let replaceMatch = macroRegExp.exec(macroText); + let replaceResult; + if (replaceMatch) { + replaceResult = ""; + let replaceLastIndex = 0; + do { + if (replaceLastIndex !== replaceMatch.index) { + replaceResult += macroText.substring( + replaceLastIndex, + replaceMatch.index, + ); + } + const replaceInput = replaceMatch[0]; + replaceResult += macroReplacer(replaceInput); + replaceLastIndex = replaceMatch.index + replaceInput.length; + // eslint-disable-next-line no-cond-assign + } while ((replaceMatch = macroRegExp.exec(macroText))); + + if (replaceLastIndex !== macroText.length) { + replaceResult += macroText.substring(replaceLastIndex); + } + } else { + replaceResult = macroText; + } + return replaceResult; +}; + +const references = { + "<": "<", + ">": ">", + '"': """, + "'": "'", + "&": "&", +}; + +/** + * @param {string} text text + * @returns {string} + */ +function encode(text) { + if (!text) { + return ""; + } + + return replaceUsingRegExp(text, /[<>'"&]/g, (input) => { + let result = references[input]; + if (!result) { + const code = + input.length > 1 ? getCodePoint(input, 0) : input.charCodeAt(0); + result = `&#${code};`; + } + return result; + }); +} + +/** + * @typedef {Object} StateDefinitions + * @property {{[event: string]: { target: string; actions?: Array }}} [on] + */ + +/** + * @typedef {Object} Options + * @property {{[state: string]: StateDefinitions}} states + * @property {object} context; + * @property {string} initial + */ + +/** + * @typedef {Object} Implementation + * @property {{[actionName: string]: (ctx: object, event: any) => object}} actions + */ + +/** + * A simplified `createMachine` from `@xstate/fsm` with the following differences: + * + * - the returned machine is technically a "service". No `interpret(machine).start()` is needed. + * - the state definition only support `on` and target must be declared with { target: 'nextState', actions: [] } explicitly. + * - event passed to `send` must be an object with `type` property. + * - actions implementation will be [assign action](https://xstate.js.org/docs/guides/context.html#assign-action) if you return any value. + * Do not return anything if you just want to invoke side effect. + * + * The goal of this custom function is to avoid installing the entire `'xstate/fsm'` package, while enabling modeling using + * state machine. You can copy the first parameter into the editor at https://stately.ai/viz to visualize the state machine. + * + * @param {Options} options + * @param {Implementation} implementation + */ +function createMachine({ states, context, initial }, { actions }) { + let currentState = initial; + let currentContext = context; + + return { + send: (event) => { + const currentStateOn = states[currentState].on; + const transitionConfig = currentStateOn && currentStateOn[event.type]; + + if (transitionConfig) { + currentState = transitionConfig.target; + if (transitionConfig.actions) { + transitionConfig.actions.forEach((actName) => { + const actionImpl = actions[actName]; + + const nextContextValue = + actionImpl && actionImpl(currentContext, event); + + if (nextContextValue) { + currentContext = { + ...currentContext, + ...nextContextValue, + }; + } + }); + } + } + }, + }; +} + +/** + * @typedef {Object} ShowOverlayData + * @property {'warning' | 'error'} level + * @property {Array} messages + * @property {'build' | 'runtime'} messageSource + */ + +/** + * @typedef {Object} CreateOverlayMachineOptions + * @property {(data: ShowOverlayData) => void} showOverlay + * @property {() => void} hideOverlay + */ + +/** + * @param {CreateOverlayMachineOptions} options + */ +const createOverlayMachine = (options) => { + const { hideOverlay, showOverlay } = options; + + return createMachine( + { + initial: "hidden", + context: { + level: "error", + messages: [], + messageSource: "build", + }, + states: { + hidden: { + on: { + BUILD_ERROR: { + target: "displayBuildError", + actions: ["setMessages", "showOverlay"], + }, + RUNTIME_ERROR: { + target: "displayRuntimeError", + actions: ["setMessages", "showOverlay"], + }, + }, + }, + displayBuildError: { + on: { + DISMISS: { + target: "hidden", + actions: ["dismissMessages", "hideOverlay"], + }, + BUILD_ERROR: { + target: "displayBuildError", + actions: ["appendMessages", "showOverlay"], + }, + }, + }, + displayRuntimeError: { + on: { + DISMISS: { + target: "hidden", + actions: ["dismissMessages", "hideOverlay"], + }, + RUNTIME_ERROR: { + target: "displayRuntimeError", + actions: ["appendMessages", "showOverlay"], + }, + BUILD_ERROR: { + target: "displayBuildError", + actions: ["setMessages", "showOverlay"], + }, + }, + }, + }, + }, + { + actions: { + dismissMessages: () => { + return { + messages: [], + level: "error", + messageSource: "build", + }; + }, + appendMessages: (context, event) => { + return { + messages: context.messages.concat(event.messages), + level: event.level || context.level, + messageSource: event.type === "RUNTIME_ERROR" ? "runtime" : "build", + }; + }, + setMessages: (context, event) => { + return { + messages: event.messages, + level: event.level || context.level, + messageSource: event.type === "RUNTIME_ERROR" ? "runtime" : "build", + }; + }, + hideOverlay, + showOverlay, + }, + }, + ); +}; + +/** + * + * @param {Error} error + */ +const parseErrorToStacks = (error) => { + if (!error || !(error instanceof Error)) { + throw new Error(`parseErrorToStacks expects Error object`); + } + if (typeof error.stack === "string") { + return error.stack + .split("\n") + .filter((stack) => stack !== `Error: ${error.message}`); + } +}; + +/** + * @callback ErrorCallback + * @param {ErrorEvent} error + * @returns {void} + */ + +/** + * @param {ErrorCallback} callback + */ +const listenToRuntimeError = (callback) => { + window.addEventListener("error", callback); + + return function cleanup() { + window.removeEventListener("error", callback); + }; +}; + +/** + * @callback UnhandledRejectionCallback + * @param {PromiseRejectionEvent} rejectionEvent + * @returns {void} + */ + +/** + * @param {UnhandledRejectionCallback} callback + */ +const listenToUnhandledRejection = (callback) => { + window.addEventListener("unhandledrejection", callback); + + return function cleanup() { + window.removeEventListener("unhandledrejection", callback); + }; +}; + +// Styles are inspired by `react-error-overlay` + +const msgStyles = { + error: { + backgroundColor: "rgba(206, 17, 38, 0.1)", + color: "#fccfcf", + }, + warning: { + backgroundColor: "rgba(251, 245, 180, 0.1)", + color: "#fbf5b4", + }, +}; +const iframeStyle = { + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + width: "100vw", + height: "100vh", + border: "none", + "z-index": 9999999999, +}; +const containerStyle = { + position: "fixed", + boxSizing: "border-box", + left: 0, + top: 0, + right: 0, + bottom: 0, + width: "100vw", + height: "100vh", + fontSize: "large", + padding: "2rem 2rem 4rem 2rem", + lineHeight: "1.2", + whiteSpace: "pre-wrap", + overflow: "auto", + backgroundColor: "rgba(0, 0, 0, 0.9)", + color: "white", +}; +const headerStyle = { + color: "#e83b46", + fontSize: "2em", + whiteSpace: "pre-wrap", + fontFamily: "sans-serif", + margin: "0 2rem 2rem 0", + flex: "0 0 auto", + maxHeight: "50%", + overflow: "auto", +}; +const dismissButtonStyle = { + color: "#ffffff", + lineHeight: "1rem", + fontSize: "1.5rem", + padding: "1rem", + cursor: "pointer", + position: "absolute", + right: 0, + top: 0, + backgroundColor: "transparent", + border: "none", +}; +const msgTypeStyle = { + color: "#e83b46", + fontSize: "1.2em", + marginBottom: "1rem", + fontFamily: "sans-serif", +}; +const msgTextStyle = { + lineHeight: "1.5", + fontSize: "1rem", + fontFamily: "Menlo, Consolas, monospace", +}; + +// ANSI HTML const colors = { reset: ["transparent", "transparent"], @@ -39,7 +385,7 @@ ansiHTML.setColors(colors); * @param {string | { file?: string, moduleName?: string, loc?: string, message?: string; stack?: string[] }} item * @returns {{ header: string, body: string }} */ -function formatProblem(type, item) { +const formatProblem = (type, item) => { let header = type === "warning" ? "WARNING" : "ERROR"; let body = ""; @@ -74,7 +420,7 @@ function formatProblem(type, item) { } return { header, body }; -} +}; /** * @typedef {Object} CreateOverlayOptions diff --git a/client-src/overlay/fsm.js b/client-src/overlay/fsm.js deleted file mode 100644 index 78020e97aa..0000000000 --- a/client-src/overlay/fsm.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @typedef {Object} StateDefinitions - * @property {{[event: string]: { target: string; actions?: Array }}} [on] - */ - -/** - * @typedef {Object} Options - * @property {{[state: string]: StateDefinitions}} states - * @property {object} context; - * @property {string} initial - */ - -/** - * @typedef {Object} Implementation - * @property {{[actionName: string]: (ctx: object, event: any) => object}} actions - */ - -/** - * A simplified `createMachine` from `@xstate/fsm` with the following differences: - * - * - the returned machine is technically a "service". No `interpret(machine).start()` is needed. - * - the state definition only support `on` and target must be declared with { target: 'nextState', actions: [] } explicitly. - * - event passed to `send` must be an object with `type` property. - * - actions implementation will be [assign action](https://xstate.js.org/docs/guides/context.html#assign-action) if you return any value. - * Do not return anything if you just want to invoke side effect. - * - * The goal of this custom function is to avoid installing the entire `'xstate/fsm'` package, while enabling modeling using - * state machine. You can copy the first parameter into the editor at https://stately.ai/viz to visualize the state machine. - * - * @param {Options} options - * @param {Implementation} implementation - */ -function createMachine({ states, context, initial }, { actions }) { - let currentState = initial; - let currentContext = context; - - return { - send: (event) => { - const currentStateOn = states[currentState].on; - const transitionConfig = currentStateOn && currentStateOn[event.type]; - - if (transitionConfig) { - currentState = transitionConfig.target; - if (transitionConfig.actions) { - transitionConfig.actions.forEach((actName) => { - const actionImpl = actions[actName]; - - const nextContextValue = - actionImpl && actionImpl(currentContext, event); - - if (nextContextValue) { - currentContext = { - ...currentContext, - ...nextContextValue, - }; - } - }); - } - } - }, - }; -} - -export default createMachine; diff --git a/client-src/overlay/runtime-error.js b/client-src/overlay/runtime-error.js deleted file mode 100644 index 756afc845f..0000000000 --- a/client-src/overlay/runtime-error.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * - * @param {Error} error - */ -function parseErrorToStacks(error) { - if (!error || !(error instanceof Error)) { - throw new Error(`parseErrorToStacks expects Error object`); - } - if (typeof error.stack === "string") { - return error.stack - .split("\n") - .filter((stack) => stack !== `Error: ${error.message}`); - } -} - -/** - * @callback ErrorCallback - * @param {ErrorEvent} error - * @returns {void} - */ - -/** - * @param {ErrorCallback} callback - */ -function listenToRuntimeError(callback) { - window.addEventListener("error", callback); - - return function cleanup() { - window.removeEventListener("error", callback); - }; -} - -/** - * @callback UnhandledRejectionCallback - * @param {PromiseRejectionEvent} rejectionEvent - * @returns {void} - */ - -/** - * @param {UnhandledRejectionCallback} callback - */ -function listenToUnhandledRejection(callback) { - window.addEventListener("unhandledrejection", callback); - - return function cleanup() { - window.removeEventListener("unhandledrejection", callback); - }; -} - -export { listenToRuntimeError, listenToUnhandledRejection, parseErrorToStacks }; diff --git a/client-src/overlay/state-machine.js b/client-src/overlay/state-machine.js deleted file mode 100644 index 4b15c6cd35..0000000000 --- a/client-src/overlay/state-machine.js +++ /dev/null @@ -1,104 +0,0 @@ -import createMachine from "./fsm.js"; - -/** - * @typedef {Object} ShowOverlayData - * @property {'warning' | 'error'} level - * @property {Array} messages - * @property {'build' | 'runtime'} messageSource - */ - -/** - * @typedef {Object} CreateOverlayMachineOptions - * @property {(data: ShowOverlayData) => void} showOverlay - * @property {() => void} hideOverlay - */ - -/** - * @param {CreateOverlayMachineOptions} options - */ -const createOverlayMachine = (options) => { - const { hideOverlay, showOverlay } = options; - const overlayMachine = createMachine( - { - initial: "hidden", - context: { - level: "error", - messages: [], - messageSource: "build", - }, - states: { - hidden: { - on: { - BUILD_ERROR: { - target: "displayBuildError", - actions: ["setMessages", "showOverlay"], - }, - RUNTIME_ERROR: { - target: "displayRuntimeError", - actions: ["setMessages", "showOverlay"], - }, - }, - }, - displayBuildError: { - on: { - DISMISS: { - target: "hidden", - actions: ["dismissMessages", "hideOverlay"], - }, - BUILD_ERROR: { - target: "displayBuildError", - actions: ["appendMessages", "showOverlay"], - }, - }, - }, - displayRuntimeError: { - on: { - DISMISS: { - target: "hidden", - actions: ["dismissMessages", "hideOverlay"], - }, - RUNTIME_ERROR: { - target: "displayRuntimeError", - actions: ["appendMessages", "showOverlay"], - }, - BUILD_ERROR: { - target: "displayBuildError", - actions: ["setMessages", "showOverlay"], - }, - }, - }, - }, - }, - { - actions: { - dismissMessages: () => { - return { - messages: [], - level: "error", - messageSource: "build", - }; - }, - appendMessages: (context, event) => { - return { - messages: context.messages.concat(event.messages), - level: event.level || context.level, - messageSource: event.type === "RUNTIME_ERROR" ? "runtime" : "build", - }; - }, - setMessages: (context, event) => { - return { - messages: event.messages, - level: event.level || context.level, - messageSource: event.type === "RUNTIME_ERROR" ? "runtime" : "build", - }; - }, - hideOverlay, - showOverlay, - }, - }, - ); - - return overlayMachine; -}; - -export default createOverlayMachine; diff --git a/client-src/overlay/styles.js b/client-src/overlay/styles.js deleted file mode 100644 index 4786c1b62e..0000000000 --- a/client-src/overlay/styles.js +++ /dev/null @@ -1,89 +0,0 @@ -// styles are inspired by `react-error-overlay` - -const msgStyles = { - error: { - backgroundColor: "rgba(206, 17, 38, 0.1)", - color: "#fccfcf", - }, - warning: { - backgroundColor: "rgba(251, 245, 180, 0.1)", - color: "#fbf5b4", - }, -}; - -const iframeStyle = { - position: "fixed", - top: 0, - left: 0, - right: 0, - bottom: 0, - width: "100vw", - height: "100vh", - border: "none", - "z-index": 9999999999, -}; - -const containerStyle = { - position: "fixed", - boxSizing: "border-box", - left: 0, - top: 0, - right: 0, - bottom: 0, - width: "100vw", - height: "100vh", - fontSize: "large", - padding: "2rem 2rem 4rem 2rem", - lineHeight: "1.2", - whiteSpace: "pre-wrap", - overflow: "auto", - backgroundColor: "rgba(0, 0, 0, 0.9)", - color: "white", -}; - -const headerStyle = { - color: "#e83b46", - fontSize: "2em", - whiteSpace: "pre-wrap", - fontFamily: "sans-serif", - margin: "0 2rem 2rem 0", - flex: "0 0 auto", - maxHeight: "50%", - overflow: "auto", -}; - -const dismissButtonStyle = { - color: "#ffffff", - lineHeight: "1rem", - fontSize: "1.5rem", - padding: "1rem", - cursor: "pointer", - position: "absolute", - right: 0, - top: 0, - backgroundColor: "transparent", - border: "none", -}; - -const msgTypeStyle = { - color: "#e83b46", - fontSize: "1.2em", - marginBottom: "1rem", - fontFamily: "sans-serif", -}; - -const msgTextStyle = { - lineHeight: "1.5", - fontSize: "1rem", - fontFamily: "Menlo, Consolas, monospace", -}; - -export { - msgStyles, - iframeStyle, - containerStyle, - headerStyle, - dismissButtonStyle, - msgTypeStyle, - msgTextStyle, -}; diff --git a/client-src/webpack.config.js b/client-src/webpack.config.js index 3e44675239..47f7155213 100644 --- a/client-src/webpack.config.js +++ b/client-src/webpack.config.js @@ -44,7 +44,6 @@ module.exports = [ merge(baseForModules, { entry: path.join(__dirname, "modules/logger/index.js"), output: { - // @ts-ignore filename: "logger/index.js", }, module: { @@ -54,7 +53,6 @@ module.exports = [ use: [ { loader: "babel-loader", - // @ts-ignore options: { plugins: ["@babel/plugin-transform-object-assign"], }, @@ -78,7 +76,6 @@ module.exports = [ entry: path.join(__dirname, "modules/sockjs-client/index.js"), output: { filename: "sockjs-client/index.js", - // @ts-ignore library: "SockJS", libraryTarget: "umd", globalObject: "(typeof self !== 'undefined' ? self : this)", diff --git a/package.json b/package.json index a7605c28cc..e85e772f46 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "connect-history-api-fallback": "^2.0.0", "express": "^4.21.1", "graceful-fs": "^4.2.6", - "html-entities": "^2.4.0", "http-proxy-middleware": "^2.0.7", "ipaddr.js": "^2.1.0", "launch-editor": "^2.6.1", From 1694b52a836eb3bcb6bb20f0921789f26a009bcb Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Thu, 5 Dec 2024 18:44:38 +0300 Subject: [PATCH 2/9] fix: speed up initial client bundling --- client-src/index.js | 26 +++++++++++++++++++++++++- client-src/utils/stripAnsi.js | 26 -------------------------- 2 files changed, 25 insertions(+), 27 deletions(-) delete mode 100644 client-src/utils/stripAnsi.js diff --git a/client-src/index.js b/client-src/index.js index d6614a7a67..a556e29567 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -1,7 +1,6 @@ /* global __resourceQuery, __webpack_hash__ */ /// import webpackHotLog from "webpack/hot/log.js"; -import stripAnsi from "./utils/stripAnsi.js"; import parseURL from "./utils/parseURL.js"; import socket from "./socket.js"; import { formatProblem, createOverlay } from "./overlay.js"; @@ -165,6 +164,31 @@ const overlay = ) : { send: () => {} }; +const ansiRegex = new RegExp( + [ + "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", + "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))", + ].join("|"), + "g", +); + +/** + * + * Strip [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) from a string. + * Adapted from code originally released by Sindre Sorhus + * Licensed the MIT License + * + * @param {string} string + * @return {string} + */ +const stripAnsi = (string) => { + if (typeof string !== "string") { + throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``); + } + + return string.replace(ansiRegex, ""); +}; + const onSocketMessage = { hot() { if (parsedResourceQuery.hot === "false") { diff --git a/client-src/utils/stripAnsi.js b/client-src/utils/stripAnsi.js deleted file mode 100644 index 140783742f..0000000000 --- a/client-src/utils/stripAnsi.js +++ /dev/null @@ -1,26 +0,0 @@ -const ansiRegex = new RegExp( - [ - "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", - "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))", - ].join("|"), - "g", -); - -/** - * - * Strip [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) from a string. - * Adapted from code originally released by Sindre Sorhus - * Licensed the MIT License - * - * @param {string} string - * @return {string} - */ -function stripAnsi(string) { - if (typeof string !== "string") { - throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``); - } - - return string.replace(ansiRegex, ""); -} - -export default stripAnsi; From acd510039496d224595f7c18351c258654985e0d Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Thu, 5 Dec 2024 18:46:17 +0300 Subject: [PATCH 3/9] fix: speed up initial client bundling --- client-src/utils/getCurrentScriptSource.js | 29 ---------------------- client-src/utils/parseURL.js | 28 ++++++++++++++++++++- 2 files changed, 27 insertions(+), 30 deletions(-) delete mode 100644 client-src/utils/getCurrentScriptSource.js diff --git a/client-src/utils/getCurrentScriptSource.js b/client-src/utils/getCurrentScriptSource.js deleted file mode 100644 index 6ada91fea4..0000000000 --- a/client-src/utils/getCurrentScriptSource.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @returns {string} - */ -function getCurrentScriptSource() { - // `document.currentScript` is the most accurate way to find the current script, - // but is not supported in all browsers. - if (document.currentScript) { - return document.currentScript.getAttribute("src"); - } - - // Fallback to getting all scripts running in the document. - const scriptElements = document.scripts || []; - const scriptElementsWithSrc = Array.prototype.filter.call( - scriptElements, - (element) => element.getAttribute("src"), - ); - - if (scriptElementsWithSrc.length > 0) { - const currentScript = - scriptElementsWithSrc[scriptElementsWithSrc.length - 1]; - - return currentScript.getAttribute("src"); - } - - // Fail as there was no script to use. - throw new Error("[webpack-dev-server] Failed to get current script source."); -} - -export default getCurrentScriptSource; diff --git a/client-src/utils/parseURL.js b/client-src/utils/parseURL.js index 9e9b5e4422..59e28a78b7 100644 --- a/client-src/utils/parseURL.js +++ b/client-src/utils/parseURL.js @@ -1,4 +1,30 @@ -import getCurrentScriptSource from "./getCurrentScriptSource.js"; +/** + * @returns {string} + */ +function getCurrentScriptSource() { + // `document.currentScript` is the most accurate way to find the current script, + // but is not supported in all browsers. + if (document.currentScript) { + return document.currentScript.getAttribute("src"); + } + + // Fallback to getting all scripts running in the document. + const scriptElements = document.scripts || []; + const scriptElementsWithSrc = Array.prototype.filter.call( + scriptElements, + (element) => element.getAttribute("src"), + ); + + if (scriptElementsWithSrc.length > 0) { + const currentScript = + scriptElementsWithSrc[scriptElementsWithSrc.length - 1]; + + return currentScript.getAttribute("src"); + } + + // Fail as there was no script to use. + throw new Error("[webpack-dev-server] Failed to get current script source."); +} /** * @param {string} resourceQuery From 85c56ba053f19cd1f667201069ef98708ec788c8 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Thu, 5 Dec 2024 18:49:28 +0300 Subject: [PATCH 4/9] fix: speed up initial client bundling --- client-src/index.js | 163 +++++++++++++++++++++++++++- client-src/utils/createSocketURL.js | 163 ---------------------------- 2 files changed, 162 insertions(+), 164 deletions(-) delete mode 100644 client-src/utils/createSocketURL.js diff --git a/client-src/index.js b/client-src/index.js index a556e29567..f2af5244ed 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -7,7 +7,6 @@ import { formatProblem, createOverlay } from "./overlay.js"; import { log, logEnabledFeatures, setLogLevel } from "./utils/log.js"; import sendMessage from "./utils/sendMessage.js"; import reloadApp from "./utils/reloadApp.js"; -import createSocketURL from "./utils/createSocketURL.js"; import { isProgressSupported, defineProgressElement } from "./progress.js"; /** @@ -406,6 +405,168 @@ const onSocketMessage = { }, }; +/** + * @param {{ protocol?: string, auth?: string, hostname?: string, port?: string, pathname?: string, search?: string, hash?: string, slashes?: boolean }} objURL + * @returns {string} + */ +const formatURL = (objURL) => { + let protocol = objURL.protocol || ""; + + if (protocol && protocol.substr(-1) !== ":") { + protocol += ":"; + } + + let auth = objURL.auth || ""; + + if (auth) { + auth = encodeURIComponent(auth); + auth = auth.replace(/%3A/i, ":"); + auth += "@"; + } + + let host = ""; + + if (objURL.hostname) { + host = + auth + + (objURL.hostname.indexOf(":") === -1 + ? objURL.hostname + : `[${objURL.hostname}]`); + + if (objURL.port) { + host += `:${objURL.port}`; + } + } + + let pathname = objURL.pathname || ""; + + if (objURL.slashes) { + host = `//${host || ""}`; + + if (pathname && pathname.charAt(0) !== "/") { + pathname = `/${pathname}`; + } + } else if (!host) { + host = ""; + } + + let search = objURL.search || ""; + + if (search && search.charAt(0) !== "?") { + search = `?${search}`; + } + + let hash = objURL.hash || ""; + + if (hash && hash.charAt(0) !== "#") { + hash = `#${hash}`; + } + + pathname = pathname.replace( + /[?#]/g, + /** + * @param {string} match + * @returns {string} + */ + (match) => encodeURIComponent(match), + ); + search = search.replace("#", "%23"); + + return `${protocol}${host}${pathname}${search}${hash}`; +}; + +/** + * @param {URL & { fromCurrentScript?: boolean }} parsedURL + * @returns {string} + */ +const createSocketURL = (parsedURL) => { + let { hostname } = parsedURL; + + // Node.js module parses it as `::` + // `new URL(urlString, [baseURLString])` parses it as '[::]' + const isInAddrAny = + hostname === "0.0.0.0" || hostname === "::" || hostname === "[::]"; + + // why do we need this check? + // hostname n/a for file protocol (example, when using electron, ionic) + // see: https://github.com/webpack/webpack-dev-server/pull/384 + if ( + isInAddrAny && + self.location.hostname && + self.location.protocol.indexOf("http") === 0 + ) { + hostname = self.location.hostname; + } + + let socketURLProtocol = parsedURL.protocol || self.location.protocol; + + // When https is used in the app, secure web sockets are always necessary because the browser doesn't accept non-secure web sockets. + if ( + socketURLProtocol === "auto:" || + (hostname && isInAddrAny && self.location.protocol === "https:") + ) { + socketURLProtocol = self.location.protocol; + } + + socketURLProtocol = socketURLProtocol.replace( + /^(?:http|.+-extension|file)/i, + "ws", + ); + + let socketURLAuth = ""; + + // `new URL(urlString, [baseURLstring])` doesn't have `auth` property + // Parse authentication credentials in case we need them + if (parsedURL.username) { + socketURLAuth = parsedURL.username; + + // Since HTTP basic authentication does not allow empty username, + // we only include password if the username is not empty. + if (parsedURL.password) { + // Result: : + socketURLAuth = socketURLAuth.concat(":", parsedURL.password); + } + } + + // In case the host is a raw IPv6 address, it can be enclosed in + // the brackets as the brackets are needed in the final URL string. + // Need to remove those as url.format blindly adds its own set of brackets + // if the host string contains colons. That would lead to non-working + // double brackets (e.g. [[::]]) host + // + // All of these web socket url params are optionally passed in through resourceQuery, + // so we need to fall back to the default if they are not provided + const socketURLHostname = ( + hostname || + self.location.hostname || + "localhost" + ).replace(/^\[(.*)\]$/, "$1"); + + let socketURLPort = parsedURL.port; + + if (!socketURLPort || socketURLPort === "0") { + socketURLPort = self.location.port; + } + + // If path is provided it'll be passed in via the resourceQuery as a + // query param so it has to be parsed out of the querystring in order for the + // client to open the socket to the correct location. + let socketURLPathname = "/ws"; + + if (parsedURL.pathname && !parsedURL.fromCurrentScript) { + socketURLPathname = parsedURL.pathname; + } + + return formatURL({ + protocol: socketURLProtocol, + auth: socketURLAuth, + hostname: socketURLHostname, + port: socketURLPort, + pathname: socketURLPathname, + slashes: true, + }); +}; + const socketURL = createSocketURL(parsedResourceQuery); socket(socketURL, onSocketMessage, options.reconnect); diff --git a/client-src/utils/createSocketURL.js b/client-src/utils/createSocketURL.js deleted file mode 100644 index 0d049eb7ab..0000000000 --- a/client-src/utils/createSocketURL.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * @param {{ protocol?: string, auth?: string, hostname?: string, port?: string, pathname?: string, search?: string, hash?: string, slashes?: boolean }} objURL - * @returns {string} - */ -function format(objURL) { - let protocol = objURL.protocol || ""; - - if (protocol && protocol.substr(-1) !== ":") { - protocol += ":"; - } - - let auth = objURL.auth || ""; - - if (auth) { - auth = encodeURIComponent(auth); - auth = auth.replace(/%3A/i, ":"); - auth += "@"; - } - - let host = ""; - - if (objURL.hostname) { - host = - auth + - (objURL.hostname.indexOf(":") === -1 - ? objURL.hostname - : `[${objURL.hostname}]`); - - if (objURL.port) { - host += `:${objURL.port}`; - } - } - - let pathname = objURL.pathname || ""; - - if (objURL.slashes) { - host = `//${host || ""}`; - - if (pathname && pathname.charAt(0) !== "/") { - pathname = `/${pathname}`; - } - } else if (!host) { - host = ""; - } - - let search = objURL.search || ""; - - if (search && search.charAt(0) !== "?") { - search = `?${search}`; - } - - let hash = objURL.hash || ""; - - if (hash && hash.charAt(0) !== "#") { - hash = `#${hash}`; - } - - pathname = pathname.replace( - /[?#]/g, - /** - * @param {string} match - * @returns {string} - */ - (match) => encodeURIComponent(match), - ); - search = search.replace("#", "%23"); - - return `${protocol}${host}${pathname}${search}${hash}`; -} - -/** - * @param {URL & { fromCurrentScript?: boolean }} parsedURL - * @returns {string} - */ -function createSocketURL(parsedURL) { - let { hostname } = parsedURL; - - // Node.js module parses it as `::` - // `new URL(urlString, [baseURLString])` parses it as '[::]' - const isInAddrAny = - hostname === "0.0.0.0" || hostname === "::" || hostname === "[::]"; - - // why do we need this check? - // hostname n/a for file protocol (example, when using electron, ionic) - // see: https://github.com/webpack/webpack-dev-server/pull/384 - if ( - isInAddrAny && - self.location.hostname && - self.location.protocol.indexOf("http") === 0 - ) { - hostname = self.location.hostname; - } - - let socketURLProtocol = parsedURL.protocol || self.location.protocol; - - // When https is used in the app, secure web sockets are always necessary because the browser doesn't accept non-secure web sockets. - if ( - socketURLProtocol === "auto:" || - (hostname && isInAddrAny && self.location.protocol === "https:") - ) { - socketURLProtocol = self.location.protocol; - } - - socketURLProtocol = socketURLProtocol.replace( - /^(?:http|.+-extension|file)/i, - "ws", - ); - - let socketURLAuth = ""; - - // `new URL(urlString, [baseURLstring])` doesn't have `auth` property - // Parse authentication credentials in case we need them - if (parsedURL.username) { - socketURLAuth = parsedURL.username; - - // Since HTTP basic authentication does not allow empty username, - // we only include password if the username is not empty. - if (parsedURL.password) { - // Result: : - socketURLAuth = socketURLAuth.concat(":", parsedURL.password); - } - } - - // In case the host is a raw IPv6 address, it can be enclosed in - // the brackets as the brackets are needed in the final URL string. - // Need to remove those as url.format blindly adds its own set of brackets - // if the host string contains colons. That would lead to non-working - // double brackets (e.g. [[::]]) host - // - // All of these web socket url params are optionally passed in through resourceQuery, - // so we need to fall back to the default if they are not provided - const socketURLHostname = ( - hostname || - self.location.hostname || - "localhost" - ).replace(/^\[(.*)\]$/, "$1"); - - let socketURLPort = parsedURL.port; - - if (!socketURLPort || socketURLPort === "0") { - socketURLPort = self.location.port; - } - - // If path is provided it'll be passed in via the resourceQuery as a - // query param so it has to be parsed out of the querystring in order for the - // client to open the socket to the correct location. - let socketURLPathname = "/ws"; - - if (parsedURL.pathname && !parsedURL.fromCurrentScript) { - socketURLPathname = parsedURL.pathname; - } - - return format({ - protocol: socketURLProtocol, - auth: socketURLAuth, - hostname: socketURLHostname, - port: socketURLPort, - pathname: socketURLPathname, - slashes: true, - }); -} - -export default createSocketURL; From 4382bcb5cacd033f2a0c3a2b49074cde794d668e Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Thu, 5 Dec 2024 18:55:35 +0300 Subject: [PATCH 5/9] fix: speed up initial client bundling --- client-src/index.js | 112 +++++++++++++++++++++++++++++++---- client-src/utils/log.js | 21 +------ client-src/utils/parseURL.js | 70 ---------------------- 3 files changed, 100 insertions(+), 103 deletions(-) delete mode 100644 client-src/utils/parseURL.js diff --git a/client-src/index.js b/client-src/index.js index f2af5244ed..54e65e5669 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -1,10 +1,9 @@ /* global __resourceQuery, __webpack_hash__ */ /// import webpackHotLog from "webpack/hot/log.js"; -import parseURL from "./utils/parseURL.js"; import socket from "./socket.js"; import { formatProblem, createOverlay } from "./overlay.js"; -import { log, logEnabledFeatures, setLogLevel } from "./utils/log.js"; +import { log, setLogLevel } from "./utils/log.js"; import sendMessage from "./utils/sendMessage.js"; import reloadApp from "./utils/reloadApp.js"; import { isProgressSupported, defineProgressElement } from "./progress.js"; @@ -46,13 +45,11 @@ const decodeOverlayOptions = (overlayOptions) => { ); // eslint-disable-next-line no-new-func - const overlayFilterFunction = new Function( + overlayOptions[property] = new Function( "message", `var callback = ${overlayFilterFunctionString} return callback(message)`, ); - - overlayOptions[property] = overlayFilterFunction; } }); } @@ -67,13 +64,75 @@ const status = { currentHash: __webpack_hash__, }; -/** @type {Options} */ -const options = { - hot: false, - liveReload: false, - progress: false, - overlay: false, +/** + * @returns {string} + */ +const getCurrentScriptSource = () => { + // `document.currentScript` is the most accurate way to find the current script, + // but is not supported in all browsers. + if (document.currentScript) { + return document.currentScript.getAttribute("src"); + } + + // Fallback to getting all scripts running in the document. + const scriptElements = document.scripts || []; + const scriptElementsWithSrc = Array.prototype.filter.call( + scriptElements, + (element) => element.getAttribute("src"), + ); + + if (scriptElementsWithSrc.length > 0) { + const currentScript = + scriptElementsWithSrc[scriptElementsWithSrc.length - 1]; + + return currentScript.getAttribute("src"); + } + + // Fail as there was no script to use. + throw new Error("[webpack-dev-server] Failed to get current script source."); +}; + +/** + * @param {string} resourceQuery + * @returns {{ [key: string]: string | boolean }} + */ +const parseURL = (resourceQuery) => { + /** @type {{ [key: string]: string }} */ + let result = {}; + + if (typeof resourceQuery === "string" && resourceQuery !== "") { + const searchParams = resourceQuery.slice(1).split("&"); + + for (let i = 0; i < searchParams.length; i++) { + const pair = searchParams[i].split("="); + + result[pair[0]] = decodeURIComponent(pair[1]); + } + } else { + // Else, get the url from the