From 6b1cbf81636676945cd1c9abf1684f10ff62ef38 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:32:35 -0700 Subject: [PATCH 1/2] Remove innerHTML uses --- .changeset/stale-steaks-drop.md | 5 + .../template/src/vendor/utils.ts | 2 +- .../playground/src/createConfigDropdown.ts | 58 +++++++-- packages/playground/src/createElements.ts | 6 +- .../playground/src/ds/createDesignSystem.ts | 81 ++++++++++-- packages/playground/src/navigation.ts | 57 +++++++-- packages/playground/src/pluginUtils.ts | 2 +- packages/playground/src/sidebar/plugins.ts | 36 ++++-- packages/playground/src/sidebar/runtime.ts | 118 ++++++++++-------- .../src/components/handbook/Contributors.tsx | 6 +- .../src/components/workbench/plugins/about.ts | 28 ++--- .../src/components/workbench/plugins/debug.ts | 9 +- .../src/components/workbench/plugins/docs.ts | 84 +++++++++---- .../src/pages/dev/twoslash.tsx | 2 - .../scripts/setupLikeDislikeButtons.ts | 11 +- 15 files changed, 357 insertions(+), 148 deletions(-) create mode 100644 .changeset/stale-steaks-drop.md diff --git a/.changeset/stale-steaks-drop.md b/.changeset/stale-steaks-drop.md new file mode 100644 index 000000000000..324ba9bc978d --- /dev/null +++ b/.changeset/stale-steaks-drop.md @@ -0,0 +1,5 @@ +--- +"create-typescript-playground-plugin": patch +--- + +Remove innerHTML uses diff --git a/packages/create-typescript-playground-plugin/template/src/vendor/utils.ts b/packages/create-typescript-playground-plugin/template/src/vendor/utils.ts index 8344892361ca..614e8b30a2a4 100644 --- a/packages/create-typescript-playground-plugin/template/src/vendor/utils.ts +++ b/packages/create-typescript-playground-plugin/template/src/vendor/utils.ts @@ -9,6 +9,6 @@ export const requireURL = (path: string) => { /** Use this to make a few dumb element generation funcs */ export const el = (str: string, el: string, container: Element) => { const para = document.createElement(el) - para.innerHTML = str + para.textContent = str container.appendChild(para) } diff --git a/packages/playground/src/createConfigDropdown.ts b/packages/playground/src/createConfigDropdown.ts index f67f8e38aca1..2d1c67f7a0c1 100644 --- a/packages/playground/src/createConfigDropdown.ts +++ b/packages/playground/src/createConfigDropdown.ts @@ -50,6 +50,40 @@ const notRelevantToPlayground = [ "forceConsistentCasingInFileNames", ] +const createInfoIcon = () => { + const svgNamespace = "http://www.w3.org/2000/svg" + const svg = document.createElementNS(svgNamespace, "svg") + svg.setAttribute("width", "20px") + svg.setAttribute("height", "20px") + svg.setAttribute("viewBox", "0 0 20 20") + svg.setAttribute("version", "1.1") + + const g = document.createElementNS(svgNamespace, "g") + g.setAttribute("stroke", "none") + g.setAttribute("stroke-width", "1") + g.setAttribute("fill", "none") + g.setAttribute("fill-rule", "evenodd") + svg.appendChild(g) + + const circle = document.createElementNS(svgNamespace, "circle") + circle.setAttribute("stroke", "#0B6F57") + circle.setAttribute("cx", "10") + circle.setAttribute("cy", "10") + circle.setAttribute("r", "9") + g.appendChild(circle) + + const path = document.createElementNS(svgNamespace, "path") + path.setAttribute( + "d", + "M9.99598394,6 C10.2048193,6 10.4243641,5.91700134 10.6546185,5.75100402 C10.8848728,5.58500669 11,5.33601071 11,5.00401606 C11,4.66666667 10.8848728,4.41499331 10.6546185,4.24899598 C10.4243641,4.08299866 10.2048193,4 9.99598394,4 C9.79250335,4 9.57563588,4.08299866 9.34538153,4.24899598 C9.11512718,4.41499331 9,4.66666667 9,5.00401606 C9,5.33601071 9.11512718,5.58500669 9.34538153,5.75100402 C9.57563588,5.91700134 9.79250335,6 9.99598394,6 Z M10.6877323,16 L10.6877323,14.8898836 L10.6877323,8 L9.30483271,8 L9.30483271,9.11011638 L9.30483271,16 L10.6877323,16 Z" + ) + path.setAttribute("fill", "#0B6F57") + path.setAttribute("fill-rule", "nonzero") + g.appendChild(path) + + return svg +} + export const createConfigDropdown = (sandbox: Sandbox, monaco: Monaco) => { const configContainer = document.getElementById("config-container")! const container = document.createElement("div") @@ -121,13 +155,21 @@ export const createConfigDropdown = (sandbox: Sandbox, monaco: Monaco) => { label.style.position = "relative" label.style.width = "100%" - const svg = ` - - - - - ` - label.innerHTML = `${optSummary.id}${svg}
${optSummary.oneliner}` + const optionName = document.createElement("span") + optionName.textContent = optSummary.id + label.appendChild(optionName) + + const optionReference = document.createElement("a") + optionReference.href = `../tsconfig#${optSummary.id}` + optionReference.className = "compiler_info_link" + optionReference.setAttribute("aria-label", `Look up ${optSummary.id} in the TSConfig Reference`) + optionReference.target = "_blank" + optionReference.rel = "noopener noreferrer" + optionReference.appendChild(createInfoIcon()) + label.appendChild(optionReference) + + label.appendChild(document.createElement("br")) + label.appendChild(document.createTextNode(optSummary.oneliner)) const input = document.createElement("input") input.value = optSummary.id @@ -244,7 +286,7 @@ const createSelect = (title: string, id: string, blurb: string, sandbox: Sandbox }) const span = document.createElement("span") - span.innerHTML = blurb + span.textContent = blurb span.classList.add("compiler-flag-blurb") label.appendChild(span) diff --git a/packages/playground/src/createElements.ts b/packages/playground/src/createElements.ts index 5cee7a81b28e..3569a973314e 100644 --- a/packages/playground/src/createElements.ts +++ b/packages/playground/src/createElements.ts @@ -119,8 +119,8 @@ export const createSidebar = () => { return sidebar } -const toggleIconWhenOpen = "⇥" -const toggleIconWhenClosed = "⇤" +const toggleIconWhenOpen = "\u21E5" +const toggleIconWhenClosed = "\u21E4" export const setupSidebarToggle = () => { const toggle = document.getElementById("sidebar-toggle")! @@ -129,7 +129,7 @@ export const setupSidebarToggle = () => { const sidebar = window.document.querySelector(".playground-sidebar") as HTMLDivElement const sidebarShowing = sidebar.style.display !== "none" - toggle.innerHTML = sidebarShowing ? toggleIconWhenOpen : toggleIconWhenClosed + toggle.textContent = sidebarShowing ? toggleIconWhenOpen : toggleIconWhenClosed toggle.setAttribute("aria-label", sidebarShowing ? "Hide Sidebar" : "Show Sidebar") } diff --git a/packages/playground/src/ds/createDesignSystem.ts b/packages/playground/src/ds/createDesignSystem.ts index a4089e42b54a..f8338072586f 100644 --- a/packages/playground/src/ds/createDesignSystem.ts +++ b/packages/playground/src/ds/createDesignSystem.ts @@ -1,5 +1,5 @@ import type { Sandbox } from "@typescript/sandbox" -import type { DiagnosticRelatedInformation, Node } from "typescript" +import type { DiagnosticRelatedInformation, Node as TSNode } from "typescript" export type LocalStorageOption = { blurb: string @@ -17,9 +17,24 @@ export type OptionsListConfig = { requireRestart?: true } +type ElementChild = string | Node + +const appendChildren = (el: Element, children: ElementChild[]) => { + children.forEach(child => { + el.appendChild(typeof child === "string" ? document.createTextNode(child) : child) + }) +} + const el = (str: string, elementType: string, container: Element) => { const el = document.createElement(elementType) - el.innerHTML = str + el.textContent = str + container.appendChild(el) + return el +} + +const elWithChildren = (children: ElementChild[], elementType: string, container: Element) => { + const el = document.createElement(elementType) + appendChildren(el, children) container.appendChild(el) return el } @@ -102,8 +117,11 @@ export const createDesignSystem = (sandbox: Sandbox) => { const li = document.createElement("li") const label = document.createElement("label") - const split = setting.oneline ? "" : "
" - label.innerHTML = `${setting.display}${split}${setting.blurb}` + const display = document.createElement("span") + display.textContent = setting.display + label.appendChild(display) + if (!setting.oneline) label.appendChild(document.createElement("br")) + label.appendChild(document.createTextNode(setting.blurb)) const key = setting.flag const input = document.createElement("input") @@ -187,6 +205,32 @@ export const createDesignSystem = (sandbox: Sandbox) => { return noErrorsMessage } + const link = (href: string, text: string) => { + const a = document.createElement("a") + a.href = href + a.textContent = text + return a + } + + const inlineCode = (text: string) => { + const code = document.createElement("code") + code.textContent = text + return code + } + + const lineBreak = () => document.createElement("br") + + const unorderedList = (...items: ElementChild[][]) => { + const ul = document.createElement("ul") + items.forEach(item => { + const li = document.createElement("li") + appendChildren(li, item) + ul.appendChild(li) + }) + container.appendChild(ul) + return ul + } + const createTabBar = () => { const tabBar = document.createElement("div") tabBar.classList.add("playground-plugin-tabview") @@ -305,13 +349,13 @@ export const createDesignSystem = (sandbox: Sandbox) => { container.appendChild(ol) } - const createASTTree = (node: Node, settings?: { closedByDefault?: true }) => { + const createASTTree = (node: TSNode, settings?: { closedByDefault?: true }) => { const autoOpen = !settings || !settings.closedByDefault const div = document.createElement("div") div.className = "ast" - const infoForNode = (node: Node) => { + const infoForNode = (node: TSNode) => { const name = ts.SyntaxKind[node.kind] return { @@ -337,20 +381,21 @@ export const createDesignSystem = (sandbox: Sandbox) => { return li } - const renderSingleChild = (key: string, value: Node, depth: number) => { + const renderSingleChild = (key: string, value: TSNode, depth: number) => { const li = document.createElement("li") - li.innerHTML = `${key}: ` + li.textContent = `${key}: ` renderItem(li, value, depth + 1) return li } - const renderManyChildren = (key: string, nodes: Node[], depth: number) => { + const renderManyChildren = (key: string, nodes: TSNode[], depth: number) => { const children = document.createElement("div") children.classList.add("ast-children") const li = document.createElement("li") - li.innerHTML = `${key}: [
` + li.textContent = `${key}: [` + li.appendChild(document.createElement("br")) children.appendChild(li) nodes.forEach(node => { @@ -358,12 +403,12 @@ export const createDesignSystem = (sandbox: Sandbox) => { }) const liEnd = document.createElement("li") - liEnd.innerHTML += "]" + liEnd.textContent = "]" children.appendChild(liEnd) return children } - const renderItem = (parentElement: Element, node: Node, depth: number) => { + const renderItem = (parentElement: Element, node: TSNode, depth: number) => { const itemDiv = document.createElement("div") parentElement.appendChild(itemDiv) itemDiv.className = "ast-tree-start" @@ -497,6 +542,18 @@ export const createDesignSystem = (sandbox: Sandbox) => { subtitle: (subtitle: string) => el(subtitle, "h4", container), /** Used to show a paragraph */ p: (subtitle: string) => el(subtitle, "p", container), + /** Used to show a paragraph with safe DOM children */ + pWithChildren: (...children: ElementChild[]) => elWithChildren(children, "p", container), + /** Used to show a section heading with safe DOM children */ + subtitleWithChildren: (...children: ElementChild[]) => elWithChildren(children, "h4", container), + /** Creates an unattached anchor with safe text */ + link, + /** Creates an unattached inline code element with safe text */ + inlineCode, + /** Creates an unattached line break */ + lineBreak, + /** Appends an unordered list with safe DOM children */ + unorderedList, /** When you can't do something, or have nothing to show */ showEmptyScreen, /** diff --git a/packages/playground/src/navigation.ts b/packages/playground/src/navigation.ts index 2c1aece35dae..944713a417b7 100644 --- a/packages/playground/src/navigation.ts +++ b/packages/playground/src/navigation.ts @@ -6,6 +6,45 @@ type StoryContent = import type { Sandbox } from "@typescript/sandbox" +const createStoryIcon = (type: "code" | "html") => { + const svgNamespace = "http://www.w3.org/2000/svg" + const svg = document.createElementNS(svgNamespace, "svg") + svg.setAttribute("fill", "none") + + if (type === "code") { + svg.setAttribute("width", "7") + svg.setAttribute("height", "7") + svg.setAttribute("viewBox", "0 0 7 7") + + const rect = document.createElementNS(svgNamespace, "rect") + rect.setAttribute("width", "7") + rect.setAttribute("height", "7") + rect.setAttribute("fill", "#187ABF") + svg.appendChild(rect) + } else { + svg.setAttribute("width", "9") + svg.setAttribute("height", "11") + svg.setAttribute("viewBox", "0 0 9 11") + + const path = document.createElementNS(svgNamespace, "path") + path.setAttribute("d", "M8 5.5V3.25L6 1H4M8 5.5V10H1V1H4M8 5.5H4V1") + path.setAttribute("stroke", "#C4C4C4") + svg.appendChild(path) + } + + return svg +} + +const createLocalDevStoryMessage = () => { + const p = document.createElement("p") + p.appendChild(document.createTextNode("Because the gatsby dev server uses JS to build your pages, and not statically, the page will not load during dev. It does work in prod though - use ")) + const code = document.createElement("code") + code.textContent = "pnpm build-site" + p.appendChild(code) + p.appendChild(document.createTextNode(" to test locally with a static build.")) + return p +} + /** Use the handbook TOC which is injected into the globals to create a sidebar */ export const showNavForHandbook = (sandbox: Sandbox, escapeFunction: () => void) => { // @ts-ignore @@ -73,16 +112,10 @@ const updateNavWithStoryContent = (title: string, storyContent: StoryContent[], li.classList.add("selectable") const a = document.createElement("a") - let logo: string - if (element.type === "code") { - logo = `` - } else if (element.type === "html") { - logo = `` - } else { - logo = "" + if (element.type === "code" || element.type === "html") { + a.appendChild(createStoryIcon(element.type)) } - - a.innerHTML = `${logo}${element.title}` + a.appendChild(document.createTextNode(element.title)) a.href = `/play#${prefix}-${i}` a.onclick = e => { @@ -164,12 +197,14 @@ const setStoryViaHref = (href: string, sandbox: Sandbox) => { } if (document.location.host === "localhost:8000") { - setStory("

Because the gatsby dev server uses JS to build your pages, and not statically, the page will not load during dev. It does work in prod though - use pnpm build-site to test locally with a static build.

", sandbox) + setStory(createLocalDevStoryMessage(), sandbox) } else { setStory(text, sandbox) } } else { - setStory(`

Failed to load the content at ${href}. Reason: ${req.status} ${req.statusText}

`, sandbox) + const errorMessage = document.createElement("p") + errorMessage.textContent = `Failed to load the content at ${href}. Reason: ${req.status} ${req.statusText}` + setStory(errorMessage, sandbox) } }) } diff --git a/packages/playground/src/pluginUtils.ts b/packages/playground/src/pluginUtils.ts index 0a5aa06fbe54..22db5fcb4dcc 100644 --- a/packages/playground/src/pluginUtils.ts +++ b/packages/playground/src/pluginUtils.ts @@ -15,7 +15,7 @@ export const createUtils = (sb: any, react: typeof React) => { const el = (str: string, elementType: string, container: Element) => { const el = document.createElement(elementType) - el.innerHTML = str + el.textContent = str container.appendChild(el) return el } diff --git a/packages/playground/src/sidebar/plugins.ts b/packages/playground/src/sidebar/plugins.ts index 755feaeeec12..67729ce5c855 100644 --- a/packages/playground/src/sidebar/plugins.ts +++ b/packages/playground/src/sidebar/plugins.ts @@ -123,15 +123,33 @@ export const optionsPlugin: PluginFactory = (i, utils) => { const label = document.createElement("label") - // Avoid XSS by someone injecting JS via the description, which is the only free text someone can use - var p = document.createElement("p") - p.appendChild(document.createTextNode(plugin.description)) - const escapedDescription = p.innerHTML - - const top = `${plugin.name} by ${plugin.author}
${escapedDescription}` - const repo = plugin.href.includes("github") ? `| repo` : "" - const bottom = `npm ${repo}` - label.innerHTML = `${top}
${bottom}` + const pluginName = document.createElement("span") + pluginName.textContent = plugin.name + label.appendChild(pluginName) + + label.appendChild(document.createTextNode(" by ")) + + const author = document.createElement("a") + author.href = `https://www.npmjs.com/~${plugin.author}` + author.textContent = plugin.author + label.appendChild(author) + + label.appendChild(document.createElement("br")) + label.appendChild(document.createTextNode(plugin.description)) + label.appendChild(document.createElement("br")) + + const npm = document.createElement("a") + npm.href = `https://www.npmjs.com/package/${plugin.id}` + npm.textContent = "npm" + label.appendChild(npm) + + if (plugin.href.includes("github")) { + label.appendChild(document.createTextNode(" | ")) + const repo = document.createElement("a") + repo.href = plugin.href + repo.textContent = "repo" + label.appendChild(repo) + } const key = "plugin-" + plugin.id const input = document.createElement("input") diff --git a/packages/playground/src/sidebar/runtime.ts b/packages/playground/src/sidebar/runtime.ts index f93a003ddabb..a26c9cd54ae5 100644 --- a/packages/playground/src/sidebar/runtime.ts +++ b/packages/playground/src/sidebar/runtime.ts @@ -2,14 +2,40 @@ import { PlaygroundPlugin, PluginFactory } from ".." import { createUI } from "../createUI" import { localize } from "../localizeWithFallback" -let allLogs: string[] = [] +type LogEntry = { + id: string + name: string + text: string +} + +let allLogs: LogEntry[] = [] let addedClearAction = false -const cancelButtonSVG = ` - - - - -` + +const createCancelButtonSVG = () => { + const svgNamespace = "http://www.w3.org/2000/svg" + const svg = document.createElementNS(svgNamespace, "svg") + svg.setAttribute("width", "13") + svg.setAttribute("height", "13") + svg.setAttribute("viewBox", "0 0 13 13") + svg.setAttribute("fill", "none") + + const circle = document.createElementNS(svgNamespace, "circle") + circle.setAttribute("cx", "6") + circle.setAttribute("cy", "7") + circle.setAttribute("r", "5") + circle.setAttribute("stroke-width", "2") + svg.appendChild(circle) + + const line = document.createElementNS(svgNamespace, "line") + line.setAttribute("x1", "0.707107") + line.setAttribute("y1", "1.29289") + line.setAttribute("x2", "11.7071") + line.setAttribute("y2", "12.2929") + line.setAttribute("stroke-width", "2") + svg.appendChild(line) + + return svg +} export const runPlugin: PluginFactory = (i, utils) => { const plugin: PlaygroundPlugin = { @@ -43,7 +69,7 @@ export const runPlugin: PluginFactory = (i, utils) => { const logs = document.createElement("div") logs.id = "log" - logs.innerHTML = allLogs.join("
") + renderLogs(logs, allLogs) errorUL.appendChild(logs) const logToolsContainer = document.createElement("div") @@ -52,7 +78,7 @@ export const runPlugin: PluginFactory = (i, utils) => { const clearLogsButton = document.createElement("div") clearLogsButton.id = "clear-logs-button" - clearLogsButton.innerHTML = cancelButtonSVG + clearLogsButton.appendChild(createCancelButtonSVG()) clearLogsButton.onclick = e => { e.preventDefault() clearLogsAction.run() @@ -69,12 +95,10 @@ export const runPlugin: PluginFactory = (i, utils) => { const inputText = e.target.value const eleLog = document.getElementById("log")! - eleLog.innerHTML = allLogs - .filter(log => { - const userLoggedText = log.substring(log.indexOf(":") + 1, log.indexOf(" 
")) - return userLoggedText.includes(inputText) - }) - .join("
") + renderLogs( + eleLog, + allLogs.filter(log => log.text.includes(inputText)) + ) if (inputText === "") { const logContainer = document.getElementById("log-container")! @@ -168,10 +192,9 @@ function rewireLoggingToElement( obj[name] = function (...objs: any[]) { const output = produceOutput(objs) const eleLog = eleLocator() - const prefix = `[${id}]: ` const eleContainerLog = eleOverflowLocator() - allLogs.push(`${prefix}${output}
`) - eleLog.innerHTML = allLogs.join("
") + allLogs.push({ id, name, text: output }) + renderLogs(eleLog, allLogs) if (autoScroll && eleContainerLog) { eleContainerLog.scrollTop = eleContainerLog.scrollHeight } @@ -179,44 +202,35 @@ function rewireLoggingToElement( } } - // Inline constants which are switched out at the end of processing - const replacers = { - "null": "1231232131231231423434534534", - "undefined": "4534534534563567567567", - ", ": "785y8345873485763874568734y535438", - } - const objectToText = (arg: any): string => { const isObj = typeof arg === "object" let textRep = "" if (arg && arg.stack && arg.message) { - // special case for err - textRep = htmlEscape(arg.message) + textRep = arg.message } else if (arg === null) { - textRep = replacers["null"] + textRep = "null" } else if (arg === undefined) { - textRep = replacers["undefined"] + textRep = "undefined" } else if (typeof arg === "symbol") { - textRep = `${htmlEscape(String(arg))}` + textRep = String(arg) } else if (Array.isArray(arg)) { - textRep = "[" + arg.map(objectToText).join(replacers[", "]) + "]" + textRep = "[" + arg.map(objectToText).join(", ") + "]" } else if (arg instanceof Set) { const setIter = [...arg] - textRep = `Set (${arg.size}) {` + setIter.map(objectToText).join(replacers[", "]) + "}" + textRep = `Set (${arg.size}) {` + setIter.map(objectToText).join(", ") + "}" } else if (arg instanceof Map) { const mapIter = [...arg.entries()] textRep = `Map (${arg.size}) {` + mapIter .map(([k, v]) => `${objectToText(k)} => ${objectToText(v)}`) - .join(replacers[", "]) + + .join(", ") + "}" } else if (typeof arg === "string") { - textRep = '"' + htmlEscape(arg) + '"' + textRep = '"' + arg + '"' } else if (isObj) { const name = arg.constructor && arg.constructor.name || "" - // No one needs to know an obj is an obj - const nameWithoutObject = name && name === "Object" ? "" : htmlEscape(name) + const nameWithoutObject = name && name === "Object" ? "" : name const prefix = nameWithoutObject ? `${nameWithoutObject}: ` : "" textRep = @@ -231,27 +245,19 @@ function rewireLoggingToElement( }, 2 ).replace(/"__undefined__"/g, "undefined") - - textRep = htmlEscape(textRep) } else { - textRep = htmlEscape(String(arg)) + textRep = String(arg) } return textRep } function produceOutput(args: any[]) { - let result: string = args.reduce((output: any, arg: any, index) => { + return args.reduce((output: any, arg: any, index) => { const textRep = objectToText(arg) const showComma = index !== args.length - 1 - const comma = showComma ? ", " : "" + const comma = showComma ? ", " : "" return output + textRep + comma + " " }, "") - - Object.keys(replacers).forEach(k => { - result = result.replace(new RegExp((replacers as any)[k], "g"), k) - }) - - return result } } @@ -260,6 +266,20 @@ function sanitizeJS(code: string) { return code.replace(`import "reflect-metadata"`, "").replace(`require("reflect-metadata")`, "") } -function htmlEscape(str: string) { - return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """) +function renderLogs(container: Element, logs: LogEntry[]) { + container.textContent = "" + + logs.forEach((log, index) => { + if (index > 0) container.appendChild(document.createElement("hr")) + + container.appendChild(document.createTextNode("[")) + + const id = document.createElement("span") + id.className = "log-" + log.name + id.textContent = log.id + container.appendChild(id) + + container.appendChild(document.createTextNode("]: " + log.text)) + container.appendChild(document.createElement("br")) + }) } diff --git a/packages/typescriptlang-org/src/components/handbook/Contributors.tsx b/packages/typescriptlang-org/src/components/handbook/Contributors.tsx index 2ad829d9906a..c82befe3bff3 100644 --- a/packages/typescriptlang-org/src/components/handbook/Contributors.tsx +++ b/packages/typescriptlang-org/src/components/handbook/Contributors.tsx @@ -41,7 +41,7 @@ export const Contributors = (props: ContributorsProps) => { if (!t) return; const pageLoadIndicator = document.querySelector("#page-loaded-time"); - if (pageLoadIndicator?.innerHTML.includes("This page")) return; + if (pageLoadIndicator?.textContent?.includes("This page")) return; const start = t.navigationStart; const end = t.domInteractive; @@ -51,8 +51,8 @@ export const Contributors = (props: ContributorsProps) => { if (loadTime < 0) return; if (pageLoadIndicator) { - pageLoadIndicator.innerHTML = "This page loaded in " + loadTime + - " seconds.

"; + pageLoadIndicator.textContent = "This page loaded in " + loadTime + + " seconds."; } }, []); diff --git a/packages/typescriptlang-org/src/components/workbench/plugins/about.ts b/packages/typescriptlang-org/src/components/workbench/plugins/about.ts index 9e8e866e6081..1f32846c300d 100644 --- a/packages/typescriptlang-org/src/components/workbench/plugins/about.ts +++ b/packages/typescriptlang-org/src/components/workbench/plugins/about.ts @@ -2,25 +2,10 @@ type Sandbox = import("@typescript/sandbox").Sandbox type Factory = import("../../../../static/js/playground").PluginFactory type PluginUtils = import("../../../../static/js/playground").PluginUtils -const intro = ` -The bug workbench uses Twoslash to help you create accurate bug reports. -Twoslash is a markup format for TypeScript files which lets you highlight code, handle-multiple files and -show the files the TypeScript compiler creates. -`.trim() - const why = ` The bug workbench lets you make reproductions of bugs which are trivial to verify against many different versions of TypeScript over time. `.trim() -const how = ` -A repro can highlight an issue in a few ways: - -`.trim() - const cta = ` To learn how the tools for making a repro, go to "Docs" @@ -34,10 +19,19 @@ export const workbenchHelpPlugin: Factory = (i, utils) => { const ds = utils.createDesignSystem(container) ds.title("Twoslash Overview") - ds.p(intro) + ds.pWithChildren( + "The bug workbench uses ", + ds.link("https://www.npmjs.com/package/@typescript/twoslash", "Twoslash"), + " to help you create accurate bug reports. Twoslash is a markup format for TypeScript files which lets you highlight code, handle-multiple files and show the files the TypeScript compiler creates." + ) ds.p(why) - ds.p(how) + ds.p("A repro can highlight an issue in a few ways:") + ds.unorderedList( + ["Does this code sample fail to compile?"], + ["Is a type wrong at a position in the file?"], + ["Is the .js/.d.ts/.map file wrong?"] + ) ds.p(cta) }, } diff --git a/packages/typescriptlang-org/src/components/workbench/plugins/debug.ts b/packages/typescriptlang-org/src/components/workbench/plugins/debug.ts index 59b03285e531..11fb8a7d2f07 100644 --- a/packages/typescriptlang-org/src/components/workbench/plugins/debug.ts +++ b/packages/typescriptlang-org/src/components/workbench/plugins/debug.ts @@ -24,8 +24,10 @@ export const workbenchDebugPlugin: PluginFactory = (i, utils) => { const ds = utils.createDesignSystem(pluginContainer) ds.clear() - ds.p( - "This tab shows the raw data passed back from Twoslash. This can be useful in debugging if something isn't working as you would expect. That said, if you're struggling with a repro - ask in the #compiler-api channel of the TypeScript Discord." + ds.pWithChildren( + "This tab shows the raw data passed back from Twoslash. This can be useful in debugging if something isn't working as you would expect. That said, if you're struggling with a repro - ask in the ", + ds.link("https://discord.gg/typescript", "#compiler-api channel of the TypeScript Discord"), + "." ) ds.subtitle(`Output Code as ${results.extension}`) @@ -45,7 +47,8 @@ export const workbenchDebugPlugin: PluginFactory = (i, utils) => { if (filename.startsWith("/lib.")) { dtsFiles.push(filename.replace("/lib", "lib")) } else { - ds.p("" + filename + "") + const filenameElement = ds.p(filename) + filenameElement.style.fontWeight = "bold" ds.code(dtsMap.get(filename)!.trim()) } }) diff --git a/packages/typescriptlang-org/src/components/workbench/plugins/docs.ts b/packages/typescriptlang-org/src/components/workbench/plugins/docs.ts index d57e5e422f46..e31f9b747566 100644 --- a/packages/typescriptlang-org/src/components/workbench/plugins/docs.ts +++ b/packages/typescriptlang-org/src/components/workbench/plugins/docs.ts @@ -4,12 +4,18 @@ type PluginUtils = import("../../../../static/js/playground").PluginUtils import tsconfigOptions from "../../../../../tsconfig-reference/output/en-summary.json" +type DesignSystem = ReturnType + const examples = [ { issue: 37231, name: "Incorrect Type Inference Example", - blurb: - "Using // ^? to highlight how inference gives different results at different locations", + blurb: (ds: DesignSystem) => + ds.pWithChildren( + "Using ", + ds.inlineCode("// ^?"), + " to highlight how inference gives different results at different locations" + ), code: `// @noImplicitAny: false type Entity = { @@ -44,21 +50,33 @@ const reference: { content: ( sandbox: Sandbox, container: HTMLDivElement, - ds: ReturnType + ds: DesignSystem ) => void }[] = [ { name: "Compiler Options", content: (sandbox, container, ds) => { - ds.p(` -You can set compiler flags via // @[option] comments inside the sample. - -`) + ds.pWithChildren( + "You can set compiler flags via ", + ds.inlineCode("// @[option]"), + " comments inside the sample." + ) + ds.unorderedList( + [ + "Booleans: ", + ds.inlineCode("// @strict: true"), + " or ", + ds.inlineCode("// @strict: false"), + ".", + ds.lineBreak(), + "You can omit ", + ds.inlineCode(": true"), + " to get the same behavior.", + ], + ["Strings: ", ds.inlineCode("// @target: ES2015")], + ["Numbers: ", ds.inlineCode("// @target: 4")], + ["Lists: ", ds.inlineCode("// @types: ['jest']")] + ) ds.subtitle("Compiler Option Reference") tsconfigOptions.options @@ -67,15 +85,17 @@ You can set compiler flags via // @[option] comments inside the sam const skip = ["Project_Files_0", "Watch_Options_999"] if (skip.includes(opt.categoryID)) return - ds.p(`// @${opt.id}
${opt.oneliner}.`) + ds.pWithChildren(ds.inlineCode(`// @${opt.id}`), ds.lineBreak(), `${opt.oneliner}.`) }) }, }, { name: "Multi File", content: (sandbox, container, ds) => { - ds.p( - "The code file can be converted into multiple files behind the scenes. This is done by chopping the code sample whenever there is a // @filename: [path]." + ds.pWithChildren( + "The code file can be converted into multiple files behind the scenes. This is done by chopping the code sample whenever there is a ", + ds.inlineCode("// @filename: [path]"), + "." ) ds.code( @@ -164,11 +184,14 @@ document.body.appendChild(button); { name: "Emitter", content: (sandbox, container, ds) => { - ds.p( - ` -There are ways to have your test repro be about the output of running TypeScript. There are two comment types which can be used to highlight these files. -

// @showEmit is a shortcut for showing the .js file for a single file code sample: -`.trim() + ds.pWithChildren( + "There are ways to have your test repro be about the output of running TypeScript. There are two comment types which can be used to highlight these files.", + ds.lineBreak(), + ds.lineBreak(), + ds.inlineCode("// @showEmit"), + " is a shortcut for showing the ", + ds.inlineCode(".js"), + " file for a single file code sample:" ) ds.code( ` @@ -176,8 +199,10 @@ There are ways to have your test repro be about the output of running TypeScript export const helloWorld: string = "Hi" `.trim() ) - ds.p( - `The long-form is // @showEmittedFile: [filename] which allows for showing any emitted file` + ds.pWithChildren( + "The long-form is ", + ds.inlineCode("// @showEmittedFile: [filename]"), + " which allows for showing any emitted file" ) ds.code( ` @@ -220,8 +245,10 @@ const abc = "" } `) }) - ds.p( - "You may need to undo strict for some samples, but the others shouldn't affect most code repros." + ds.pWithChildren( + "You may need to undo ", + ds.inlineCode("strict"), + " for some samples, but the others shouldn't affect most code repros." ) }, }, @@ -232,9 +259,12 @@ const abc = "" "Note: this section is tricky to document... These bugs may have been fixed since the docs were created. Consider theses as ideas in how to make repros rather than useful bug reproductions." ) examples.forEach(e => { - // prettier-ignore - ds.subtitle(e.name + ` ${e.issue}`) - ds.p(e.blurb) + ds.subtitleWithChildren( + e.name, + " ", + ds.link(`https://github.com/microsoft/TypeScript/issues/${e.issue}`, String(e.issue)) + ) + e.blurb(ds) const button = document.createElement("button") button.textContent = "Show example" button.onclick = () => sandbox.setText(e.code) diff --git a/packages/typescriptlang-org/src/pages/dev/twoslash.tsx b/packages/typescriptlang-org/src/pages/dev/twoslash.tsx index 697abff95a3b..56123f46859d 100755 --- a/packages/typescriptlang-org/src/pages/dev/twoslash.tsx +++ b/packages/typescriptlang-org/src/pages/dev/twoslash.tsx @@ -127,8 +127,6 @@ const Index: React.FC = props => { document.getElementById("twoslash-failure")!.style.display = "none" - document.getElementById("twoslash-results")!.innerHTML = html - // Remove all the kids while (results.firstChild) { results.removeChild(results.firstChild) diff --git a/packages/typescriptlang-org/src/templates/scripts/setupLikeDislikeButtons.ts b/packages/typescriptlang-org/src/templates/scripts/setupLikeDislikeButtons.ts index 1c17f4cbf070..8b07566278c5 100644 --- a/packages/typescriptlang-org/src/templates/scripts/setupLikeDislikeButtons.ts +++ b/packages/typescriptlang-org/src/templates/scripts/setupLikeDislikeButtons.ts @@ -9,8 +9,15 @@ export const setupLikeDislikeButtons = (slug: string, i: any) => { const textSectionNav = document.getElementById("like-dislike-subnav")! const popoverPopup = document.getElementById("page-helpful-popup")! - textSectionNav.innerHTML = `
${newContent}
` - popoverPopup.innerHTML = `

${newContent}

` + textSectionNav.textContent = "" + const thanksHeader = document.createElement("h5") + thanksHeader.textContent = newContent + textSectionNav.appendChild(thanksHeader) + + popoverPopup.textContent = "" + const thanksParagraph = document.createElement("p") + thanksParagraph.textContent = newContent + popoverPopup.appendChild(thanksParagraph) } likeButton.onclick = clicked("Liked Page") From fc34100ae3bc2c9d064a75d49e4bc3a54ff19f02 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:37:53 -0700 Subject: [PATCH 2/2] Stop suggesting bad code --- .../template/src/vendor/utils.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/create-typescript-playground-plugin/template/src/vendor/utils.ts b/packages/create-typescript-playground-plugin/template/src/vendor/utils.ts index 614e8b30a2a4..4a5932f2a5d0 100644 --- a/packages/create-typescript-playground-plugin/template/src/vendor/utils.ts +++ b/packages/create-typescript-playground-plugin/template/src/vendor/utils.ts @@ -5,10 +5,3 @@ export const requireURL = (path: string) => { const prefix = isDev ? 'local/' : 'unpkg/typescript-playground-presentation-mode/dist/' return prefix + path } - -/** Use this to make a few dumb element generation funcs */ -export const el = (str: string, el: string, container: Element) => { - const para = document.createElement(el) - para.textContent = str - container.appendChild(para) -}