diff --git a/package-lock.json b/package-lock.json index 24cff7fcc..b9a307f1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "@opentelemetry/winston-transport": "^0.26.0", "@types/compression": "^1.8.1", "colord": "^2.9.3", - "colorjs.io": "^0.6.1", "compression": "^1.8.1", "dompurify": "^3.4.2", "dotenv": "^17.4.2", @@ -77,7 +76,7 @@ "jsdom": "^29.1.1", "lint-staged": "^16.4.0", "lit": "^3.3.2", - "lit-markdown": "^1.3.2", + "marked": "^18.0.5", "mrmime": "^2.0.1", "pixi-filters": "^6.1.5", "pixi.js": "^8.18.1", @@ -3896,16 +3895,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colorjs.io": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.6.1.tgz", - "integrity": "sha512-8lyR2wHzuIykCpqHKgluGsqQi5iDm3/a2IgP2GBZrasn2sBRkE4NOGsglZxWLs/jZQoNkmA/KM/8NV16rLUdBg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/color" - } - }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -4599,13 +4588,6 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/dayjs": { - "version": "1.11.20", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", - "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "dev": true, - "license": "MIT" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4663,16 +4645,6 @@ "dev": true, "license": "MIT" }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/delaunator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", @@ -5892,39 +5864,6 @@ "dev": true, "license": "MIT" }, - "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -6121,16 +6060,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -6376,16 +6305,6 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, - "node_modules/launder": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/launder/-/launder-1.7.1.tgz", - "integrity": "sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dayjs": "^1.11.7" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6769,62 +6688,6 @@ "@types/trusted-types": "^2.0.2" } }, - "node_modules/lit-markdown": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/lit-markdown/-/lit-markdown-1.3.2.tgz", - "integrity": "sha512-51M4QRR2UmJIKck3kRQClD8CAM2Ox7BBZ9qzQLA1ppkr5tFfyNJDXnM0A2xmpNNfFrELZrnTmD5ST6VEdcemPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lit": "^2.6.1", - "marked": "^4.2.12", - "sanitize-html": "^2.9.0" - } - }, - "node_modules/lit-markdown/node_modules/@lit/reactive-element": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", - "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.0.0" - } - }, - "node_modules/lit-markdown/node_modules/lit": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", - "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^1.6.0", - "lit-element": "^3.3.0", - "lit-html": "^2.8.0" - } - }, - "node_modules/lit-markdown/node_modules/lit-element": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", - "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.1.0", - "@lit/reactive-element": "^1.3.0", - "lit-html": "^2.8.0" - } - }, - "node_modules/lit-markdown/node_modules/lit-html": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", - "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -6993,16 +6856,16 @@ "license": "ISC" }, "node_modules/marked": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", "dev": true, "license": "MIT", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 12" + "node": ">= 20" } }, "node_modules/math-intrinsics": { @@ -7412,13 +7275,6 @@ "tslib": "^2.0.3" } }, - "node_modules/parse-srcset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", - "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", - "dev": true, - "license": "MIT" - }, "node_modules/parse-svg-path": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", @@ -8185,22 +8041,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/sanitize-html": { - "version": "2.17.4", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.4.tgz", - "integrity": "sha512-2HW7v2ol/uAM7sX4hbD8Z59OGWmAPrvjL8E71UWlBcj6m+kcF6ilQBLny+cIgY214QJeJT5tQuxKKqX0SQqjGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "deepmerge": "^4.2.2", - "escape-string-regexp": "^4.0.0", - "htmlparser2": "^10.1.0", - "is-plain-object": "^5.0.0", - "launder": "^1.7.1", - "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" - } - }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", diff --git a/package.json b/package.json index 236aaa353..6e7416d96 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "jsdom": "^29.1.1", "lint-staged": "^16.4.0", "lit": "^3.3.2", - "lit-markdown": "^1.3.2", + "marked": "^18.0.5", "mrmime": "^2.0.1", "pixi-filters": "^6.1.5", "pixi.js": "^8.18.1", @@ -93,7 +93,6 @@ "@opentelemetry/winston-transport": "^0.26.0", "@types/compression": "^1.8.1", "colord": "^2.9.3", - "colorjs.io": "^0.6.1", "compression": "^1.8.1", "dompurify": "^3.4.2", "dotenv": "^17.4.2", diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index b25a05396..e8947f011 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -71,9 +71,9 @@ import { createRenderer, GameRenderer } from "./hud/GameRenderer"; import { applyDarkModeOverride, applyGraphicsOverrides, - createDebugGui, createRenderSettings, deepAssign, + preloadAtlasData, GameView as WebGLGameView, } from "./render/gl"; import { ALL_UNIT_TYPES, UnitState } from "./render/types"; @@ -451,8 +451,12 @@ async function createClientGame( mapLoader, ); } + // Kick off the font-atlas fetch so it overlaps with worker init; the + // render passes need it parsed before createWebGLView runs. + const atlasDataLoad = preloadAtlasData(); const worker = new WorkerClient(lobbyConfig.gameStartInfo, clientID); await worker.initialize(); + await atlasDataLoad; const gameView = new GameView( worker, config, @@ -519,11 +523,21 @@ async function createClientGame( { signal: graphicsListenerAbort.signal }, ); - let debugGui: ReturnType | null = null; + // Loaded on demand so lil-gui and the debug GUI stay out of the main bundle. + let debugGui: { open(): void; destroy(): void } | null = null; + let debugGuiLoading = false; eventBus.on(ToggleRenderDebugGuiEvent, () => { if (debugGui === null) { - debugGui = createDebugGui(view.getSettings()); - debugGui.open(); + if (debugGuiLoading) return; + debugGuiLoading = true; + import("./render/gl/debug/index") + .then(({ createDebugGui }) => { + debugGui = createDebugGui(view.getSettings()); + debugGui.open(); + }) + .finally(() => { + debugGuiLoading = false; + }); } else { debugGui.destroy(); debugGui = null; diff --git a/src/client/Markdown.ts b/src/client/Markdown.ts new file mode 100644 index 000000000..4097fef7a --- /dev/null +++ b/src/client/Markdown.ts @@ -0,0 +1,21 @@ +import DOMPurify from "dompurify"; +import { unsafeHTML } from "lit/directives/unsafe-html.js"; +import { marked } from "marked"; + +/** + * Render markdown to a sanitized lit template value. Images are stripped + * unless `includeImages` is set (matching the lit-markdown default this + * replaces — lit-markdown pulled in sanitize-html (~325 KB min); DOMPurify is + * already in the bundle). + */ +export function renderMarkdown( + rawMarkdown: string, + options?: { includeImages?: boolean }, +) { + const rawHTML = marked.parse(rawMarkdown, { async: false }); + const cleanHTML = DOMPurify.sanitize( + rawHTML, + options?.includeImages ? {} : { FORBID_TAGS: ["img"] }, + ); + return unsafeHTML(cleanHTML); +} diff --git a/src/client/NewsModal.ts b/src/client/NewsModal.ts index a1e2e2c67..7b4ea45a7 100644 --- a/src/client/NewsModal.ts +++ b/src/client/NewsModal.ts @@ -1,11 +1,11 @@ import { html, LitElement } from "lit"; -import { resolveMarkdown } from "lit-markdown"; import { customElement, property, query } from "lit/decorators.js"; import version from "resources/version.txt?raw"; import { translateText } from "../client/Utils"; import { assetUrl } from "../core/AssetUrls"; import { BaseModal } from "./components/BaseModal"; import { modalHeader } from "./components/ui/ModalHeader"; +import { renderMarkdown } from "./Markdown"; import { normalizeNewsMarkdown } from "./NewsMarkdown"; @customElement("news-modal") @@ -36,10 +36,7 @@ export class NewsModal extends BaseModal { [&_li]:text-gray-300 [&_li]:leading-relaxed [&_p]:text-gray-300 [&_p]:mb-3 [&_strong]:text-white [&_strong]:font-bold" > - ${resolveMarkdown(this.markdown, { - includeImages: true, - includeCodeBlockClassNames: true, - })} + ${renderMarkdown(this.markdown, { includeImages: true })} `; } diff --git a/src/client/components/NewsBox.ts b/src/client/components/NewsBox.ts index 42b7982b3..0a801a4c7 100644 --- a/src/client/components/NewsBox.ts +++ b/src/client/components/NewsBox.ts @@ -1,8 +1,8 @@ import { LitElement, html, nothing } from "lit"; -import { resolveMarkdown } from "lit-markdown"; import { customElement, state } from "lit/decorators.js"; import type { NewsItem } from "../../core/ApiSchemas"; import { getNews } from "../Api"; +import { renderMarkdown } from "../Markdown"; import { translateText } from "../Utils"; export type { NewsItem }; @@ -140,7 +140,7 @@ export class NewsBox extends LitElement { >`} ${resolveMarkdown( + >${renderMarkdown( item.descriptionTranslationKey ? translateText(item.descriptionTranslationKey) : (item.description ?? ""), diff --git a/src/client/components/baseComponents/Modal.ts b/src/client/components/baseComponents/Modal.ts index 68ebd4462..a8c6bc2b8 100644 --- a/src/client/components/baseComponents/Modal.ts +++ b/src/client/components/baseComponents/Modal.ts @@ -1,12 +1,12 @@ -import { LitElement, html, unsafeCSS } from "lit"; +import { LitElement, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; -import tailwindStyles from "../../styles.css?inline"; +import { documentStylesSheet } from "./SharedStyles"; export type OModalTab = { key: string; label: string }; @customElement("o-modal") export class OModal extends LitElement { - static styles = [unsafeCSS(tailwindStyles)]; + static styles = [documentStylesSheet()]; @state() public isModalOpen = false; diff --git a/src/client/components/baseComponents/SharedStyles.ts b/src/client/components/baseComponents/SharedStyles.ts new file mode 100644 index 000000000..9089d4096 --- /dev/null +++ b/src/client/components/baseComponents/SharedStyles.ts @@ -0,0 +1,53 @@ +/** + * Shared constructed stylesheet mirroring the document's global CSS, for + * shadow-DOM components that want the page's Tailwind styles. Importing + * styles.css?inline instead would ship the full Tailwind CSS a second time + * (~160 KB) inside the JS bundle. + * + * In production the page stylesheet is fetched (same URL the browser + * already loaded, so it resolves from HTTP cache); in dev Vite injects + *