diff --git a/packages/core/src/compiler/externalScripts.ts b/packages/core/src/compiler/externalScripts.ts new file mode 100644 index 0000000000..796a5f46c6 --- /dev/null +++ b/packages/core/src/compiler/externalScripts.ts @@ -0,0 +1,49 @@ +export interface ExternalScriptAttributes { + integrity?: string; + crossorigin?: string; +} + +export function readExternalScriptAttributes(el: Element): ExternalScriptAttributes { + const attributes: ExternalScriptAttributes = {}; + if (el.hasAttribute("integrity")) attributes.integrity = el.getAttribute("integrity") || ""; + if (el.hasAttribute("crossorigin")) attributes.crossorigin = el.getAttribute("crossorigin") || ""; + return attributes; +} + +/** Deduplicate scripts without discarding a nested composition's integrity requirement. */ +export function ensureExternalScriptTag( + doc: Document, + src: string, + attributes: ExternalScriptAttributes = {}, +): void { + const existing = [...doc.querySelectorAll("script[src]")].filter( + (el) => el.getAttribute("src")?.trim() === src, + ); + const requirements = new Set( + [attributes.integrity, ...existing.map((el) => el.getAttribute("integrity"))] + .map((value) => + value + ?.trim() + .replace( + /(^|[\t\n\f\r ])(sha256|sha384|sha512)-/gi, + (_match, space: string, algorithm: string) => `${space}${algorithm.toLowerCase()}-`, + ), + ) + .filter((value): value is string => Boolean(value)), + ); + if (requirements.size > 1) { + throw new Error(`Conflicting script integrity requirements for ${src}`); + } + const integrity = [...requirements][0]; + const elements = existing.length ? existing : [doc.createElement("script")]; + for (const el of elements) { + if (integrity) el.setAttribute("integrity", integrity); + if (attributes.crossorigin !== undefined) + el.setAttribute("crossorigin", attributes.crossorigin); + } + if (!existing.length) { + const el = elements[0]!; + el.setAttribute("src", src); + doc.body.appendChild(el); + } +} diff --git a/packages/core/src/compiler/htmlBundler.test.ts b/packages/core/src/compiler/htmlBundler.test.ts index 43c2b94caf..3939288be5 100644 --- a/packages/core/src/compiler/htmlBundler.test.ts +++ b/packages/core/src/compiler/htmlBundler.test.ts @@ -5,6 +5,7 @@ import { join } from "node:path"; import { parseHTML } from "linkedom"; import { afterEach, beforeEach, describe, it, expect, vi } from "vitest"; import { bundleToSingleHtml, emitRootCompositionVariableStyles } from "./htmlBundler"; +import { ensureExternalScriptTag } from "./externalScripts"; import { resetUnknownEnumWarnings } from "../runtime/getVariables"; import { sanitizeCssValue } from "../runtime/applyVariableBindings"; import { getHyperframeRuntimeScript } from "../generated/runtime-inline"; @@ -1629,3 +1630,98 @@ describe("emitRootCompositionVariableStyles —