diff --git a/.prettierrc b/.prettierrc index f8ba1865993c..433079fa2a81 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,6 @@ { "semi": false, "singleQuote": false, - "printWidth": 120 + "printWidth": 120, + "arrowParens": "avoid" } diff --git a/package.json b/package.json index 228a04af9f80..dede8072e026 100644 --- a/package.json +++ b/package.json @@ -36,12 +36,6 @@ "clean": "yarn workspace typescriptlang-org gatsby clean", "test": "CI=true yarn workspaces run test" }, - "prettier": { - "printWidth": 120, - "semi": false, - "singleQuote": true, - "trailingComma": "es5" - }, "dependencies": { "serve-handler": "^6.1.2" }, diff --git a/packages/create-typescript-playground-plugin/template/package.json b/packages/create-typescript-playground-plugin/template/package.json index 52eb124d1b62..958f44795f60 100644 --- a/packages/create-typescript-playground-plugin/template/package.json +++ b/packages/create-typescript-playground-plugin/template/package.json @@ -3,6 +3,7 @@ "version": "0.0.1", "main": "dist/index.js", "license": "MIT", + "keywords":["playground-plugin"], "scripts": { "build": "rollup -c rollup.config.js;", "compile": "tsc", diff --git a/packages/gatsby-remark-shiki-twoslash/src/renderer.ts b/packages/gatsby-remark-shiki-twoslash/src/renderer.ts index e6a6f7f40dbb..b94ebb384920 100644 --- a/packages/gatsby-remark-shiki-twoslash/src/renderer.ts +++ b/packages/gatsby-remark-shiki-twoslash/src/renderer.ts @@ -1,10 +1,10 @@ // This started as a JS port of https://github.com/octref/shiki/blob/master/packages/shiki/src/renderer.ts -type Lines = import('shiki').IThemedToken[][] -type Options = import('shiki/dist/renderer').HtmlRendererOptions -type TwoSlash = import('@typescript/twoslash').TwoSlashReturn +type Lines = import("shiki").IThemedToken[][] +type Options = import("shiki/dist/renderer").HtmlRendererOptions +type TwoSlash = import("@typescript/twoslash").TwoSlashReturn -import { stripHTML, createHighlightedString2 } from './utils' +import { stripHTML, createHighlightedString2 } from "./utils" // OK, so - this is just straight up complex code. @@ -30,7 +30,7 @@ export function renderToHTML(lines: Lines, options: Options, twoslash?: TwoSlash return plainOleShikiRenderer(lines, options) } - let html = '' + let html = "" html += `
`
   if (options.langId) {
@@ -38,10 +38,10 @@ export function renderToHTML(lines: Lines, options: Options, twoslash?: TwoSlash
   }
   html += `
` - const errorsGroupedByLine = groupBy(twoslash.errors, (e) => e.line) || new Map() - const staticQuickInfosGroupedByLine = groupBy(twoslash.staticQuickInfos, (q) => q.line) || new Map() + const errorsGroupedByLine = groupBy(twoslash.errors, e => e.line) || new Map() + const staticQuickInfosGroupedByLine = groupBy(twoslash.staticQuickInfos, q => q.line) || new Map() // A query is always about the line above it! - const queriesGroupedByLine = groupBy(twoslash.queries, (q) => q.line - 1) || new Map() + const queriesGroupedByLine = groupBy(twoslash.queries, q => q.line - 1) || new Map() let filePos = 0 lines.forEach((l, i) => { @@ -60,8 +60,8 @@ export function renderToHTML(lines: Lines, options: Options, twoslash?: TwoSlash // errors and lang serv identifiers let tokenPos = 0 - l.forEach((token) => { - let tokenContent = '' + l.forEach(token => { + let tokenContent = "" // Underlining particular words const findTokenFunc = (start: number) => (e: any) => start <= e.character && start + token.content.length >= e.character + e.length @@ -71,8 +71,8 @@ export function renderToHTML(lines: Lines, options: Options, twoslash?: TwoSlash // prettier-ignore console.log(result, start, '<=', e.character, '&&', start + token.content.length, '<=', e.character + e.length) if (result) { - console.log('Found:', e) - console.log('Inside:', token) + console.log("Found:", e) + console.log("Inside:", token) } return result } @@ -87,7 +87,7 @@ export function renderToHTML(lines: Lines, options: Options, twoslash?: TwoSlash }) if (allTokensByStart.length) { - const ranges = allTokensByStart.map((token) => { + const ranges = allTokensByStart.map(token => { const range: any = { begin: token.start! - filePos, end: token.start! + token.length! - filePos, @@ -101,11 +101,11 @@ export function renderToHTML(lines: Lines, options: Options, twoslash?: TwoSlash // throw new Error(`The begin range of a token is at a minus location, filePos:${filePos} current token: ${JSON.stringify(token, null, ' ')}\n result: ${JSON.stringify(range, null, ' ')}`) } - if ('renderedMessage' in token) range.classes = 'err' - if ('kind' in token) range.classes = token.kind - if ('targetString' in token) { - range.classes = 'lsp' - range['lsp'] = stripHTML(token.text) + if ("renderedMessage" in token) range.classes = "err" + if ("kind" in token) range.classes = token.kind + if ("targetString" in token) { + range.classes = "lsp" + range["lsp"] = stripHTML(token.text) } return range }) @@ -126,34 +126,34 @@ export function renderToHTML(lines: Lines, options: Options, twoslash?: TwoSlash // Adding error messages to the line after if (errors.length) { - const messages = errors.map((e) => escapeHtml(e.renderedMessage)).join('
') - const codes = errors.map((e) => e.code).join('
') + const messages = errors.map(e => escapeHtml(e.renderedMessage)).join("
") + const codes = errors.map(e => e.code).join("
") html += `${messages}${codes}` html += `${messages}` } // Add queries to the next line if (queries.length) { - queries.forEach((query) => { - html += `${'//' + ''.padStart(query.offset - 2) + '^ = ' + query.text}` + queries.forEach(query => { + html += `${"//" + "".padStart(query.offset - 2) + "^ = " + query.text}` }) - html += '\n' + html += "\n" } }) - html = html.replace(/\n*$/, '') // Get rid of final new lines + html = html.replace(/\n*$/, "") // Get rid of final new lines html += `
` return html } function escapeHtml(html: string) { - return html.replace(//g, '>') + return html.replace(//g, ">") } /** Returns a map where all the keys are the value in keyGetter */ function groupBy(list: T[], keyGetter: (obj: any) => number) { const map = new Map() - list.forEach((item) => { + list.forEach(item => { const key = keyGetter(item) const collection = map.get(key) if (!collection) { @@ -166,7 +166,7 @@ function groupBy(list: T[], keyGetter: (obj: any) => number) { } export function plainOleShikiRenderer(lines: Lines, options: Options) { - let html = '' + let html = "" html += `
`
   if (options.langId) {
@@ -175,18 +175,18 @@ export function plainOleShikiRenderer(lines: Lines, options: Options) {
 
   html += `
` - lines.forEach((l) => { + lines.forEach(l => { if (l.length === 0) { html += `\n` } else { - l.forEach((token) => { + l.forEach(token => { html += `${escapeHtml(token.content)}` }) html += `\n` } }) - html = html.replace(/\n*$/, '') // Get rid of final new lines + html = html.replace(/\n*$/, "") // Get rid of final new lines html += `
` return html } diff --git a/packages/playground-examples/.prettierrc b/packages/playground-examples/.prettierrc index 7defe98f8ef9..87df7b8f867f 100644 --- a/packages/playground-examples/.prettierrc +++ b/packages/playground-examples/.prettierrc @@ -4,5 +4,6 @@ "singleQuote": false, "tabWidth": 2, "printWidth": 120, - "trailingComma": "es5" + "trailingComma": "es5", + "arrowParens": "avoid" } diff --git a/packages/playground/src/createElements.ts b/packages/playground/src/createElements.ts index 915c21687b29..3849fd2f7d89 100644 --- a/packages/playground/src/createElements.ts +++ b/packages/playground/src/createElements.ts @@ -1,10 +1,10 @@ -import { PlaygroundPlugin } from '.' +import { PlaygroundPlugin } from "." -type Sandbox = import('typescript-sandbox').Sandbox +type Sandbox = import("typescript-sandbox").Sandbox export const createDragBar = () => { - const sidebar = document.createElement('div') - sidebar.className = 'playground-dragbar' + const sidebar = document.createElement("div") + sidebar.className = "playground-dragbar" let left: HTMLElement, right: HTMLElement const drag = (e: MouseEvent) => { @@ -23,8 +23,8 @@ export const createDragBar = () => { // Save the x coordinate of the if (window.localStorage) { - window.localStorage.setItem('dragbar-x', '' + clampedOffset) - window.localStorage.setItem('dragbar-window-width', '' + window.innerWidth) + window.localStorage.setItem("dragbar-x", "" + clampedOffset) + window.localStorage.setItem("dragbar-window-width", "" + window.innerWidth) } // @ts-ignore - I know what I'm doing @@ -36,19 +36,19 @@ export const createDragBar = () => { } } - sidebar.addEventListener('mousedown', e => { - left = document.getElementById('editor-container')! - right = sidebar.parentElement?.getElementsByClassName('playground-sidebar').item(0)! as any + sidebar.addEventListener("mousedown", e => { + left = document.getElementById("editor-container")! + right = sidebar.parentElement?.getElementsByClassName("playground-sidebar").item(0)! as any // Handle dragging all over the screen - document.addEventListener('mousemove', drag) + document.addEventListener("mousemove", drag) // Remove it when you lt go anywhere - document.addEventListener('mouseup', () => { - document.removeEventListener('mousemove', drag) - document.body.style.userSelect = 'auto' + document.addEventListener("mouseup", () => { + document.removeEventListener("mousemove", drag) + document.body.style.userSelect = "auto" }) // Don't allow the drag to select text accidentally - document.body.style.userSelect = 'none' + document.body.style.userSelect = "none" e.stopPropagation() e.cancelBubble = true }) @@ -56,25 +56,25 @@ export const createDragBar = () => { return sidebar } -export const sidebarHidden = () => !!window.localStorage.getItem('sidebar-hidden') +export const sidebarHidden = () => !!window.localStorage.getItem("sidebar-hidden") export const createSidebar = () => { - const sidebar = document.createElement('div') - sidebar.className = 'playground-sidebar' + const sidebar = document.createElement("div") + sidebar.className = "playground-sidebar" // Start with the sidebar hidden on small screens const isTinyScreen = window.innerWidth < 800 // This is independent of the sizing below so that you keep the same sized sidebar if (isTinyScreen || sidebarHidden()) { - sidebar.style.display = 'none' + sidebar.style.display = "none" } - if (window.localStorage && window.localStorage.getItem('dragbar-x')) { + if (window.localStorage && window.localStorage.getItem("dragbar-x")) { // Don't restore the x pos if the window isn't the same size - if (window.innerWidth === Number(window.localStorage.getItem('dragbar-window-width'))) { + if (window.innerWidth === Number(window.localStorage.getItem("dragbar-window-width"))) { // Set the dragger to the previous x pos - let width = window.localStorage.getItem('dragbar-x') + let width = window.localStorage.getItem("dragbar-x") if (isTinyScreen) { width = String(Math.min(Number(width), 280)) @@ -84,7 +84,7 @@ export const createSidebar = () => { sidebar.style.flexBasis = `${width}px` sidebar.style.maxWidth = `${width}px` - const left = document.getElementById('editor-container')! + const left = document.getElementById("editor-container")! left.style.width = `calc(100% - ${width}px)` } } @@ -92,30 +92,30 @@ export const createSidebar = () => { return sidebar } -const toggleIconWhenOpen = '⇥' -const toggleIconWhenClosed = '⇤' +const toggleIconWhenOpen = "⇥" +const toggleIconWhenClosed = "⇤" export const setupSidebarToggle = () => { - const toggle = document.getElementById('sidebar-toggle')! + const toggle = document.getElementById("sidebar-toggle")! const updateToggle = () => { - const sidebar = window.document.querySelector('.playground-sidebar') as HTMLDivElement - const sidebarShowing = sidebar.style.display !== 'none' + const sidebar = window.document.querySelector(".playground-sidebar") as HTMLDivElement + const sidebarShowing = sidebar.style.display !== "none" toggle.innerHTML = sidebarShowing ? toggleIconWhenOpen : toggleIconWhenClosed - toggle.setAttribute('aria-label', sidebarShowing ? 'Hide Sidebar' : 'Show Sidebar') + toggle.setAttribute("aria-label", sidebarShowing ? "Hide Sidebar" : "Show Sidebar") } toggle.onclick = () => { - const sidebar = window.document.querySelector('.playground-sidebar') as HTMLDivElement - const newState = sidebar.style.display !== 'none' + const sidebar = window.document.querySelector(".playground-sidebar") as HTMLDivElement + const newState = sidebar.style.display !== "none" if (newState) { - localStorage.setItem('sidebar-hidden', 'true') - sidebar.style.display = 'none' + localStorage.setItem("sidebar-hidden", "true") + sidebar.style.display = "none" } else { - localStorage.removeItem('sidebar-hidden') - sidebar.style.display = 'block' + localStorage.removeItem("sidebar-hidden") + sidebar.style.display = "block" } updateToggle() @@ -131,19 +131,19 @@ export const setupSidebarToggle = () => { } export const createTabBar = () => { - const tabBar = document.createElement('div') - tabBar.classList.add('playground-plugin-tabview') + const tabBar = document.createElement("div") + tabBar.classList.add("playground-plugin-tabview") return tabBar } export const createPluginContainer = () => { - const container = document.createElement('div') - container.classList.add('playground-plugin-container') + const container = document.createElement("div") + container.classList.add("playground-plugin-container") return container } export const createTabForPlugin = (plugin: PlaygroundPlugin) => { - const element = document.createElement('button') + const element = document.createElement("button") element.textContent = plugin.displayName return element } @@ -163,13 +163,13 @@ export const activatePlugin = ( } // @ts-ignore - if (!newPluginTab) throw new Error('Could not get a tab for the plugin: ' + plugin.displayName) + if (!newPluginTab) throw new Error("Could not get a tab for the plugin: " + plugin.displayName) // Tell the old plugin it's getting the boot // @ts-ignore if (previousPlugin && oldPluginTab) { if (previousPlugin.willUnmount) previousPlugin.willUnmount(sandbox, container) - oldPluginTab.classList.remove('active') + oldPluginTab.classList.remove("active") } // Wipe the sidebar @@ -178,12 +178,12 @@ export const activatePlugin = ( } // Start booting up the new plugin - newPluginTab.classList.add('active') + newPluginTab.classList.add("active") // Tell the new plugin to start doing some work if (plugin.willMount) plugin.willMount(sandbox, container) - if (plugin.modelChanged) plugin.modelChanged(sandbox, sandbox.getModel()) - if (plugin.modelChangedDebounce) plugin.modelChangedDebounce(sandbox, sandbox.getModel()) + if (plugin.modelChanged) plugin.modelChanged(sandbox, sandbox.getModel(), container) + if (plugin.modelChangedDebounce) plugin.modelChangedDebounce(sandbox, sandbox.getModel(), container) if (plugin.didMount) plugin.didMount(sandbox, container) // Let the previous plugin do any slow work after it's all done diff --git a/packages/playground/src/index.ts b/packages/playground/src/index.ts index 2cd0261ae7ec..0fc0056ff168 100644 --- a/packages/playground/src/index.ts +++ b/packages/playground/src/index.ts @@ -1,9 +1,9 @@ -type Sandbox = import('typescript-sandbox').Sandbox -type Monaco = typeof import('monaco-editor') +type Sandbox = import("typescript-sandbox").Sandbox +type Monaco = typeof import("monaco-editor") declare const window: any -import { compiledJSPlugin } from './sidebar/showJS' +import { compiledJSPlugin } from "./sidebar/showJS" import { createSidebar, createTabForPlugin, @@ -12,23 +12,24 @@ import { activatePlugin, createDragBar, setupSidebarToggle, -} from './createElements' -import { showDTSPlugin } from './sidebar/showDTS' -import { runWithCustomLogs, runPlugin } from './sidebar/runtime' -import { createExporter } from './exporter' -import { createUI } from './createUI' -import { getExampleSourceCode } from './getExample' -import { ExampleHighlighter } from './monaco/ExampleHighlight' -import { createConfigDropdown, updateConfigDropdownForCompilerOptions } from './createConfigDropdown' -import { showErrors } from './sidebar/showErrors' -import { optionsPlugin, allowConnectingToLocalhost, activePlugins, addCustomPlugin } from './sidebar/options' -import { createUtils, PluginUtils } from './pluginUtils' -import type React from 'react' - -export { PluginUtils } from './pluginUtils' +} from "./createElements" +import { showDTSPlugin } from "./sidebar/showDTS" +import { runWithCustomLogs, runPlugin } from "./sidebar/runtime" +import { createExporter } from "./exporter" +import { createUI } from "./createUI" +import { getExampleSourceCode } from "./getExample" +import { ExampleHighlighter } from "./monaco/ExampleHighlight" +import { createConfigDropdown, updateConfigDropdownForCompilerOptions } from "./createConfigDropdown" +import { showErrors } from "./sidebar/showErrors" +import { optionsPlugin, allowConnectingToLocalhost, activePlugins, addCustomPlugin } from "./sidebar/plugins" +import { createUtils, PluginUtils } from "./pluginUtils" +import type React from "react" +import { settingsPlugin } from "./sidebar/settings" + +export { PluginUtils } from "./pluginUtils" export type PluginFactory = { - (i: (key: string, components?: any) => string): PlaygroundPlugin + (i: (key: string, components?: any) => string, utils: PluginUtils): PlaygroundPlugin } /** The interface of all sidebar plugins */ @@ -44,9 +45,13 @@ export interface PlaygroundPlugin { /** After we show the tab */ didMount?: (sandbox: Sandbox, container: HTMLDivElement) => void /** Model changes while this plugin is actively selected */ - modelChanged?: (sandbox: Sandbox, model: import('monaco-editor').editor.ITextModel) => void + modelChanged?: (sandbox: Sandbox, model: import("monaco-editor").editor.ITextModel, container: HTMLDivElement) => void /** Delayed model changes while this plugin is actively selected, useful when you are working with the TS API because it won't run on every keypress */ - modelChangedDebounce?: (sandbox: Sandbox, model: import('monaco-editor').editor.ITextModel) => void + modelChangedDebounce?: ( + sandbox: Sandbox, + model: import("monaco-editor").editor.ITextModel, + container: HTMLDivElement + ) => void /** Before we remove the tab */ willUnmount?: (sandbox: Sandbox, container: HTMLDivElement) => void /** After we remove the tab */ @@ -56,8 +61,14 @@ export interface PlaygroundPlugin { } interface PlaygroundConfig { + /** Language like "en" / "ja" etc */ lang: string + /** Site prefix, like "v2" during the pre-release */ prefix: string + /** Optional plugins so that we can re-use the playground with different sidebars */ + plugins?: PluginFactory[] + /** Should this playground load up custom plugins from localStorage? */ + supportCustomPlugins: boolean } const defaultPluginFactories: PluginFactory[] = [compiledJSPlugin, showDTSPlugin, showErrors, runPlugin, optionsPlugin] @@ -85,29 +96,39 @@ export const setupPlayground = ( const plugins = [] as PlaygroundPlugin[] const tabs = [] as HTMLButtonElement[] + // Let's things like the workbench hook into tab changes + let didUpdateTab: (newPlugin: PlaygroundPlugin, previousPlugin: PlaygroundPlugin) => void | undefined + const registerPlugin = (plugin: PlaygroundPlugin) => { plugins.push(plugin) const tab = createTabForPlugin(plugin) tabs.push(tab) - const tabClicked: HTMLElement['onclick'] = e => { - const previousPlugin = currentPlugin() + const tabClicked: HTMLElement["onclick"] = e => { + const previousPlugin = getCurrentPlugin() const newTab = e.target as HTMLElement const newPlugin = plugins.find(p => p.displayName == newTab.textContent)! activatePlugin(newPlugin, previousPlugin, sandbox, tabBar, container) + didUpdateTab && didUpdateTab(newPlugin, previousPlugin) } tabBar.appendChild(tab) tab.onclick = tabClicked } - const currentPlugin = () => { - const selectedTab = tabs.find(t => t.classList.contains('active'))! + const setDidUpdateTab = (func: (newPlugin: PlaygroundPlugin, previousPlugin: PlaygroundPlugin) => void) => { + didUpdateTab = func + } + + const getCurrentPlugin = () => { + const selectedTab = tabs.find(t => t.classList.contains("active"))! return plugins[tabs.indexOf(selectedTab)] } - const initialPlugins = defaultPluginFactories.map(f => f(i)) + const defaultPlugins = config.plugins || defaultPluginFactories + const utils = createUtils(sandbox, react) + const initialPlugins = defaultPlugins.map(f => f(i, utils)) initialPlugins.forEach(p => registerPlugin(p)) // Choose which should be selected @@ -118,8 +139,8 @@ export const setupPlayground = ( let debouncingTimer = false sandbox.editor.onDidChangeModelContent(_event => { - const plugin = currentPlugin() - if (plugin.modelChanged) plugin.modelChanged(sandbox, sandbox.getModel()) + const plugin = getCurrentPlugin() + if (plugin.modelChanged) plugin.modelChanged(sandbox, sandbox.getModel(), container) // This needs to be last in the function if (debouncingTimer) return @@ -129,33 +150,34 @@ export const setupPlayground = ( playgroundDebouncedMainFunction() // Only call the plugin function once every 0.3s - if (plugin.modelChangedDebounce && plugin.displayName === currentPlugin().displayName) { - plugin.modelChangedDebounce(sandbox, sandbox.getModel()) + if (plugin.modelChangedDebounce && plugin.displayName === getCurrentPlugin().displayName) { + console.log("Debounced", container) + plugin.modelChangedDebounce(sandbox, sandbox.getModel(), container) } }, 300) }) // Sets the URL and storage of the sandbox string const playgroundDebouncedMainFunction = () => { - const alwaysUpdateURL = !localStorage.getItem('disable-save-on-type') + const alwaysUpdateURL = !localStorage.getItem("disable-save-on-type") if (alwaysUpdateURL) { const newURL = sandbox.createURLQueryWithCompilerOptions(sandbox) - window.history.replaceState({}, '', newURL) + window.history.replaceState({}, "", newURL) } - localStorage.setItem('sandbox-history', sandbox.getText()) + localStorage.setItem("sandbox-history", sandbox.getText()) } // When any compiler flags are changed, trigger a potential change to the URL sandbox.setDidUpdateCompilerSettings(() => { playgroundDebouncedMainFunction() // @ts-ignore - window.appInsights.trackEvent({ name: 'Compiler Settings changed' }) + window.appInsights.trackEvent({ name: "Compiler Settings changed" }) const model = sandbox.editor.getModel() - const plugin = currentPlugin() - if (model && plugin.modelChanged) plugin.modelChanged(sandbox, model) - if (model && plugin.modelChangedDebounce) plugin.modelChangedDebounce(sandbox, model) + const plugin = getCurrentPlugin() + if (model && plugin.modelChanged) plugin.modelChanged(sandbox, model, container) + if (model && plugin.modelChangedDebounce) plugin.modelChangedDebounce(sandbox, model, container) }) // Setup working with the existing UI, once it's loaded @@ -163,24 +185,24 @@ export const setupPlayground = ( // Versions of TypeScript // Set up the label for the dropdown - document.querySelectorAll('#versions > a').item(0).innerHTML = 'v' + sandbox.ts.version + " " + document.querySelectorAll("#versions > a").item(0).innerHTML = "v" + sandbox.ts.version + " " // Add the versions to the dropdown - const versionsMenu = document.querySelectorAll('#versions > ul').item(0) + const versionsMenu = document.querySelectorAll("#versions > ul").item(0) + + const notWorkingInPlayground = ["3.1.6", "3.0.1", "2.8.1", "2.7.2", "2.4.1"] - const notWorkingInPlayground = ['3.1.6', '3.0.1', '2.8.1', '2.7.2', '2.4.1'] - const allVersions = [ - '3.9.0-beta', - ...sandbox.supportedVersions.filter(f => !notWorkingInPlayground.includes(f)), - 'Nightly' + "3.9.0-beta", + ...sandbox.supportedVersions.filter(f => !notWorkingInPlayground.includes(f)), + "Nightly", ] allVersions.forEach((v: string) => { - const li = document.createElement('li') - const a = document.createElement('a') + const li = document.createElement("li") + const a = document.createElement("a") a.textContent = v - a.href = '#' + a.href = "#" if (v === "Nightly") { li.classList.add("nightly") @@ -192,11 +214,11 @@ export const setupPlayground = ( li.onclick = () => { const currentURL = sandbox.createURLQueryWithCompilerOptions(sandbox) - const params = new URLSearchParams(currentURL.split('#')[0]) - const version = v === 'Nightly' ? 'next' : v - params.set('ts', version) + const params = new URLSearchParams(currentURL.split("#")[0]) + const version = v === "Nightly" ? "next" : v + params.set("ts", version) - const hash = document.location.hash.length ? document.location.hash : '' + const hash = document.location.hash.length ? document.location.hash : "" const newURL = `${document.location.protocol}//${document.location.host}${document.location.pathname}?${params}${hash}` // @ts-ignore - it is allowed @@ -208,27 +230,24 @@ export const setupPlayground = ( }) // Support dropdowns - document.querySelectorAll('.navbar-sub li.dropdown > a').forEach(link => { + document.querySelectorAll(".navbar-sub li.dropdown > a").forEach(link => { const a = link as HTMLAnchorElement a.onclick = _e => { - if (a.parentElement!.classList.contains('open')) { - document.querySelectorAll('.navbar-sub li.open').forEach(i => i.classList.remove('open')) + if (a.parentElement!.classList.contains("open")) { + document.querySelectorAll(".navbar-sub li.open").forEach(i => i.classList.remove("open")) } else { - document.querySelectorAll('.navbar-sub li.open').forEach(i => i.classList.remove('open')) - a.parentElement!.classList.toggle('open') + document.querySelectorAll(".navbar-sub li.open").forEach(i => i.classList.remove("open")) + a.parentElement!.classList.toggle("open") - const exampleContainer = a - .closest('li')! - .getElementsByTagName('ul') - .item(0)! + const exampleContainer = a.closest("li")!.getElementsByTagName("ul").item(0)! // Set exact height and widths for the popovers for the main playground navigation - const isPlaygroundSubmenu = !!a.closest('nav') + const isPlaygroundSubmenu = !!a.closest("nav") if (isPlaygroundSubmenu) { - const playgroundContainer = document.getElementById('playground-container')! + const playgroundContainer = document.getElementById("playground-container")! exampleContainer.style.height = `calc(${playgroundContainer.getBoundingClientRect().height + 26}px - 4rem)` - const sideBarWidth = (document.querySelector('.playground-sidebar') as any).offsetWidth + const sideBarWidth = (document.querySelector(".playground-sidebar") as any).offsetWidth exampleContainer.style.width = `calc(100% - ${sideBarWidth}px - 71px)` } } @@ -236,79 +255,110 @@ export const setupPlayground = ( }) // Set up some key commands - sandbox.editor.addAction({ - id: 'copy-clipboard', - label: 'Save to clipboard', - keybindings: [ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S ], - - contextMenuGroupId: 'run', + sandbox.editor.addAction({ + id: "copy-clipboard", + label: "Save to clipboard", + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S], + + contextMenuGroupId: "run", contextMenuOrder: 1.5, - - run: function(ed) { + + run: function (ed) { window.navigator.clipboard.writeText(location.href.toString()).then( - () => ui.flashInfo(i('play_export_clipboard')), + () => ui.flashInfo(i("play_export_clipboard")), (e: any) => alert(e) ) - } - }); + }, + }) + sandbox.editor.addAction({ + id: "run-js", + label: "Run the evaluated JavaScript for your TypeScript file", + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], - sandbox.editor.addAction({ - id: 'run-js', - label: 'Run the evaluated JavaScript for your TypeScript file', - keybindings: [ monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter ], - - contextMenuGroupId: 'run', + contextMenuGroupId: "run", contextMenuOrder: 1.5, - - run: function(ed) { - const runButton = document.getElementById('run-button')! - runButton.onclick && runButton.onclick({} as any) - } - }); + run: function (ed) { + const runButton = document.getElementById("run-button") + runButton && runButton.onclick && runButton.onclick({} as any) + }, + }) - const runButton = document.getElementById('run-button')! - runButton.onclick = () => { - const run = sandbox.getRunnableJS() - const runPlugin = plugins.find(p => p.id === 'logs')! - activatePlugin(runPlugin, currentPlugin(), sandbox, tabBar, container) + const runButton = document.getElementById("run-button") + if (runButton) { + runButton.onclick = () => { + const run = sandbox.getRunnableJS() + const runPlugin = plugins.find(p => p.id === "logs")! + activatePlugin(runPlugin, getCurrentPlugin(), sandbox, tabBar, container) - runWithCustomLogs(run, i) + runWithCustomLogs(run, i) - const isJS = sandbox.config.useJavaScript - ui.flashInfo(i(isJS ? 'play_run_js' : 'play_run_ts')) + const isJS = sandbox.config.useJavaScript + ui.flashInfo(i(isJS ? "play_run_js" : "play_run_ts")) + } } // Handle the close buttons on the examples - document.querySelectorAll('button.examples-close').forEach(b => { + document.querySelectorAll("button.examples-close").forEach(b => { const button = b as HTMLButtonElement button.onclick = (e: any) => { const button = e.target as HTMLButtonElement - const navLI = button.closest('li') - navLI?.classList.remove('open') + const navLI = button.closest("li") + navLI?.classList.remove("open") } }) setupSidebarToggle() - createConfigDropdown(sandbox, monaco) - updateConfigDropdownForCompilerOptions(sandbox, monaco) + if (document.getElementById("config-container")) { + createConfigDropdown(sandbox, monaco) + updateConfigDropdownForCompilerOptions(sandbox, monaco) + } + + if (document.getElementById("playground-settings")) { + const settingsToggle = document.getElementById("playground-settings")! + + settingsToggle.onclick = () => { + const open = settingsToggle.parentElement!.classList.contains("open") + const sidebarTabs = document.querySelector(".playground-plugin-tabview") as HTMLDivElement + const sidebarContent = document.querySelector(".playground-plugin-container") as HTMLDivElement + let settingsContent = document.querySelector(".playground-settings-container") as HTMLDivElement + if (!settingsContent) { + settingsContent = document.createElement("div") + settingsContent.className = "playground-settings-container playground-plugin-container" + const settings = settingsPlugin(i, utils) + settings.didMount && settings.didMount(sandbox, settingsContent) + document.querySelector(".playground-sidebar")!.appendChild(settingsContent) + } + + if (open) { + sidebarTabs.style.display = "flex" + sidebarContent.style.display = "block" + settingsContent.style.display = "none" + } else { + sidebarTabs.style.display = "none" + sidebarContent.style.display = "none" + settingsContent.style.display = "block" + } + settingsToggle.parentElement!.classList.toggle("open") + } + } // Support grabbing examples from the location hash - if (location.hash.startsWith('#example')) { - const exampleName = location.hash.replace('#example/', '').trim() - sandbox.config.logger.log('Loading example:', exampleName) + if (location.hash.startsWith("#example")) { + const exampleName = location.hash.replace("#example/", "").trim() + sandbox.config.logger.log("Loading example:", exampleName) getExampleSourceCode(config.prefix, config.lang, exampleName).then(ex => { if (ex.example && ex.code) { const { example, code } = ex // Update the localstorage showing that you've seen this page if (localStorage) { - const seenText = localStorage.getItem('examples-seen') || '{}' + const seenText = localStorage.getItem("examples-seen") || "{}" const seen = JSON.parse(seenText) seen[example.id] = example.hash - localStorage.setItem('examples-seen', JSON.stringify(seen)) + localStorage.setItem("examples-seen", JSON.stringify(seen)) } // Set the menu to be the same section as this current example @@ -321,18 +371,18 @@ export const setupPlayground = ( // } // } - const allLinks = document.querySelectorAll('example-link') + const allLinks = document.querySelectorAll("example-link") // @ts-ignore for (const link of allLinks) { if (link.textContent === example.title) { - link.classList.add('highlight') + link.classList.add("highlight") } } - document.title = 'TypeScript Playground - ' + example.title + document.title = "TypeScript Playground - " + example.title sandbox.setText(code) } else { - sandbox.setText('// There was an issue getting the example, bad URL? Check the console in the developer tools') + sandbox.setText("// There was an issue getting the example, bad URL? Check the console in the developer tools") } }) } @@ -340,16 +390,20 @@ export const setupPlayground = ( // Sets up a way to click between examples monaco.languages.registerLinkProvider(sandbox.language, new ExampleHighlighter()) - const languageSelector = document.getElementById('language-selector')! as HTMLSelectElement - const params = new URLSearchParams(location.search) - languageSelector.options.selectedIndex = params.get('useJavaScript') ? 1 : 0 + const languageSelector = document.getElementById("language-selector") as HTMLSelectElement + if (languageSelector) { + const params = new URLSearchParams(location.search) + languageSelector.options.selectedIndex = params.get("useJavaScript") ? 1 : 0 - languageSelector.onchange = () => { - const useJavaScript = languageSelector.value === 'JavaScript' - const query = sandbox.createURLQueryWithCompilerOptions(sandbox, { useJavaScript: useJavaScript ? true : undefined }) - const fullURL = `${document.location.protocol}//${document.location.host}${document.location.pathname}${query}` - // @ts-ignore - document.location = fullURL + languageSelector.onchange = () => { + const useJavaScript = languageSelector.value === "JavaScript" + const query = sandbox.createURLQueryWithCompilerOptions(sandbox, { + useJavaScript: useJavaScript ? true : undefined, + }) + const fullURL = `${document.location.protocol}//${document.location.host}${document.location.pathname}${query}` + // @ts-ignore + document.location = fullURL + } } const ui = createUI() @@ -359,6 +413,10 @@ export const setupPlayground = ( exporter, ui, registerPlugin, + plugins, + getCurrentPlugin, + tabs, + setDidUpdateTab, } window.ts = sandbox.ts @@ -367,13 +425,12 @@ export const setupPlayground = ( console.log(`Using TypeScript ${window.ts.version}`) - console.log('Available globals:') - console.log('\twindow.ts', window.ts) - console.log('\twindow.sandbox', window.sandbox) - console.log('\twindow.playground', window.playground) - console.log('\twindow.react', window.react) - console.log('\twindow.reactDOM', window.reactDOM) - + console.log("Available globals:") + console.log("\twindow.ts", window.ts) + console.log("\twindow.sandbox", window.sandbox) + console.log("\twindow.playground", window.playground) + console.log("\twindow.react", window.react) + console.log("\twindow.reactDOM", window.reactDOM) /** A plugin */ const activateExternalPlugin = ( @@ -382,7 +439,7 @@ export const setupPlayground = ( ) => { let readyPlugin: PlaygroundPlugin // Can either be a factory, or object - if (typeof plugin === 'function') { + if (typeof plugin === "function") { const utils = createUtils(sandbox, react) readyPlugin = plugin(utils) } else { @@ -400,30 +457,30 @@ export const setupPlayground = ( if (pluginWantsFront || autoActivate) { // Auto-select the dev plugin - activatePlugin(readyPlugin, currentPlugin(), sandbox, tabBar, container) + activatePlugin(readyPlugin, getCurrentPlugin(), sandbox, tabBar, container) } } // Dev mode plugin - if (allowConnectingToLocalhost()) { + if (config.supportCustomPlugins && allowConnectingToLocalhost()) { window.exports = {} - console.log('Connecting to dev plugin') + console.log("Connecting to dev plugin") try { // @ts-ignore const re = window.require - re(['local/index'], (devPlugin: any) => { - console.log('Set up dev plugin from localhost:5000') + re(["local/index"], (devPlugin: any) => { + console.log("Set up dev plugin from localhost:5000") try { activateExternalPlugin(devPlugin, true) } catch (error) { console.error(error) setTimeout(() => { - ui.flashInfo('Error: Could not load dev plugin from localhost:5000') + ui.flashInfo("Error: Could not load dev plugin from localhost:5000") }, 700) } }) } catch (error) { - console.error('Problem loading up the dev plugin') + console.error("Problem loading up the dev plugin") console.error(error) } } @@ -436,38 +493,42 @@ export const setupPlayground = ( activateExternalPlugin(devPlugin, autoEnable) }) } catch (error) { - console.error('Problem loading up the plugin:', plugin) + console.error("Problem loading up the plugin:", plugin) console.error(error) } } - activePlugins().forEach(p => downloadPlugin(p.module, false)) + if (config.supportCustomPlugins) { + // Grab ones from localstorage + activePlugins().forEach(p => downloadPlugin(p.module, false)) + + // Offer to install one if 'install-plugin' is a query param + const params = new URLSearchParams(location.search) + const pluginToInstall = params.get("install-plugin") + if (pluginToInstall) { + const alreadyInstalled = activePlugins().find(p => p.module === pluginToInstall) + if (!alreadyInstalled) { + const shouldDoIt = confirm("Would you like to install the third party plugin?\n\n" + pluginToInstall) + if (shouldDoIt) { + addCustomPlugin(pluginToInstall) + downloadPlugin(pluginToInstall, true) + } + } + } + } - if (location.hash.startsWith('#show-examples')) { + if (location.hash.startsWith("#show-examples")) { setTimeout(() => { - document.getElementById('examples-button')?.click() + document.getElementById("examples-button")?.click() }, 100) } - if (location.hash.startsWith('#show-whatisnew')) { + if (location.hash.startsWith("#show-whatisnew")) { setTimeout(() => { - document.getElementById('whatisnew-button')?.click() + document.getElementById("whatisnew-button")?.click() }, 100) } - const pluginToInstall = params.get('install-plugin') - if (pluginToInstall) { - const alreadyInstalled = activePlugins().find(p => p.module === pluginToInstall) - console.log(activePlugins(), alreadyInstalled) - if (!alreadyInstalled) { - const shouldDoIt = confirm('Would you like to install the third party plugin?\n\n' + pluginToInstall) - if (shouldDoIt) { - addCustomPlugin(pluginToInstall) - downloadPlugin(pluginToInstall, true) - } - } - } - return playground } diff --git a/packages/playground/src/pluginUtils.ts b/packages/playground/src/pluginUtils.ts index 305710bc6f72..ba7bbd9e84f3 100644 --- a/packages/playground/src/pluginUtils.ts +++ b/packages/playground/src/pluginUtils.ts @@ -1,27 +1,184 @@ -import type { Sandbox } from 'typescript-sandbox' -import type { Node } from "typescript" -import type React from 'react' +import type { Sandbox } from "typescript-sandbox" +import { Node, DiagnosticRelatedInformation } from "typescript" +import type React from "react" /** Creates a set of util functions which is exposed to Plugins to make it easier to build consistent UIs */ export const createUtils = (sb: any, react: typeof React) => { - const sandbox: Sandbox = sb + const sandbox: Sandbox = sb const ts = sandbox.ts const requireURL = (path: string) => { // https://unpkg.com/browse/typescript-playground-presentation-mode@0.0.1/dist/x.js => unpkg/browse/typescript-playground-presentation-mode@0.0.1/dist/x - const isDev = document.location.host.includes('localhost') - const prefix = isDev ? 'local/' : 'unpkg/typescript-playground-presentation-mode/dist/' + const isDev = document.location.host.includes("localhost") + const prefix = isDev ? "local/" : "unpkg/typescript-playground-presentation-mode/dist/" return prefix + path } - const el = (str: string, el: string, container: Element) => { - const para = document.createElement(el) - para.innerHTML = str - container.appendChild(para) + const el = (str: string, elementType: string, container: Element) => { + const el = document.createElement(elementType) + el.innerHTML = str + container.appendChild(el) + return el + } + + // The Playground Plugin design system + const createDesignSystem = (container: Element) => { + const clear = () => { + while (container.firstChild) { + container.removeChild(container.firstChild) + } + } + let decorations: string[] = [] + let decorationLock = false + + return { + /** Clear the sidebar */ + clear, + /** Present code in a pre > code */ + code: (code: string) => { + const createCodePre = document.createElement("pre") + const codeElement = document.createElement("code") + + codeElement.innerHTML = code + + createCodePre.appendChild(codeElement) + container.appendChild(createCodePre) + + return codeElement + }, + /** Ideally only use this once, and maybe even prefer using subtitles everywhere */ + title: (title: string) => el(title, "h3", container), + /** Used to denote sections, give info etc */ + subtitle: (subtitle: string) => el(subtitle, "h4", container), + p: (subtitle: string) => el(subtitle, "p", container), + /** When you can't do something, or have nothing to show */ + showEmptyScreen: (message: string) => { + clear() + + const noErrorsMessage = document.createElement("div") + noErrorsMessage.id = "empty-message-container" + + const messageDiv = document.createElement("div") + messageDiv.textContent = message + messageDiv.classList.add("empty-plugin-message") + noErrorsMessage.appendChild(messageDiv) + + container.appendChild(noErrorsMessage) + return noErrorsMessage + }, + /** + * Shows a list of hoverable, and selectable items (errors, highlights etc) which have code representation. + * The type is quite small, so it should be very feasible for you to massage other data to fit into this function + */ + listDiags: ( + sandbox: Sandbox, + model: import("monaco-editor").editor.ITextModel, + diags: DiagnosticRelatedInformation[] + ) => { + const errorUL = document.createElement("ul") + errorUL.className = "compiler-diagnostics" + + container.appendChild(errorUL) + + diags.forEach(diag => { + const li = document.createElement("li") + li.classList.add("diagnostic") + switch (diag.category) { + case 0: + li.classList.add("warning") + break + case 1: + li.classList.add("error") + break + case 2: + li.classList.add("suggestion") + break + case 3: + li.classList.add("message") + break + } + + if (typeof diag === "string") { + li.textContent = diag + } else { + li.textContent = sandbox.ts.flattenDiagnosticMessageText(diag.messageText, "\n") + } + errorUL.appendChild(li) + + li.onmouseenter = () => { + if (diag.start && diag.length && !decorationLock) { + const start = model.getPositionAt(diag.start) + const end = model.getPositionAt(diag.start + diag.length) + decorations = sandbox.editor.deltaDecorations(decorations, [ + { + range: new sandbox.monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), + options: { inlineClassName: "error-highlight" }, + }, + ]) + } + } + + li.onmouseleave = () => { + if (!decorationLock) { + sandbox.editor.deltaDecorations(decorations, []) + } + } + + li.onclick = () => { + if (diag.start && diag.length) { + const start = model.getPositionAt(diag.start) + sandbox.editor.revealLine(start.lineNumber) + + const end = model.getPositionAt(diag.start + diag.length) + decorations = sandbox.editor.deltaDecorations(decorations, [ + { + range: new sandbox.monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), + options: { inlineClassName: "error-highlight", isWholeLine: true }, + }, + ]) + + decorationLock = true + setTimeout(() => { + decorationLock = false + sandbox.editor.deltaDecorations(decorations, []) + }, 300) + } + } + }) + return errorUL + }, + + localStorageOption: (setting: { blurb: string; flag: string; display: string }) => { + const li = document.createElement("li") + const label = document.createElement("label") + label.innerHTML = `${setting.display}
${setting.blurb}` + + const key = setting.flag + const input = document.createElement("input") + input.type = "checkbox" + input.id = key + input.checked = !!localStorage.getItem(key) + + input.onchange = () => { + if (input.checked) { + localStorage.setItem(key, "true") + } else { + localStorage.removeItem(key) + } + } + + label.htmlFor = input.id + + li.appendChild(input) + li.appendChild(label) + container.appendChild(li) + return li + }, + } } const createASTTree = (node: Node) => { - const div = document.createElement('div') + const div = document.createElement("div") div.className = "ast" const infoForNode = (node: Node) => { @@ -32,47 +189,47 @@ export const createUtils = (sb: any, react: typeof React) => { } const renderLiteralField = (key: string, value: string) => { - const li = document.createElement('li') + const li = document.createElement("li") li.innerHTML = `${key}: ${value}` return li } const renderSingleChild = (key: string, value: Node) => { - const li = document.createElement('li') + const li = document.createElement("li") li.innerHTML = `${key}: ${ts.SyntaxKind[value.kind]}` return li } const renderManyChildren = (key: string, value: Node[]) => { - const li = document.createElement('li') - const nodes = value.map(n => "  " + ts.SyntaxKind[n.kind] + "").join("
") + const li = document.createElement("li") + const nodes = value.map(n => "  " + ts.SyntaxKind[n.kind] + "").join("
") li.innerHTML = `${key}: [
${nodes}
]` return li } - + const renderItem = (parentElement: Element, node: Node) => { - const ul = document.createElement('ul') + const ul = document.createElement("ul") parentElement.appendChild(ul) - ul.className = 'ast-tree' + ul.className = "ast-tree" const info = infoForNode(node) - - const li = document.createElement('li') + + const li = document.createElement("li") ul.appendChild(li) - - const a = document.createElement('a') - a.textContent = info.name + + const a = document.createElement("a") + a.textContent = info.name li.appendChild(a) - - const properties = document.createElement('ul') - properties.className = 'ast-tree' + + const properties = document.createElement("ul") + properties.className = "ast-tree" li.appendChild(properties) Object.keys(node).forEach(field => { if (typeof field === "function") return if (field === "parent" || field === "flowNode") return - const value = (node as any)[field] + const value = (node as any)[field] if (typeof value === "object" && Array.isArray(value) && "pos" in value[0] && "end" in value[0]) { // Is an array of Nodes properties.appendChild(renderManyChildren(field, value)) @@ -82,23 +239,27 @@ export const createUtils = (sb: any, react: typeof React) => { } else { properties.appendChild(renderLiteralField(field, value)) } - }) + }) } - + renderItem(div, node) return div } - return { - /** Use this to make a few dumb element generation funcs */ + /** Use this to make a few dumb element generation funcs */ el, /** Get a relative URL for something in your dist folder depending on if you're in dev mode or not */ requireURL, /** Returns a div which has an interactive AST a TypeScript AST by passing in the root node */ createASTTree, /** The Gatsby copy of React */ - react + react, + /** + * The playground plugin design system. Calling any of the functions will append the + * element to the container you pass into the first param, and return the HTMLElement + */ + createDesignSystem, } } diff --git a/packages/playground/src/sidebar/options.ts b/packages/playground/src/sidebar/options.ts deleted file mode 100644 index 2518bd501b09..000000000000 --- a/packages/playground/src/sidebar/options.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { PlaygroundPlugin, PluginFactory } from '..' - -const pluginRegistry = [ - { - module: 'typescript-playground-presentation-mode', - display: 'Presentation Mode', - blurb: 'Create presentations inside the TypeScript playground, seamlessly jump between slides and live-code.', - repo: 'https://github.com/orta/playground-slides/#README', - author: { - name: 'Orta', - href: 'https://orta.io', - }, - }, -] - -/** Whether the playground should actively reach out to an existing plugin */ -export const allowConnectingToLocalhost = () => { - return !!localStorage.getItem('compiler-setting-connect-dev-plugin') -} - -export const activePlugins = () => { - const existing = customPlugins().map(module => ({ module })) - return existing.concat(pluginRegistry.filter(p => !!localStorage.getItem('plugin-' + p.module))) -} - -const removeCustomPlugins = (mod: string) => { - const newPlugins = customPlugins().filter(p => p !== mod) - localStorage.setItem('custom-plugins-playground', JSON.stringify(newPlugins)) -} - -export const addCustomPlugin = (mod: string) => { - const newPlugins = customPlugins() - newPlugins.push(mod) - localStorage.setItem('custom-plugins-playground', JSON.stringify(newPlugins)) - // @ts-ignore - window.appInsights && - // @ts-ignore - window.appInsights.trackEvent({ name: 'Added Custom Module', properties: { id: mod } }) -} - -const customPlugins = (): string[] => { - return JSON.parse(localStorage.getItem('custom-plugins-playground') || '[]') -} - -export const optionsPlugin: PluginFactory = i => { - const settings = [ - { - display: i('play_sidebar_options_disable_ata'), - blurb: i('play_sidebar_options_disable_ata_copy'), - flag: 'disable-ata', - }, - { - display: i('play_sidebar_options_disable_save'), - blurb: i('play_sidebar_options_disable_save_copy'), - flag: 'disable-save-on-type', - }, - // { - // display: 'Verbose Logging', - // blurb: 'Turn on superfluous logging', - // flag: 'enable-superfluous-logging', - // }, - ] - - const plugin: PlaygroundPlugin = { - id: 'options', - displayName: i('play_sidebar_options'), - // shouldBeSelected: () => true, // uncomment to make this the first tab on reloads - willMount: (_sandbox, container) => { - const categoryDiv = document.createElement('div') - container.appendChild(categoryDiv) - - const p = document.createElement('p') - p.id = 'restart-required' - p.textContent = i('play_sidebar_options_restart_required') - categoryDiv.appendChild(p) - - const ol = document.createElement('ol') - ol.className = 'playground-options' - - createSection(i('play_sidebar_options_external'), categoryDiv) - - const pluginsOL = document.createElement('ol') - pluginsOL.className = 'playground-plugins' - pluginRegistry.forEach(plugin => { - const settingButton = createPlugin(plugin) - pluginsOL.appendChild(settingButton) - }) - categoryDiv.appendChild(pluginsOL) - - const warning = document.createElement('p') - warning.className = 'warning' - warning.textContent = i('play_sidebar_options_external_warning') - categoryDiv.appendChild(warning) - - createSection(i('play_sidebar_options_modules'), categoryDiv) - const customModulesOL = document.createElement('ol') - customModulesOL.className = 'custom-modules' - - const updateCustomModules = () => { - while (customModulesOL.firstChild) { - customModulesOL.removeChild(customModulesOL.firstChild) - } - customPlugins().forEach(module => { - const li = document.createElement('li') - li.innerHTML = module - const a = document.createElement('a') - a.href = '#' - a.textContent = 'X' - a.onclick = () => { - removeCustomPlugins(module) - updateCustomModules() - announceWeNeedARestart() - return false - } - li.appendChild(a) - - customModulesOL.appendChild(li) - }) - } - updateCustomModules() - - categoryDiv.appendChild(customModulesOL) - const inputForm = createNewModuleInputForm(updateCustomModules, i) - categoryDiv.appendChild(inputForm) - - createSection('Plugin Dev', categoryDiv) - - const pluginsDevOL = document.createElement('ol') - pluginsDevOL.className = 'playground-options' - const connectToDev = createButton({ - display: i('play_sidebar_options_plugin_dev_option'), - blurb: i('play_sidebar_options_plugin_dev_copy'), - flag: 'connect-dev-plugin', - }) - pluginsDevOL.appendChild(connectToDev) - categoryDiv.appendChild(pluginsDevOL) - - categoryDiv.appendChild(document.createElement('hr')) - - createSection(i('play_sidebar_options'), categoryDiv) - - settings.forEach(setting => { - const settingButton = createButton(setting) - ol.appendChild(settingButton) - }) - - categoryDiv.appendChild(ol) - }, - } - - return plugin -} - -const announceWeNeedARestart = () => { - document.getElementById('restart-required')!.style.display = 'block' -} - -const createSection = (title: string, container: Element) => { - const pluginDevTitle = document.createElement('h4') - pluginDevTitle.textContent = title - container.appendChild(pluginDevTitle) -} - -const createPlugin = (plugin: typeof pluginRegistry[0]) => { - const li = document.createElement('li') - const div = document.createElement('div') - - const label = document.createElement('label') - - const top = `${plugin.display} by ${plugin.author.name}
${plugin.blurb}` - const bottom = `npm | repo` - label.innerHTML = `${top}
${bottom}` - - const key = 'plugin-' + plugin.module - const input = document.createElement('input') - input.type = 'checkbox' - input.id = key - input.checked = !!localStorage.getItem(key) - - input.onchange = () => { - announceWeNeedARestart() - if (input.checked) { - // @ts-ignore - window.appInsights && - // @ts-ignore - window.appInsights.trackEvent({ name: 'Added Registry Plugin', properties: { id: key } }) - localStorage.setItem(key, 'true') - } else { - localStorage.removeItem(key) - } - } - - label.htmlFor = input.id - - div.appendChild(input) - div.appendChild(label) - li.appendChild(div) - return li -} - -const createButton = (setting: { blurb: string; flag: string; display: string }) => { - const li = document.createElement('li') - const label = document.createElement('label') - label.innerHTML = `${setting.display}
${setting.blurb}` - - const key = 'compiler-setting-' + setting.flag - const input = document.createElement('input') - input.type = 'checkbox' - input.id = key - input.checked = !!localStorage.getItem(key) - - input.onchange = () => { - if (input.checked) { - localStorage.setItem(key, 'true') - } else { - localStorage.removeItem(key) - } - } - - label.htmlFor = input.id - - li.appendChild(input) - li.appendChild(label) - return li -} - -const createNewModuleInputForm = (updateOL: Function, i: any) => { - const form = document.createElement('form') - - const newModuleInput = document.createElement('input') - newModuleInput.type = 'text' - newModuleInput.id = 'gist-input' - newModuleInput.placeholder = i('play_sidebar_options_modules_placeholder') - form.appendChild(newModuleInput) - - form.onsubmit = e => { - announceWeNeedARestart() - addCustomPlugin(newModuleInput.value) - e.stopPropagation() - updateOL() - return false - } - - return form -} diff --git a/packages/playground/src/sidebar/plugins.ts b/packages/playground/src/sidebar/plugins.ts new file mode 100644 index 000000000000..6261f1872de2 --- /dev/null +++ b/packages/playground/src/sidebar/plugins.ts @@ -0,0 +1,195 @@ +import { PlaygroundPlugin, PluginFactory } from ".." + +const pluginRegistry = [ + { + module: "typescript-playground-presentation-mode", + display: "Presentation Mode", + blurb: "Create presentations inside the TypeScript playground, seamlessly jump between slides and live-code.", + repo: "https://github.com/orta/playground-slides/#README", + author: { + name: "Orta", + href: "https://orta.io", + }, + }, +] + +/** Whether the playground should actively reach out to an existing plugin */ +export const allowConnectingToLocalhost = () => { + return !!localStorage.getItem("compiler-setting-connect-dev-plugin") +} + +export const activePlugins = () => { + const existing = customPlugins().map(module => ({ module })) + return existing.concat(pluginRegistry.filter(p => !!localStorage.getItem("plugin-" + p.module))) +} + +const removeCustomPlugins = (mod: string) => { + const newPlugins = customPlugins().filter(p => p !== mod) + localStorage.setItem("custom-plugins-playground", JSON.stringify(newPlugins)) +} + +export const addCustomPlugin = (mod: string) => { + const newPlugins = customPlugins() + newPlugins.push(mod) + localStorage.setItem("custom-plugins-playground", JSON.stringify(newPlugins)) + // @ts-ignore + window.appInsights && + // @ts-ignore + window.appInsights.trackEvent({ name: "Added Custom Module", properties: { id: mod } }) +} + +const customPlugins = (): string[] => { + return JSON.parse(localStorage.getItem("custom-plugins-playground") || "[]") +} + +export const optionsPlugin: PluginFactory = (i, utils) => { + const plugin: PlaygroundPlugin = { + id: "plugins", + displayName: i("play_sidebar_plugins"), + shouldBeSelected: () => true, // uncomment to make this the first tab on reloads + willMount: (_sandbox, container) => { + const ds = utils.createDesignSystem(container) + + const restartReq = ds.p(i("play_sidebar_options_restart_required")) + restartReq.id = "restart-required" + + ds.subtitle(i("play_sidebar_plugins_options_external")) + + const pluginsOL = document.createElement("ol") + pluginsOL.className = "playground-plugins" + pluginRegistry.forEach(plugin => { + const settingButton = createPlugin(plugin) + pluginsOL.appendChild(settingButton) + }) + container.appendChild(pluginsOL) + + const warning = document.createElement("p") + warning.className = "warning" + warning.textContent = i("play_sidebar_plugins_options_external_warning") + container.appendChild(warning) + + ds.subtitle(i("play_sidebar_plugins_options_modules")) + + const customModulesOL = document.createElement("ol") + customModulesOL.className = "custom-modules" + + const updateCustomModules = () => { + while (customModulesOL.firstChild) { + customModulesOL.removeChild(customModulesOL.firstChild) + } + customPlugins().forEach(module => { + const li = document.createElement("li") + li.innerHTML = module + const a = document.createElement("a") + a.href = "#" + a.textContent = "X" + a.onclick = () => { + removeCustomPlugins(module) + updateCustomModules() + announceWeNeedARestart() + return false + } + li.appendChild(a) + + customModulesOL.appendChild(li) + }) + } + updateCustomModules() + + container.appendChild(customModulesOL) + const inputForm = createNewModuleInputForm(updateCustomModules, i) + container.appendChild(inputForm) + + ds.subtitle(i("play_sidebar_plugins_plugin_dev")) + + const pluginsDevOL = document.createElement("ol") + pluginsDevOL.className = "playground-options" + + const connectToDev = ds.localStorageOption({ + display: i("play_sidebar_plugins_plugin_dev_option"), + blurb: i("play_sidebar_plugins_plugin_dev_copy"), + flag: "connect-dev-plugin", + }) + pluginsDevOL.appendChild(connectToDev) + container.appendChild(pluginsDevOL) + + // createSection(i("play_sidebar_options"), categoryDiv) + + // settings.forEach(setting => { + // const settingButton = createButton(setting) + // ol.appendChild(settingButton) + // }) + + // categoryDiv.appendChild(ol) + }, + } + + return plugin +} + +const announceWeNeedARestart = () => { + document.getElementById("restart-required")!.style.display = "block" +} + +const createSection = (title: string, container: Element) => { + const pluginDevTitle = document.createElement("h4") + pluginDevTitle.textContent = title + container.appendChild(pluginDevTitle) +} + +const createPlugin = (plugin: typeof pluginRegistry[0]) => { + const li = document.createElement("li") + const div = document.createElement("div") + + const label = document.createElement("label") + + const top = `${plugin.display} by ${plugin.author.name}
${plugin.blurb}` + const bottom = `npm | repo` + label.innerHTML = `${top}
${bottom}` + + const key = "plugin-" + plugin.module + const input = document.createElement("input") + input.type = "checkbox" + input.id = key + input.checked = !!localStorage.getItem(key) + + input.onchange = () => { + announceWeNeedARestart() + if (input.checked) { + // @ts-ignore + window.appInsights && + // @ts-ignore + window.appInsights.trackEvent({ name: "Added Registry Plugin", properties: { id: key } }) + localStorage.setItem(key, "true") + } else { + localStorage.removeItem(key) + } + } + + label.htmlFor = input.id + + div.appendChild(input) + div.appendChild(label) + li.appendChild(div) + return li +} + +const createNewModuleInputForm = (updateOL: Function, i: any) => { + const form = document.createElement("form") + + const newModuleInput = document.createElement("input") + newModuleInput.type = "text" + newModuleInput.id = "gist-input" + newModuleInput.placeholder = i("play_sidebar_plugins_options_modules_placeholder") + form.appendChild(newModuleInput) + + form.onsubmit = e => { + announceWeNeedARestart() + addCustomPlugin(newModuleInput.value) + e.stopPropagation() + updateOL() + return false + } + + return form +} diff --git a/packages/playground/src/sidebar/runtime.ts b/packages/playground/src/sidebar/runtime.ts index 3f20a2a9cd97..b44ef60c1c94 100644 --- a/packages/playground/src/sidebar/runtime.ts +++ b/packages/playground/src/sidebar/runtime.ts @@ -1,30 +1,30 @@ -import { PlaygroundPlugin, PluginFactory } from '..' -import { localize } from '../localizeWithFallback' +import { PlaygroundPlugin, PluginFactory } from ".." +import { localize } from "../localizeWithFallback" -let allLogs = '' +let allLogs = "" -export const runPlugin: PluginFactory = i => { +export const runPlugin: PluginFactory = (i, utils) => { const plugin: PlaygroundPlugin = { - id: 'logs', - displayName: i('play_sidebar_logs'), + id: "logs", + displayName: i("play_sidebar_logs"), willMount: (sandbox, container) => { if (allLogs.length === 0) { - const noErrorsMessage = document.createElement('div') - noErrorsMessage.id = 'empty-message-container' + const noErrorsMessage = document.createElement("div") + noErrorsMessage.id = "empty-message-container" container.appendChild(noErrorsMessage) - const message = document.createElement('div') - message.textContent = localize('play_sidebar_logs_no_logs', 'No logs') - message.classList.add('empty-plugin-message') + const message = document.createElement("div") + message.textContent = localize("play_sidebar_logs_no_logs", "No logs") + message.classList.add("empty-plugin-message") noErrorsMessage.appendChild(message) } - const errorUL = document.createElement('div') - errorUL.id = 'log-container' + const errorUL = document.createElement("div") + errorUL.id = "log-container" container.appendChild(errorUL) - const logs = document.createElement('div') - logs.id = 'log' + const logs = document.createElement("div") + logs.id = "log" logs.innerHTML = allLogs errorUL.appendChild(logs) }, @@ -34,14 +34,14 @@ export const runPlugin: PluginFactory = i => { } export const runWithCustomLogs = (closure: Promise, i: Function) => { - const noLogs = document.getElementById('empty-message-container') + const noLogs = document.getElementById("empty-message-container") if (noLogs) { - noLogs.style.display = 'none' + noLogs.style.display = "none" } rewireLoggingToElement( - () => document.getElementById('log')!, - () => document.getElementById('log-container')!, + () => document.getElementById("log")!, + () => document.getElementById("log-container")!, closure, true, i @@ -57,44 +57,44 @@ function rewireLoggingToElement( autoScroll: boolean, i: Function ) { - fixLoggingFunc('log', 'LOG') - fixLoggingFunc('debug', 'DBG') - fixLoggingFunc('warn', 'WRN') - fixLoggingFunc('error', 'ERR') - fixLoggingFunc('info', 'INF') + fixLoggingFunc("log", "LOG") + fixLoggingFunc("debug", "DBG") + fixLoggingFunc("warn", "WRN") + fixLoggingFunc("error", "ERR") + fixLoggingFunc("info", "INF") closure.then(js => { try { eval(js) } catch (error) { - console.error(i('play_run_js_fail')) + console.error(i("play_run_js_fail")) console.error(error) } - allLogs = allLogs + '
' + allLogs = allLogs + "
" - undoLoggingFunc('log') - undoLoggingFunc('debug') - undoLoggingFunc('warn') - undoLoggingFunc('error') - undoLoggingFunc('info') + undoLoggingFunc("log") + undoLoggingFunc("debug") + undoLoggingFunc("warn") + undoLoggingFunc("error") + undoLoggingFunc("info") }) function undoLoggingFunc(name: string) { // @ts-ignore - console[name] = console['old' + name] + console[name] = console["old" + name] } function fixLoggingFunc(name: string, id: string) { // @ts-ignore - console['old' + name] = console[name] + console["old" + name] = console[name] // @ts-ignore - console[name] = function(...objs: any[]) { + console[name] = function (...objs: any[]) { const output = produceOutput(objs) const eleLog = eleLocator() - const prefix = '[' + id + ']: ' + const prefix = '[' + id + "]: " const eleContainerLog = eleOverflowLocator() - allLogs = allLogs + prefix + output + '
' + allLogs = allLogs + prefix + output + "
" if (eleLog && eleContainerLog) { if (autoScroll) { @@ -108,14 +108,14 @@ function rewireLoggingToElement( } // @ts-ignore - console['old' + name].apply(undefined, objs) + console["old" + name].apply(undefined, objs) } } function produceOutput(args: any[]) { return args.reduce((output: any, arg: any, index) => { - const isObj = typeof arg === 'object' - let textRep = '' + const isObj = typeof arg === "object" + let textRep = "" if (arg && arg.stack && arg.message) { // special case for err textRep = arg.message @@ -126,8 +126,8 @@ function rewireLoggingToElement( } const showComma = index !== args.length - 1 - const comma = showComma ? ", " : '' - return output + textRep + comma + ' ' - }, '') + const comma = showComma ? ", " : "" + return output + textRep + comma + " " + }, "") } } diff --git a/packages/playground/src/sidebar/settings.ts b/packages/playground/src/sidebar/settings.ts new file mode 100644 index 000000000000..e63ba29fd226 --- /dev/null +++ b/packages/playground/src/sidebar/settings.ts @@ -0,0 +1,43 @@ +import { PlaygroundPlugin, PluginFactory } from ".." + +export const settingsPlugin: PluginFactory = (i, utils) => { + const settings = [ + { + display: i("play_sidebar_options_disable_ata"), + blurb: i("play_sidebar_options_disable_ata_copy"), + flag: "disable-ata", + }, + { + display: i("play_sidebar_options_disable_save"), + blurb: i("play_sidebar_options_disable_save_copy"), + flag: "disable-save-on-type", + }, + // { + // display: 'Verbose Logging', + // blurb: 'Turn on superfluous logging', + // flag: 'enable-superfluous-logging', + // }, + ] + + const plugin: PlaygroundPlugin = { + id: "settings", + displayName: i("play_subnav_settings"), + didMount: async (sandbox, container) => { + const ds = utils.createDesignSystem(container) + + ds.subtitle(i("play_subnav_settings")) + + const ol = document.createElement("ol") + ol.className = "playground-options" + + settings.forEach(setting => { + const settingButton = ds.localStorageOption(setting) + ol.appendChild(settingButton) + }) + + container.appendChild(ol) + }, + } + + return plugin +} diff --git a/packages/playground/src/sidebar/showDTS.ts b/packages/playground/src/sidebar/showDTS.ts index 4e68efdfb007..6c4463b5f137 100644 --- a/packages/playground/src/sidebar/showDTS.ts +++ b/packages/playground/src/sidebar/showDTS.ts @@ -1,23 +1,18 @@ -import { PlaygroundPlugin, PluginFactory } from '..' -import { localize } from '../localizeWithFallback' +import { PlaygroundPlugin, PluginFactory } from ".." -export const showDTSPlugin: PluginFactory = i => { +export const showDTSPlugin: PluginFactory = (i, utils) => { let codeElement: HTMLElement const plugin: PlaygroundPlugin = { - id: 'dts', - displayName: i('play_sidebar_dts'), - willMount: (sandbox, container) => { - // TODO: Monaco? - const createCodePre = document.createElement('pre') - codeElement = document.createElement('code') - - createCodePre.appendChild(codeElement) - container.appendChild(createCodePre) + id: "dts", + displayName: i("play_sidebar_dts"), + willMount: (_, container) => { + const { code } = utils.createDesignSystem(container) + codeElement = code("") }, modelChanged: (sandbox, model) => { sandbox.getDTSForCode().then(dts => { - sandbox.monaco.editor.colorize(dts, 'typescript', {}).then(coloredDTS => { + sandbox.monaco.editor.colorize(dts, "typescript", {}).then(coloredDTS => { codeElement.innerHTML = coloredDTS }) }) diff --git a/packages/playground/src/sidebar/showErrors.ts b/packages/playground/src/sidebar/showErrors.ts index 6fa1ad00a6ae..026d4566b1b9 100644 --- a/packages/playground/src/sidebar/showErrors.ts +++ b/packages/playground/src/sidebar/showErrors.ts @@ -1,117 +1,24 @@ -import { PlaygroundPlugin, PluginFactory } from '..' -import { localize } from '../localizeWithFallback' - -export const showErrors: PluginFactory = i => { - let decorations: string[] = [] - let decorationLock = false +import { PlaygroundPlugin, PluginFactory } from ".." +import { localize } from "../localizeWithFallback" +export const showErrors: PluginFactory = (i, utils) => { const plugin: PlaygroundPlugin = { - id: 'errors', - displayName: i('play_sidebar_errors'), - willMount: async (sandbox, container) => { - const noErrorsMessage = document.createElement('div') - noErrorsMessage.id = 'empty-message-container' - container.appendChild(noErrorsMessage) - - const errorUL = document.createElement('ul') - errorUL.id = 'compiler-errors' - container.appendChild(errorUL) - }, + id: "errors", + displayName: i("play_sidebar_errors"), + modelChangedDebounce: async (sandbox, model, container) => { + const ds = utils.createDesignSystem(container) - modelChangedDebounce: async (sandbox, model) => { sandbox.getWorkerProcess().then(worker => { worker.getSemanticDiagnostics(model.uri.toString()).then(diags => { - const errorUL = document.getElementById('compiler-errors') - const noErrorsMessage = document.getElementById('empty-message-container') - if (!errorUL || !noErrorsMessage) return - - while (errorUL.firstChild) { - errorUL.removeChild(errorUL.firstChild) - } - // Bail early if there's nothing to show if (!diags.length) { - errorUL.style.display = 'none' - noErrorsMessage.style.display = 'flex' - - // Already has a message - if (noErrorsMessage.children.length) return - - const message = document.createElement('div') - message.textContent = localize('play_sidebar_errors_no_errors', 'No errors') - message.classList.add('empty-plugin-message') - noErrorsMessage.appendChild(message) + ds.showEmptyScreen(localize("play_sidebar_errors_no_errors", "No errors")) return } - noErrorsMessage.style.display = 'none' - errorUL.style.display = 'block' - - diags.forEach(diag => { - const li = document.createElement('li') - li.classList.add('diagnostic') - switch (diag.category) { - case 0: - li.classList.add('warning') - break - case 1: - li.classList.add('error') - break - case 2: - li.classList.add('suggestion') - break - case 3: - li.classList.add('message') - break - } - - if (typeof diag === 'string') { - li.textContent = diag - } else { - li.textContent = sandbox.ts.flattenDiagnosticMessageText(diag.messageText, '\n') - } - errorUL.appendChild(li) - - li.onmouseenter = () => { - if (diag.start && diag.length && !decorationLock) { - const start = model.getPositionAt(diag.start) - const end = model.getPositionAt(diag.start + diag.length) - decorations = sandbox.editor.deltaDecorations(decorations, [ - { - range: new sandbox.monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), - options: { inlineClassName: 'error-highlight' }, - }, - ]) - } - } - - li.onmouseleave = () => { - if (!decorationLock) { - sandbox.editor.deltaDecorations(decorations, []) - } - } - - li.onclick = () => { - if (diag.start && diag.length) { - const start = model.getPositionAt(diag.start) - sandbox.editor.revealLine(start.lineNumber) - - const end = model.getPositionAt(diag.start + diag.length) - decorations = sandbox.editor.deltaDecorations(decorations, [ - { - range: new sandbox.monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), - options: { inlineClassName: 'error-highlight', isWholeLine: true }, - }, - ]) - - decorationLock = true - setTimeout(() => { - decorationLock = false - sandbox.editor.deltaDecorations(decorations, []) - }, 300) - } - } - }) + // Clean any potential empty screens + ds.clear() + ds.listDiags(sandbox, model, diags) }) }) }, diff --git a/packages/playground/src/sidebar/showJS.ts b/packages/playground/src/sidebar/showJS.ts index 06fc37810e32..9f7a48c95f70 100644 --- a/packages/playground/src/sidebar/showJS.ts +++ b/packages/playground/src/sidebar/showJS.ts @@ -1,21 +1,18 @@ -import { PlaygroundPlugin, PluginFactory } from '..' +import { PlaygroundPlugin, PluginFactory } from ".." -export const compiledJSPlugin: PluginFactory = i => { +export const compiledJSPlugin: PluginFactory = (i, utils) => { let codeElement: HTMLElement const plugin: PlaygroundPlugin = { - id: 'js', - displayName: i('play_sidebar_js'), - willMount: (sandbox, container) => { - const createCodePre = document.createElement('pre') - codeElement = document.createElement('code') - - createCodePre.appendChild(codeElement) - container.appendChild(createCodePre) + id: "js", + displayName: i("play_sidebar_js"), + willMount: (_, container) => { + const { code } = utils.createDesignSystem(container) + codeElement = code("") }, modelChangedDebounce: (sandbox, model) => { sandbox.getRunnableJS().then(js => { - sandbox.monaco.editor.colorize(js, 'javascript', {}).then(coloredJS => { + sandbox.monaco.editor.colorize(js, "javascript", {}).then(coloredJS => { codeElement.innerHTML = coloredJS }) }) diff --git a/packages/sandbox/src/index.ts b/packages/sandbox/src/index.ts index 6fd070ca583e..89ef79ae692d 100644 --- a/packages/sandbox/src/index.ts +++ b/packages/sandbox/src/index.ts @@ -1,19 +1,19 @@ -import { detectNewImportsToAcquireTypeFor } from './typeAcquisition' -import { sandboxTheme, sandboxThemeDark } from './theme' -import { TypeScriptWorker } from './tsWorker' +import { detectNewImportsToAcquireTypeFor } from "./typeAcquisition" +import { sandboxTheme, sandboxThemeDark } from "./theme" +import { TypeScriptWorker } from "./tsWorker" import { getDefaultSandboxCompilerOptions, getCompilerOptionsFromParams, createURLQueryWithCompilerOptions, -} from './compilerOptions' -import lzstring from './vendor/lzstring.min' -import { supportedReleases } from './releases' -import { getInitialCode } from './getInitialCode' -import { extractTwoSlashComplierOptions } from './twoslashSupport' -import * as tsvfs from './vendor/typescript-vfs' +} from "./compilerOptions" +import lzstring from "./vendor/lzstring.min" +import { supportedReleases } from "./releases" +import { getInitialCode } from "./getInitialCode" +import { extractTwoSlashComplierOptions } from "./twoslashSupport" +import * as tsvfs from "./vendor/typescript-vfs" -type CompilerOptions = import('monaco-editor').languages.typescript.CompilerOptions -type Monaco = typeof import('monaco-editor') +type CompilerOptions = import("monaco-editor").languages.typescript.CompilerOptions +type Monaco = typeof import("monaco-editor") /** * These are settings for the playground which are the equivalent to props in React @@ -27,7 +27,7 @@ export type PlaygroundConfig = { /** Compiler options which are automatically just forwarded on */ compilerOptions: CompilerOptions /** Optional monaco settings overrides */ - monacoSettings?: import('monaco-editor').editor.IEditorOptions + monacoSettings?: import("monaco-editor").editor.IEditorOptions /** Acquire types via type acquisition */ acquireTypes: boolean /** Support twoslash compiler options */ @@ -48,10 +48,10 @@ export type PlaygroundConfig = { | { /** theID of a dom node to add monaco to */ elementToAppend: HTMLElement } ) -const languageType = (config: PlaygroundConfig) => (config.useJavaScript ? 'javascript' : 'typescript') +const languageType = (config: PlaygroundConfig) => (config.useJavaScript ? "javascript" : "typescript") /** Default Monaco settings for playground */ -const sharedEditorOptions: import('monaco-editor').editor.IEditorOptions = { +const sharedEditorOptions: import("monaco-editor").editor.IEditorOptions = { automaticLayout: true, scrollBeyondLastLine: true, scrollBeyondLastColumn: 3, @@ -63,8 +63,8 @@ const sharedEditorOptions: import('monaco-editor').editor.IEditorOptions = { /** The default settings which we apply a partial over */ export function defaultPlaygroundSettings() { const config: PlaygroundConfig = { - text: '', - domID: '', + text: "", + domID: "", compilerOptions: {}, acquireTypes: true, useJavaScript: false, @@ -76,9 +76,9 @@ export function defaultPlaygroundSettings() { function defaultFilePath(config: PlaygroundConfig, compilerOptions: CompilerOptions, monaco: Monaco) { const isJSX = compilerOptions.jsx !== monaco.languages.typescript.JsxEmit.None - const fileExt = config.useJavaScript ? 'js' : 'ts' - const ext = isJSX ? fileExt + 'x' : fileExt - return 'input.' + ext + const fileExt = config.useJavaScript ? "js" : "ts" + const ext = isJSX ? fileExt + "x" : fileExt + return "input." + ext } /** Creates a monaco file reference, basically a fancy path */ @@ -90,11 +90,11 @@ function createFileUri(config: PlaygroundConfig, compilerOptions: CompilerOption export const createTypeScriptSandbox = ( partialConfig: Partial, monaco: Monaco, - ts: typeof import('typescript') + ts: typeof import("typescript") ) => { const config = { ...defaultPlaygroundSettings(), ...partialConfig } - if (!('domID' in config) && !('elementToAppend' in config)) - throw new Error('You did not provide a domID or elementToAppend') + if (!("domID" in config) && !("elementToAppend" in config)) + throw new Error("You did not provide a domID or elementToAppend") const defaultText = config.suppressAutomaticallyGettingDefaultText ? config.text @@ -109,7 +109,7 @@ export const createTypeScriptSandbox = ( const params = new URLSearchParams(location.search) let queryParamCompilerOptions = getCompilerOptionsFromParams(compilerDefaults, params) if (Object.keys(queryParamCompilerOptions).length) - config.logger.log('[Compiler] Found compiler options in query params: ', queryParamCompilerOptions) + config.logger.log("[Compiler] Found compiler options in query params: ", queryParamCompilerOptions) compilerOptions = { ...compilerDefaults, ...queryParamCompilerOptions } } else { compilerOptions = compilerDefaults @@ -117,12 +117,12 @@ export const createTypeScriptSandbox = ( const language = languageType(config) const filePath = createFileUri(config, compilerOptions, monaco) - const element = 'domID' in config ? document.getElementById(config.domID) : (config as any).elementToAppend + const element = "domID" in config ? document.getElementById(config.domID) : (config as any).elementToAppend const model = monaco.editor.createModel(defaultText, language, filePath) - monaco.editor.defineTheme('sandbox', sandboxTheme) - monaco.editor.defineTheme('sandbox-dark', sandboxThemeDark) - monaco.editor.setTheme('sandbox') + monaco.editor.defineTheme("sandbox", sandboxTheme) + monaco.editor.defineTheme("sandbox-dark", sandboxThemeDark) + monaco.editor.setTheme("sandbox") const monacoSettings = Object.assign({ model }, sharedEditorOptions, config.monacoSettings || {}) const editor = monaco.editor.create(element, monacoSettings) @@ -156,7 +156,7 @@ export const createTypeScriptSandbox = ( } }) - config.logger.log('[Compiler] Set compiler options: ', compilerOptions) + config.logger.log("[Compiler] Set compiler options: ", compilerOptions) defaults.setCompilerOptions(compilerOptions) // Grab types last so that it logs in a logical way @@ -170,21 +170,21 @@ export const createTypeScriptSandbox = ( let didUpdateCompilerSettings = (opts: CompilerOptions) => {} const updateCompilerSettings = (opts: CompilerOptions) => { - config.logger.log('[Compiler] Updating compiler options: ', opts) + config.logger.log("[Compiler] Updating compiler options: ", opts) compilerOptions = { ...opts, ...compilerOptions } defaults.setCompilerOptions(compilerOptions) didUpdateCompilerSettings(compilerOptions) } const updateCompilerSetting = (key: keyof CompilerOptions, value: any) => { - config.logger.log('[Compiler] Setting compiler options ', key, 'to', value) + config.logger.log("[Compiler] Setting compiler options ", key, "to", value) compilerOptions[key] = value defaults.setCompilerOptions(compilerOptions) didUpdateCompilerSettings(compilerOptions) } const setCompilerSettings = (opts: CompilerOptions) => { - config.logger.log('[Compiler] Setting compiler options: ', opts) + config.logger.log("[Compiler] Setting compiler options: ", opts) compilerOptions = opts defaults.setCompilerOptions(compilerOptions) didUpdateCompilerSettings(compilerOptions) @@ -213,14 +213,14 @@ export const createTypeScriptSandbox = ( } const result = await getEmitResult() - const firstJS = result.outputFiles.find((o: any) => o.name.endsWith('.js') || o.name.endsWith('.jsx')) - return (firstJS && firstJS.text) || '' + const firstJS = result.outputFiles.find((o: any) => o.name.endsWith(".js") || o.name.endsWith(".jsx")) + return (firstJS && firstJS.text) || "" } /** Gets the DTS for the JS/TS of compiling your editor's code */ const getDTSForCode = async () => { const result = await getEmitResult() - return result.outputFiles.find((o: any) => o.name.endsWith('.d.ts'))!.text + return result.outputFiles.find((o: any) => o.name.endsWith(".d.ts"))!.text } const getWorkerProcess = async (): Promise => { @@ -326,6 +326,8 @@ export const createTypeScriptSandbox = ( getTwoSlashComplierOptions, /** Gets to the current monaco-language, this is how you talk to the background webworkers */ languageServiceDefaults: defaults, + /** The path which represents the current file using the current compiler options */ + filepath: filePath.path, } } diff --git a/packages/sandbox/src/twoslashSupport.ts b/packages/sandbox/src/twoslashSupport.ts index dd2569bc760d..0e25dfc2bb52 100644 --- a/packages/sandbox/src/twoslashSupport.ts +++ b/packages/sandbox/src/twoslashSupport.ts @@ -16,7 +16,7 @@ export const extractTwoSlashComplierOptions = (ts: TS) => (code: string) => { const codeLines = code.split('\n') const options = {} as any - codeLines.forEach(line => { + codeLines.forEach((line) => { let match if ((match = booleanConfigRegexp.exec(line))) { options[match[1]] = true @@ -40,7 +40,7 @@ function setOption(name: string, value: string, opts: CompilerOptions, ts: TS) { break case 'list': - opts[opt.name] = value.split(',').map(v => parsePrimitive(v, opt.element!.type as string)) + opts[opt.name] = value.split(',').map((v) => parsePrimitive(v, opt.element!.type as string)) break default: @@ -56,7 +56,10 @@ function setOption(name: string, value: string, opts: CompilerOptions, ts: TS) { } } - throw new Error(`No compiler setting named '${name}' exists!`) + // Skip the note of errors + if (name !== 'errors') { + throw new Error(`No compiler setting named '${name}' exists!`) + } } export function parsePrimitive(value: string, type: string): any { diff --git a/packages/ts-twoslasher/CHANGELOG.md b/packages/ts-twoslasher/CHANGELOG.md new file mode 100644 index 000000000000..69a075b708af --- /dev/null +++ b/packages/ts-twoslasher/CHANGELOG.md @@ -0,0 +1,31 @@ +## 0.3.0 + +Lots of work on the query engine, now it works across many files and multiple times in the same file. For example: + +```ts +const a = "123" +// ^? +const b = "345" +// ^? +``` + +and + +```ts +// @filename: index.ts +const a = "123" +// ^? +// @filename: main-file-queries.ts +const b = "345" +// ^? +``` + +Now returns correct query responses, I needed this for the bug workbench. +http://www.staging-typescript.org/dev/bug-workbench + +Also has a way to set the defaults for the config + +## 0.2.0 + +Initial public version of Twoslash. Good enough for using on the +TypeScript website, but still with a chunk of holes. diff --git a/packages/ts-twoslasher/CONTRIBUTING.md b/packages/ts-twoslasher/CONTRIBUTING.md new file mode 100644 index 000000000000..538f1125b8d1 --- /dev/null +++ b/packages/ts-twoslasher/CONTRIBUTING.md @@ -0,0 +1,11 @@ +### How to make a change in Twoslash + +It's likely you have a failing twoslash test case, copy that into `test/fixtures/tests/[name].ts` and run + +> `yarn workspace @typescript/twoslash test` + +This will create a Jest snapshot of that test run which you can use as an integration test to ensure your change doesn't get regressed. + +### Other complex code + +It's a normal Jest project where you can also make unit tests like `test/cutting.test.ts`. diff --git a/packages/ts-twoslasher/README.md b/packages/ts-twoslasher/README.md index c7b92f0e5e1a..b746207a0a5c 100644 --- a/packages/ts-twoslasher/README.md +++ b/packages/ts-twoslasher/README.md @@ -103,8 +103,12 @@ export interface ExampleOptions { * means when you just use `showEmit` above it shows the transpiled JS. */ showEmittedFile: string - /** Whether to disable the pre-cache of LSP calls for interesting identifiers */ - noStaticSemanticInfo: false + /** Whether to disable the pre-cache of LSP calls for interesting identifiers, defaults to false */ + noStaticSemanticInfo: boolean + /** Declare that the TypeScript program should edit the fsMap which is passed in, this is only useful for tool-makers, defaults to false */ + emit: boolean + /** Declare that you don't need to validate that errors have corresponding annotations, defaults to false */ + noErrorValidation: boolean } ``` @@ -215,16 +219,16 @@ type NameOrId = T extends number ? IdLabel : NameLabe // ---cut--- function createLabel(idOrName: T): NameOrId { - throw 'unimplemented' + throw "unimplemented" } -let a = createLabel('typescript') +let a = createLabel("typescript") // ^? let b = createLabel(2.8) // ^? -let c = createLabel(Math.random() ? 'hello' : 42) +let c = createLabel(Math.random() ? "hello" : 42) // ^? ``` @@ -232,14 +236,14 @@ Turns to: > ```ts > function createLabel(idOrName: T): NameOrId { -> throw 'unimplemented' +> throw "unimplemented" > } > -> let a = createLabel('typescript') +> let a = createLabel("typescript") > > let b = createLabel(2.8) > -> let c = createLabel(Math.random() ? 'hello' : 42) +> let c = createLabel(Math.random() ? "hello" : 42) > ``` > With: @@ -249,7 +253,35 @@ Turns to: > "code": "See above", > "extension": "ts", > "highlights": [], -> "queries": [], +> "queries": [ +> { +> "docs": "", +> "kind": "query", +> "start": 354, +> "length": 16, +> "text": "let a: NameLabel", +> "offset": 4, +> "line": 4 +> }, +> { +> "docs": "", +> "kind": "query", +> "start": 390, +> "length": 14, +> "text": "let b: IdLabel", +> "offset": 4, +> "line": 6 +> }, +> { +> "docs": "", +> "kind": "query", +> "start": 417, +> "length": 26, +> "text": "let c: IdLabel | NameLabel", +> "offset": 4, +> "line": 8 +> } +> ], > "staticQuickInfos": "[ 14 items ]", > "errors": [], > "playgroundURL": "https://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgJIBMAycBGEA2yA3ssOgFzIgCuAtnlADTID0AVMgM4D2tKMwAuk7I2LZAF8AUKEixEKAHJw+2PIRIgVESpzBRQAc2btk3MAAtoyAUJFjJUsAE8ADku0B5KBgA8AFWQIAA9IEGEqOgZkAB8ufSMAPmQAXmRAkLCImnprAH40LFwCZEplVWL8AG4pFnF-C2ARBF4+cC4Lbmp8dCpzZDxSEAR8anQIdCla8QBaOYRqMDmZqRhqYbBgbhBkBCgIOEg1AgCg0IhwkRzouL0DEENEgAoyb3KddIBKMq8fdADkkQpMgQchLFBuAB3ZAAInWwFornwEDakHQMKk0ikyLAyDgqV2+0OEGO+CeMJc7k4e2ArjAMM+NTqIIAenkpjiBgS9gcjpUngAmAB0AA5GdNWezsRBcQhuUS+eongBZQ4WIVQODhXhPT7IAowqz4fDcGGlZAAFgF4uZyDZUiAA" @@ -303,7 +335,7 @@ function greet(person: string, date: Date) { console.log(`Hello ${person}, today is ${date.toDateString()}!`) } -greet('Maddison', new Date()) +greet("Maddison", new Date()) // ^^^^^^^^^^ ``` @@ -314,7 +346,7 @@ Turns to: > console.log(`Hello ${person}, today is ${date.toDateString()}!`) > } > -> greet('Maddison', new Date()) +> greet("Maddison", new Date()) > ``` > With: @@ -343,10 +375,10 @@ Turns to: ```ts // @filename: file-with-export.ts -export const helloWorld = 'Example string' +export const helloWorld = "Example string" // @filename: index.ts -import { helloWorld } from './file-with-export' +import { helloWorld } from "./file-with-export" console.log(helloWorld) ``` @@ -354,10 +386,10 @@ Turns to: > ```ts > // @filename: file-with-export.ts -> export const helloWorld = 'Example string' +> export const helloWorld = "Example string" > > // @filename: index.ts -> import { helloWorld } from './file-with-export' +> import { helloWorld } from "./file-with-export" > console.log(helloWorld) > ``` @@ -378,14 +410,14 @@ Turns to: #### `query.ts` ```ts -let foo = 'hello there!' +let foo = "hello there!" // ^? ``` Turns to: > ```ts -> let foo = 'hello there!' +> let foo = "hello there!" > ``` > With: @@ -397,14 +429,13 @@ Turns to: > "highlights": [], > "queries": [ > { +> "docs": "", > "kind": "query", -> "offset": 4, -> "position": 4, +> "start": 4, +> "length": 15, > "text": "let foo: string", -> "docs": "", -> "line": 1, -> "start": 3, -> "length": 4 +> "offset": 4, +> "line": 0 > } > ], > "staticQuickInfos": "[ 1 items ]", @@ -434,7 +465,7 @@ Turns to: > var __read = > (this && this.__read) || > function (o, n) { -> var m = typeof Symbol === 'function' && o[Symbol.iterator] +> var m = typeof Symbol === "function" && o[Symbol.iterator] > if (!m) return o > var i = m.call(o), > r, @@ -446,7 +477,7 @@ Turns to: > e = { error: error } > } finally { > try { -> if (r && !r.done && (m = i['return'])) m.call(i) +> if (r && !r.done && (m = i["return"])) m.call(i) > } finally { > if (e) throw e.error > } @@ -489,6 +520,7 @@ The API is one main exported function: * * @param code The twoslash markup'd code * @param extension For example: "ts", "tsx", "typescript", "javascript" or "js". + * @param defaultOptions Allows setting any of the handbook options from outside the function, useful if you don't want LSP identifiers * @param tsModule An optional copy of the TypeScript import, if missing it will be require'd. * @param lzstringModule An optional copy of the lz-string import, if missing it will be require'd. * @param fsMap An optional Map object which is passed into @typescript/vfs - if you are using twoslash on the @@ -497,6 +529,7 @@ The API is one main exported function: export function twoslasher( code: string, extension: string, + defaultOptions?: Partial, tsModule?: TS, lzstringModule?: LZ, fsMap?: Map @@ -513,7 +546,7 @@ export interface TwoSlashReturn { extension: string /** Sample requests to highlight a particular part of the code */ highlights: { - kind: 'highlight' + kind: "highlight" position: number length: number description: string @@ -538,15 +571,19 @@ export interface TwoSlashReturn { }[] /** Requests to use the LSP to get info for a particular symbol in the source */ queries: { - kind: 'query' - /** The index of the text in the file */ - start: number - /** how long the identifier */ - length: number + kind: "query" + /** What line is the highlighted identifier on? */ + line: number + /** At what index in the line does the caret represent */ offset: number - // TODO: Add these so we can present something + /** The text of the token which is highlighted */ text: string + /** Any attached JSDocs */ docs: string | undefined + /** The token start which the query indicates */ + start: number + /** The length of the token */ + length: number }[] /** Diagnostic error messages which came up when creating the program */ errors: { diff --git a/packages/ts-twoslasher/package.json b/packages/ts-twoslasher/package.json index 4c8e57d0bbad..55fb9053d9d0 100644 --- a/packages/ts-twoslasher/package.json +++ b/packages/ts-twoslasher/package.json @@ -1,6 +1,6 @@ { "name": "@typescript/twoslash", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "author": "TypeScript team", "main": "dist/index.js", diff --git a/packages/ts-twoslasher/src/index.ts b/packages/ts-twoslasher/src/index.ts index e66b101b894a..6a7e9fbb6038 100644 --- a/packages/ts-twoslasher/src/index.ts +++ b/packages/ts-twoslasher/src/index.ts @@ -1,8 +1,8 @@ -import debug from 'debug' +import debug from "debug" -type LZ = typeof import('lz-string') -type TS = typeof import('typescript') -type CompilerOptions = import('typescript').CompilerOptions +type LZ = typeof import("lz-string") +type TS = typeof import("typescript") +type CompilerOptions = import("typescript").CompilerOptions import { parsePrimitive, @@ -11,18 +11,18 @@ import { typesToExtension, stringAroundIndex, getIdentifierTextSpans, -} from './utils' -import { validateInput, validateCodeForErrors } from './validation' +} from "./utils" +import { validateInput, validateCodeForErrors } from "./validation" -import { createSystem, createVirtualTypeScriptEnvironment, createDefaultMapFromNodeModules } from '@typescript/vfs' +import { createSystem, createVirtualTypeScriptEnvironment, createDefaultMapFromNodeModules } from "@typescript/vfs" -const log = debug('twoslasher') +const log = debug("twoslasher") // Hacking in some internal stuff -declare module 'typescript' { +declare module "typescript" { type Option = { name: string - type: 'list' | 'boolean' | 'number' | 'string' | import('typescript').Map + type: "list" | "boolean" | "number" | "string" | import("typescript").Map element?: Option } @@ -30,16 +30,24 @@ declare module 'typescript' { } type QueryPosition = { - kind: 'query' - position: number + kind: "query" offset: number text: string | undefined docs: string | undefined line: number } +type PartialQueryResults = { + kind: string + text: string + docs: string | undefined + line: number + offset: number + file: string +} + type HighlightPosition = { - kind: 'highlight' + kind: "highlight" position: number length: number description: string @@ -52,25 +60,29 @@ function filterHighlightLines(codeLines: string[]): { highlights: HighlightPosit let nextContentOffset = 0 let contentOffset = 0 + let removedLines = 0 + for (let i = 0; i < codeLines.length; i++) { const line = codeLines[i] const highlightMatch = /^\/\/\s*\^+( .+)?$/.exec(line) const queryMatch = /^\/\/\s*\^\?\s*$/.exec(line) if (queryMatch !== null) { - const start = line.indexOf('^') - const position = contentOffset + start - queries.push({ kind: 'query', offset: start, position, text: undefined, docs: undefined, line: i }) + const start = line.indexOf("^") + queries.push({ kind: "query", offset: start, text: undefined, docs: undefined, line: i + removedLines - 1 }) log(`Removing line ${i} for having a query`) + + removedLines++ codeLines.splice(i, 1) i-- } else if (highlightMatch !== null) { - const start = line.indexOf('^') - const length = line.lastIndexOf('^') - start + 1 + const start = line.indexOf("^") + const length = line.lastIndexOf("^") - start + 1 const position = contentOffset + start - const description = highlightMatch[1] ? highlightMatch[1].trim() : '' - highlights.push({ kind: 'highlight', position, length, description, line: i }) + const description = highlightMatch[1] ? highlightMatch[1].trim() : "" + highlights.push({ kind: "highlight", position, length, description, line: i }) log(`Removing line ${i} for having a highlight`) codeLines.splice(i, 1) + removedLines++ i-- } else { contentOffset = nextContentOffset @@ -86,14 +98,14 @@ function setOption(name: string, value: string, opts: CompilerOptions, ts: TS) { for (const opt of ts.optionDeclarations) { if (opt.name.toLowerCase() === name.toLowerCase()) { switch (opt.type) { - case 'number': - case 'string': - case 'boolean': + case "number": + case "string": + case "boolean": opts[opt.name] = parsePrimitive(value, opt.type) break - case 'list': - opts[opt.name] = value.split(',').map((v) => parsePrimitive(v, opt.element!.type as string)) + case "list": + opts[opt.name] = value.split(",").map(v => parsePrimitive(v, opt.element!.type as string)) break default: @@ -101,7 +113,7 @@ function setOption(name: string, value: string, opts: CompilerOptions, ts: TS) { log(`Set ${opt.name} to ${opts[opt.name]}`) if (opts[opt.name] === undefined) { const keys = Array.from(opt.type.keys() as any) - throw new Error(`Invalid value ${value} for ${opt.name}. Allowed values: ${keys.join(',')}`) + throw new Error(`Invalid value ${value} for ${opt.name}. Allowed values: ${keys.join(",")}`) } break } @@ -123,10 +135,10 @@ function filterCompilerOptions(codeLines: string[], defaultCompilerOptions: Comp let match if ((match = booleanConfigRegexp.exec(codeLines[i]))) { options[match[1]] = true - setOption(match[1], 'true', options, ts) + setOption(match[1], "true", options, ts) } else if ((match = valuedConfigRegexp.exec(codeLines[i]))) { // Skip a filename tag, which should propagate through this stage - if (match[1] === 'filename') { + if (match[1] === "filename") { i++ continue } @@ -154,16 +166,22 @@ export interface ExampleOptions { */ showEmittedFile: string - /** Whether to disable the pre-cache of LSP calls for interesting identifiers */ - noStaticSemanticInfo: false + /** Whether to disable the pre-cache of LSP calls for interesting identifiers, defaults to false */ + noStaticSemanticInfo: boolean + /** Declare that the TypeScript program should edit the fsMap which is passed in, this is only useful for tool-makers, defaults to false */ + emit: boolean + /** Declare that you don't need to validate that errors have corresponding annotations, defaults to false */ + noErrorValidation: boolean } const defaultHandbookOptions: ExampleOptions = { errors: [], noErrors: false, showEmit: false, - showEmittedFile: 'index.js', + showEmittedFile: "index.js", noStaticSemanticInfo: false, + emit: false, + noErrorValidation: false, } function filterHandbookOptions(codeLines: string[]): ExampleOptions { @@ -188,9 +206,9 @@ function filterHandbookOptions(codeLines: string[]): ExampleOptions { } // Edge case the errors object to turn it into a string array - if ('errors' in options && typeof options.errors === 'string') { - options.errors = options.errors.split(' ').map(Number) - log('Setting options.error to ', options.errors) + if ("errors" in options && typeof options.errors === "string") { + options.errors = options.errors.split(" ").map(Number) + log("Setting options.error to ", options.errors) } return options @@ -205,7 +223,7 @@ export interface TwoSlashReturn { /** Sample requests to highlight a particular part of the code */ highlights: { - kind: 'highlight' + kind: "highlight" position: number length: number description: string @@ -232,15 +250,19 @@ export interface TwoSlashReturn { /** Requests to use the LSP to get info for a particular symbol in the source */ queries: { - kind: 'query' - /** The index of the text in the file */ - start: number - /** how long the identifier */ - length: number + kind: "query" + /** What line is the highlighted identifier on? */ + line: number + /** At what index in the line does the caret represent */ offset: number - // TODO: Add these so we can present something + /** The text of the token which is highlighted */ text: string + /** Any attached JSDocs */ docs: string | undefined + /** The token start which the query indicates */ + start: number + /** The length of the token */ + length: number }[] /** Diagnostic error messages which came up when creating the program */ @@ -265,6 +287,7 @@ export interface TwoSlashReturn { * * @param code The twoslash markup'd code * @param extension For example: "ts", "tsx", "typescript", "javascript" or "js". + * @param defaultOptions Allows setting any of the handbook options from outside the function, useful if you don't want LSP identifiers * @param tsModule An optional copy of the TypeScript import, if missing it will be require'd. * @param lzstringModule An optional copy of the lz-string import, if missing it will be require'd. * @param fsMap An optional Map object which is passed into @typescript/vfs - if you are using twoslash on the @@ -273,16 +296,17 @@ export interface TwoSlashReturn { export function twoslasher( code: string, extension: string, + defaultOptions?: Partial, tsModule?: TS, lzstringModule?: LZ, fsMap?: Map ): TwoSlashReturn { - const ts: TS = tsModule ?? require('typescript') - const lzstring: LZ = lzstringModule ?? require('lz-string') + const ts: TS = tsModule ?? require("typescript") + const lzstring: LZ = lzstringModule ?? require("lz-string") const originalCode = code const safeExtension = typesToExtension(extension) - const defaultFileName = 'index.' + safeExtension + const defaultFileName = "index." + safeExtension log(`\n\nLooking at code: \n\`\`\`${safeExtension}\n${code}\n\`\`\`\n`) @@ -299,7 +323,7 @@ export function twoslasher( // This is mutated as the below functions pull out info const codeLines = code.split(/\r\n?|\n/g) - const handbookOptions = filterHandbookOptions(codeLines) + const handbookOptions = { ...filterHandbookOptions(codeLines), ...defaultOptions } const compilerOptions = filterCompilerOptions(codeLines, defaultCompilerOptions, ts) const vfs = fsMap ?? createLocallyPoweredVFS(compilerOptions) @@ -307,19 +331,20 @@ export function twoslasher( const env = createVirtualTypeScriptEnvironment(system, [], ts, compilerOptions) const ls = env.languageService - code = codeLines.join('\n') + code = codeLines.join("\n") - let queries = [] as TwoSlashReturn['queries'] - let highlights = [] as TwoSlashReturn['highlights'] + let partialQueries = [] as PartialQueryResults[] + let queries = [] as TwoSlashReturn["queries"] + let highlights = [] as TwoSlashReturn["highlights"] // TODO: This doesn't handle a single file with a name - const fileContent = code.split('// @filename: ') + const fileContent = code.split("// @filename: ") const noFilepaths = fileContent.length === 1 const makeDefault: [string, string[]] = [defaultFileName, code.split(/\r\n?|\n/g)] const makeMultiFile = (filenameSplit: string): [string, string[]] => { const [filename, ...content] = filenameSplit.split(/\r\n?|\n/g) - const firstLine = '// @filename: ' + filename + const firstLine = "// @filename: " + filename return [filename, [firstLine, ...content]] } @@ -329,16 +354,16 @@ export function twoslasher( * [name, lines_of_code] basically for each set. */ const unfilteredNameContent: Array<[string, string[]]> = noFilepaths ? [makeDefault] : fileContent.map(makeMultiFile) - const nameContent = unfilteredNameContent.filter((n) => n[0].length) + const nameContent = unfilteredNameContent.filter(n => n[0].length) /** All of the referenced files in the markup */ - const filenames = nameContent.map((nc) => nc[0]) + const filenames = nameContent.map(nc => nc[0]) for (const file of nameContent) { const [filename, codeLines] = file // Create the file in the vfs - const newFileCode = codeLines.join('\n') + const newFileCode = codeLines.join("\n") env.createFile(filename, newFileCode) const updates = filterHighlightLines(codeLines) @@ -346,103 +371,128 @@ export function twoslasher( // ------ Do the LSP lookup for the queries - // TODO: this is not perfect, it seems to have issues when there are multiple queries - // in the same sourcefile. Looks like it's about removing the query comments before them. - let removedChars = 0 - const lspedQueries = updates.queries.map((q) => { - const quickInfo = ls.getQuickInfoAtPosition(filename, q.position - removedChars) - const token = ls.getDefinitionAtPosition(filename, q.position - removedChars) + const lspedQueries = updates.queries.map((q, i) => { + const sourceFile = env.getSourceFile(filename)! + const position = ts.getPositionOfLineAndCharacter(sourceFile, q.line, q.offset) + const quickInfo = ls.getQuickInfoAtPosition(filename, position) + const token = ls.getDefinitionAtPosition(filename, position) - removedChars += ('//' + q.offset + '?^\n').length - - let text = `Could not get LSP result: ${stringAroundIndex(env.getSourceFile(filename)!.text, q.position)}` - let docs, - start = 0, - length = 0 + // prettier-ignore + let text = `Could not get LSP result: ${stringAroundIndex(env.getSourceFile(filename)!.text, position)}` + let docs = undefined if (quickInfo && token && quickInfo.displayParts) { - text = quickInfo.displayParts.map((dp) => dp.text).join('') - docs = quickInfo.documentation ? quickInfo.documentation.map((d) => d.text).join('
') : undefined - length = token[0].textSpan.start - start = token[0].textSpan.length + text = quickInfo.displayParts.map(dp => dp.text).join("") + docs = quickInfo.documentation ? quickInfo.documentation.map(d => d.text).join("
") : undefined } - const queryResult = { ...q, text, docs, start, length } + const queryResult = { + kind: "query", + text, + docs, + line: q.line - i, + offset: q.offset, + file: filename, + } return queryResult }) - queries.push(...lspedQueries) + partialQueries.push(...lspedQueries) // Sets the file in the compiler as being without the comments - const newEditedFileCode = codeLines.join('\n') + const newEditedFileCode = codeLines.join("\n") env.updateFile(filename, newEditedFileCode) } // We need to also strip the highlights + queries from the main file which is shown to people const allCodeLines = code.split(/\r\n?|\n/g) filterHighlightLines(allCodeLines) - code = allCodeLines.join('\n') + code = allCodeLines.join("\n") + + // Lets fs changes propagate back up to the fsMap + if (handbookOptions.emit) { + env.languageService.getProgram()?.emit() + } // Code should now be safe to compile, so we're going to split it into different files - const errs: import('typescript').Diagnostic[] = [] + const errs: import("typescript").Diagnostic[] = [] // Let because of a filter when cutting - let staticQuickInfos: TwoSlashReturn['staticQuickInfos'] = [] + let staticQuickInfos: TwoSlashReturn["staticQuickInfos"] = [] // Iterate through the declared files and grab errors and LSP quickinfos // const declaredFiles = Object.keys(fileMap) - filenames.forEach((file) => { + filenames.forEach(file => { if (!handbookOptions.noErrors) { errs.push(...ls.getSemanticDiagnostics(file)) errs.push(...ls.getSyntacticDiagnostics(file)) } + const source = env.sys.readFile(file)! + const sourceFile = env.getSourceFile(file) + if (!sourceFile) throw new Error(`No sourcefile found for ${file} in twoslash`) + // Get all of the interesting quick info popover if (!handbookOptions.noStaticSemanticInfo && !handbookOptions.showEmit) { - // const fileRep = fileMap[file] - const source = env.sys.readFile(file)! - const fileContentStartIndexInModifiedFile = code.indexOf(source) == -1 ? 0 : code.indexOf(source) - - const sourceFile = env.getSourceFile(file) - if (sourceFile) { - // Get all interesting identifiers in the file, so we can show hover info for it - const identifiers = getIdentifierTextSpans(ts, sourceFile) - for (const identifier of identifiers) { - const span = identifier.span - const quickInfo = ls.getQuickInfoAtPosition(file, span.start) - - if (quickInfo && quickInfo.displayParts) { - const text = quickInfo.displayParts.map((dp) => dp.text).join('') - const targetString = identifier.text - const docs = quickInfo.documentation ? quickInfo.documentation.map((d) => d.text).join('\n') : undefined - - // Get the position of the - const position = span.start + fileContentStartIndexInModifiedFile - // Use TypeScript to pull out line/char from the original code at the position + any previous offset - const burnerSourceFile = ts.createSourceFile('_.ts', code, ts.ScriptTarget.ES2015) - const { line, character } = ts.getLineAndCharacterOfPosition(burnerSourceFile, position) - - staticQuickInfos.push({ text, docs, start: position, length: span.length, line, character, targetString }) - } + const linesAbove = code.slice(0, fileContentStartIndexInModifiedFile).split("\n").length - 1 + + // Get all interesting identifiers in the file, so we can show hover info for it + const identifiers = getIdentifierTextSpans(ts, sourceFile) + for (const identifier of identifiers) { + const span = identifier.span + const quickInfo = ls.getQuickInfoAtPosition(file, span.start) + + if (quickInfo && quickInfo.displayParts) { + const text = quickInfo.displayParts.map(dp => dp.text).join("") + const targetString = identifier.text + const docs = quickInfo.documentation ? quickInfo.documentation.map(d => d.text).join("\n") : undefined + + // Get the position of the + const position = span.start + fileContentStartIndexInModifiedFile + // Use TypeScript to pull out line/char from the original code at the position + any previous offset + const burnerSourceFile = ts.createSourceFile("_.ts", code, ts.ScriptTarget.ES2015) + const { line, character } = ts.getLineAndCharacterOfPosition(burnerSourceFile, position) + + staticQuickInfos.push({ text, docs, start: position, length: span.length, line, character, targetString }) } } + + // Offset the queries for this file because they are based on the line for that one + // specific file, and not the global twoslash document. This has to be done here because + // in the above loops, the code for queries/highlights hasn't been stripped yet. + partialQueries + .filter((q: any) => q.file === file) + .forEach(q => { + const pos = + ts.getPositionOfLineAndCharacter(sourceFile, q.line, q.offset) + fileContentStartIndexInModifiedFile + + queries.push({ + docs: q.docs, + kind: "query", + start: pos, + length: q.text.length, + text: q.text, + offset: q.offset, + line: q.line + linesAbove, + }) + }) } }) - const relevantErrors = errs.filter((e) => e.file && filenames.includes(e.file.fileName)) + const relevantErrors = errs.filter(e => e.file && filenames.includes(e.file.fileName)) // A validator that error codes are mentioned, so we can know if something has broken in the future - if (relevantErrors.length) { + if (!handbookOptions.noErrorValidation && relevantErrors.length) { validateCodeForErrors(relevantErrors, handbookOptions, extension, originalCode) } - let errors: TwoSlashReturn['errors'] = [] + let errors: TwoSlashReturn["errors"] = [] // We can't pass the ts.DiagnosticResult out directly (it can't be JSON.stringified) for (const err of relevantErrors) { const codeWhereErrorLives = env.sys.readFile(err.file!.fileName)! const fileContentStartIndexInModifiedFile = code.indexOf(codeWhereErrorLives) - const renderedMessage = escapeHtml(ts.flattenDiagnosticMessageText(err.messageText, '\n')) + const renderedMessage = escapeHtml(ts.flattenDiagnosticMessageText(err.messageText, "\n")) const id = `err-${err.code}-${err.start}-${err.length}` const { line, character } = ts.getLineAndCharacterOfPosition(err.file!, err.start!) @@ -461,19 +511,19 @@ export function twoslasher( // Handle emitting files if (handbookOptions.showEmit) { const output = ls.getEmitOutput(defaultFileName) - const file = output.outputFiles.find((o) => o.name === handbookOptions.showEmittedFile) + const file = output.outputFiles.find(o => o.name === handbookOptions.showEmittedFile) if (!file) { - const allFiles = output.outputFiles.map((o) => o.name).join(', ') + const allFiles = output.outputFiles.map(o => o.name).join(", ") throw new Error(`Cannot find the file ${handbookOptions.showEmittedFile} - in ${allFiles}`) } code = file.text - extension = file.name.split('.').pop()! + extension = file.name.split(".").pop()! // Remove highlights and queries, because it won't work across transpiles, // though I guess source-mapping could handle the transition highlights = [] - queries = [] + partialQueries = [] staticQuickInfos = [] } @@ -483,38 +533,38 @@ export function twoslasher( // Cutting happens last, and it means editing the lines and character index of all // the type annotations which are attached to a location - const cutString = '// ---cut---\n' + const cutString = "// ---cut---\n" if (code.includes(cutString)) { // Get the place it is, then find the end and the start of the next line const cutIndex = code.indexOf(cutString) + cutString.length - const lineOffset = code.substr(0, cutIndex).split('\n').length - 1 + const lineOffset = code.substr(0, cutIndex).split("\n").length - 1 // Kills the code shown code = code.split(cutString).pop()! // For any type of metadata shipped, it will need to be shifted to // fit in with the new positions after the cut - staticQuickInfos.forEach((info) => { + staticQuickInfos.forEach(info => { info.start -= cutIndex info.line -= lineOffset }) - staticQuickInfos = staticQuickInfos.filter((s) => s.start > -1) + staticQuickInfos = staticQuickInfos.filter(s => s.start > -1) - errors.forEach((err) => { + errors.forEach(err => { if (err.start) err.start -= cutIndex if (err.line) err.line -= lineOffset }) - errors = errors.filter((e) => e.start && e.start > -1) + errors = errors.filter(e => e.start && e.start > -1) - highlights.forEach((highlight) => { + highlights.forEach(highlight => { highlight.position -= cutIndex highlight.line -= lineOffset }) - highlights = highlights.filter((e) => e.position > -1) + highlights = highlights.filter(e => e.position > -1) - queries.forEach((q) => (q.start -= cutIndex)) - queries = queries.filter((q) => q.start > -1) + queries.forEach(q => (q.line -= lineOffset)) + queries = queries.filter(q => q.line > -1) } return { diff --git a/packages/ts-twoslasher/test/cutting.test.ts b/packages/ts-twoslasher/test/cutting.test.ts index 69a8b810a53a..bc1469b046e2 100644 --- a/packages/ts-twoslasher/test/cutting.test.ts +++ b/packages/ts-twoslasher/test/cutting.test.ts @@ -1,23 +1,23 @@ -import { twoslasher } from '../src/index' +import { twoslasher } from "../src/index" -describe('supports hiding the example code', () => { +describe("supports hiding the example code", () => { const file = ` const a = "123" // ---cut--- const b = "345" ` - const result = twoslasher(file, 'ts') + const result = twoslasher(file, "ts") - it('hides the right code', () => { + it("hides the right code", () => { // Has the right code shipped - expect(result.code).not.toContain('const a') - expect(result.code).toContain('const b') + expect(result.code).not.toContain("const a") + expect(result.code).toContain("const b") }) - it.skip('shows the right LSP results', () => { - expect(result.staticQuickInfos.find(info => info.text.includes('const a'))).toBeUndefined() + it("shows the right LSP results", () => { + expect(result.staticQuickInfos.find(info => info.text.includes("const a"))).toBeUndefined() - const bLSPResult = result.staticQuickInfos.find(info => info.text.includes('const b')) + const bLSPResult = result.staticQuickInfos.find(info => info.text.includes("const b")) expect(bLSPResult).toBeTruthy() // b is one char long @@ -27,7 +27,7 @@ const b = "345" }) }) -describe.skip('supports hiding the example code with multi-files', () => { +describe("supports hiding the example code with multi-files", () => { const file = ` // @filename: main-file.ts const a = "123" @@ -35,12 +35,12 @@ const a = "123" // ---cut--- const b = "345" ` - const result = twoslasher(file, 'ts') + const result = twoslasher(file, "ts") - it('shows the right LSP results', () => { - expect(result.staticQuickInfos.find(info => info.text.includes('const a'))).toBeUndefined() + it("shows the right LSP results", () => { + expect(result.staticQuickInfos.find(info => info.text.includes("const a"))).toBeUndefined() - const bLSPResult = result.staticQuickInfos.find(info => info.text.includes('const b')) + const bLSPResult = result.staticQuickInfos.find(info => info.text.includes("const b")) expect(bLSPResult).toBeTruthy() // b is one char long @@ -50,22 +50,23 @@ const b = "345" }) }) -describe.skip('supports handling queries in cut code', () => { +describe("supports handling queries in cut code", () => { const file = ` const a = "123" // ---cut--- const b = "345" // ^? ` - const result = twoslasher(file, 'ts') + const result = twoslasher(file, "ts") - it('shows the right query results', () => { - const bLSPResult = result.queries.find(info => info.start === 6) + it("shows the right query results", () => { + const bLSPResult = result.queries.find(info => info.line === 0) expect(bLSPResult).toBeTruthy() + expect(bLSPResult!.text).toContain("const b:") }) }) -describe('supports handling many queries in cut multi-file code', () => { +describe("supports handling a query in cut multi-file code", () => { const file = ` // @filename: index.ts const a = "123" @@ -75,20 +76,12 @@ const b = "345" const c = "678" // ^? ` - const result = twoslasher(file, 'ts') + const result = twoslasher(file, "ts") - it.skip('shows the right query results', () => { + it("shows the right query results", () => { // 6 = `const ` length - const bQueryResult = result.queries.find(info => info.start === 6) - console.log(result.queries) + const bQueryResult = result.queries.find(info => info.line === 0) expect(bQueryResult).toBeTruthy() - expect(bQueryResult!.text).toContain('const b') - - // 22 = "const b = "345"\nconst " - const cQueryResult = result.queries.find(info => info.start === 22) - expect(cQueryResult).toBeTruthy() - // You can only get one query per file, hard-coding this limitation in for now - // but open to folks (or me) fixing this. - expect(cQueryResult!.text).toContain('Could not get LSP') + expect(bQueryResult!.text).toContain("const c") }) }) diff --git a/packages/ts-twoslasher/test/queries.test.ts b/packages/ts-twoslasher/test/queries.test.ts new file mode 100644 index 000000000000..ec56ccfa2b27 --- /dev/null +++ b/packages/ts-twoslasher/test/queries.test.ts @@ -0,0 +1,81 @@ +import { twoslasher } from "../src/index" + +it("works in a trivial case", () => { + const file = ` +const a = "123" +// ^? + ` + const result = twoslasher(file, "ts") + const bQueryResult = result.queries.find(info => info.line === 1) + + expect(bQueryResult).toBeTruthy() + expect(bQueryResult!.text).toContain("const a") +}) + +it("supports carets in the middle of an identifier", () => { + const file = ` +const abc = "123" +// ^? + ` + const result = twoslasher(file, "ts") + const bQueryResult = result.queries.find(info => info.line === 1) + expect(bQueryResult!.text).toContain("const abc") +}) + +it("supports two queries", () => { + const file = ` +const a = "123" +// ^? +const b = "345" +// ^? + ` + const result = twoslasher(file, "ts") + + const aQueryResult = result.queries.find(info => info.line === 1) + expect(aQueryResult!.text).toContain("const a:") + + const bQueryResult = result.queries.find(info => info.line === 2) + expect(bQueryResult!.text).toContain("const b:") +}) + +it("supports many queries", () => { + const file = ` +const a = "123" +// ^? +const b = "345" +// ^? +// A comment to throw things off +let c = "789" +// ^? + ` + const result = twoslasher(file, "ts") + expect(result.queries.length).toEqual(3) + + const aQueryResult = result.queries.find(info => info.line === 1) + expect(aQueryResult!.text).toContain("const a:") + + const bQueryResult = result.queries.find(info => info.line === 2) + expect(bQueryResult!.text).toContain("const b:") + + const cQueryResult = result.queries.find(info => info.line === 4) + expect(cQueryResult!.text).toContain("let c:") +}) + +it("supports queries across many files", () => { + const file = ` +// @filename: index.ts +const a = "123" +// ^? +// @filename: main-file-queries.ts +const b = "345" +// ^? + ` + const result = twoslasher(file, "ts") + expect(result.queries.length).toEqual(2) + + const aQueryResult = result.queries.find(info => info.line === 2) + expect(aQueryResult!.text).toContain("const a:") + + const bQueryResult = result.queries.find(info => info.line === 4) + expect(bQueryResult!.text).toContain("const b:") +}) diff --git a/packages/ts-twoslasher/test/results/cuts_out_unneccessary_code.json b/packages/ts-twoslasher/test/results/cuts_out_unneccessary_code.json index 76278da5e5fa..c29b78ad6b71 100644 --- a/packages/ts-twoslasher/test/results/cuts_out_unneccessary_code.json +++ b/packages/ts-twoslasher/test/results/cuts_out_unneccessary_code.json @@ -2,7 +2,35 @@ "code": "function createLabel(idOrName: T): NameOrId {\n throw \"unimplemented\"\n}\n\nlet a = createLabel(\"typescript\");\n\nlet b = createLabel(2.8);\n\nlet c = createLabel(Math.random() ? \"hello\" : 42);\n", "extension": "ts", "highlights": [], - "queries": [], + "queries": [ + { + "docs": "", + "kind": "query", + "start": 354, + "length": 16, + "text": "let a: NameLabel", + "offset": 4, + "line": 4 + }, + { + "docs": "", + "kind": "query", + "start": 390, + "length": 14, + "text": "let b: IdLabel", + "offset": 4, + "line": 6 + }, + { + "docs": "", + "kind": "query", + "start": 417, + "length": 26, + "text": "let c: IdLabel | NameLabel", + "offset": 4, + "line": 8 + } + ], "staticQuickInfos": [ { "text": "function createLabel(idOrName: T): NameOrId", diff --git a/packages/ts-twoslasher/test/results/query.json b/packages/ts-twoslasher/test/results/query.json index ebaa4489cbf9..92eb4116423c 100644 --- a/packages/ts-twoslasher/test/results/query.json +++ b/packages/ts-twoslasher/test/results/query.json @@ -4,14 +4,13 @@ "highlights": [], "queries": [ { + "docs": "", "kind": "query", - "offset": 4, - "position": 4, + "start": 4, + "length": 15, "text": "let foo: string", - "docs": "", - "line": 1, - "start": 3, - "length": 4 + "offset": 4, + "line": 0 } ], "staticQuickInfos": [ diff --git a/packages/typescript-vfs/src/index.ts b/packages/typescript-vfs/src/index.ts index 7dd4f8e248df..4478d21ac1aa 100644 --- a/packages/typescript-vfs/src/index.ts +++ b/packages/typescript-vfs/src/index.ts @@ -1,21 +1,21 @@ -type System = import('typescript').System -type CompilerOptions = import('typescript').CompilerOptions -type LanguageServiceHost = import('typescript').LanguageServiceHost -type CompilerHost = import('typescript').CompilerHost -type SourceFile = import('typescript').SourceFile -type TS = typeof import('typescript') +type System = import("typescript").System +type CompilerOptions = import("typescript").CompilerOptions +type LanguageServiceHost = import("typescript").LanguageServiceHost +type CompilerHost = import("typescript").CompilerHost +type SourceFile = import("typescript").SourceFile +type TS = typeof import("typescript") const hasLocalStorage = typeof localStorage !== `undefined` const hasProcess = typeof process !== `undefined` -const shouldDebug = (hasLocalStorage && localStorage.getItem('DEBUG')) || (hasProcess && process.env.DEBUG) -const debugLog = shouldDebug ? console.log : (_message?: any, ..._optionalParams: any[]) => '' +const shouldDebug = (hasLocalStorage && localStorage.getItem("DEBUG")) || (hasProcess && process.env.DEBUG) +const debugLog = shouldDebug ? console.log : (_message?: any, ..._optionalParams: any[]) => "" export interface VirtualTypeScriptEnvironment { sys: System - languageService: import('typescript').LanguageService - getSourceFile: (fileName: string) => import('typescript').SourceFile | undefined + languageService: import("typescript").LanguageService + getSourceFile: (fileName: string) => import("typescript").SourceFile | undefined createFile: (fileName: string, content: string) => void - updateFile: (fileName: string, content: string, replaceTextSpan?: import('typescript').TextSpan) => void + updateFile: (fileName: string, content: string, replaceTextSpan?: import("typescript").TextSpan) => void } /** @@ -48,7 +48,7 @@ export function createVirtualTypeScriptEnvironment( return { sys, languageService, - getSourceFile: (fileName) => languageService.getProgram()?.getSourceFile(fileName), + getSourceFile: fileName => languageService.getProgram()?.getSourceFile(fileName), createFile: (fileName, content) => { updateFile(ts.createSourceFile(fileName, content, mergedCompilerOpts.target!, false)) @@ -85,72 +85,72 @@ export const knownLibFilesForCompilerOptions = (compilerOptions: CompilerOptions const lib = compilerOptions.lib || [] const files = [ - 'lib.d.ts', - 'lib.dom.d.ts', - 'lib.dom.iterable.d.ts', - 'lib.webworker.d.ts', - 'lib.webworker.importscripts.d.ts', - 'lib.scripthost.d.ts', - 'lib.es5.d.ts', - 'lib.es6.d.ts', - 'lib.es2015.collection.d.ts', - 'lib.es2015.core.d.ts', - 'lib.es2015.d.ts', - 'lib.es2015.generator.d.ts', - 'lib.es2015.iterable.d.ts', - 'lib.es2015.promise.d.ts', - 'lib.es2015.proxy.d.ts', - 'lib.es2015.reflect.d.ts', - 'lib.es2015.symbol.d.ts', - 'lib.es2015.symbol.wellknown.d.ts', - 'lib.es2016.array.include.d.ts', - 'lib.es2016.d.ts', - 'lib.es2016.full.d.ts', - 'lib.es2017.d.ts', - 'lib.es2017.full.d.ts', - 'lib.es2017.intl.d.ts', - 'lib.es2017.object.d.ts', - 'lib.es2017.sharedmemory.d.ts', - 'lib.es2017.string.d.ts', - 'lib.es2017.typedarrays.d.ts', - 'lib.es2018.asyncgenerator.d.ts', - 'lib.es2018.asynciterable.d.ts', - 'lib.es2018.d.ts', - 'lib.es2018.full.d.ts', - 'lib.es2018.intl.d.ts', - 'lib.es2018.promise.d.ts', - 'lib.es2018.regexp.d.ts', - 'lib.es2019.array.d.ts', - 'lib.es2019.d.ts', - 'lib.es2019.full.d.ts', - 'lib.es2019.object.d.ts', - 'lib.es2019.string.d.ts', - 'lib.es2019.symbol.d.ts', - 'lib.es2020.d.ts', - 'lib.es2020.full.d.ts', - 'lib.es2020.string.d.ts', - 'lib.es2020.symbol.wellknown.d.ts', - 'lib.es2020.bigint.d.ts', - 'lib.es2020.promise.d.ts', - 'lib.esnext.array.d.ts', - 'lib.esnext.asynciterable.d.ts', - 'lib.esnext.bigint.d.ts', - 'lib.esnext.d.ts', - 'lib.esnext.full.d.ts', - 'lib.esnext.intl.d.ts', - 'lib.esnext.symbol.d.ts', + "lib.d.ts", + "lib.dom.d.ts", + "lib.dom.iterable.d.ts", + "lib.webworker.d.ts", + "lib.webworker.importscripts.d.ts", + "lib.scripthost.d.ts", + "lib.es5.d.ts", + "lib.es6.d.ts", + "lib.es2015.collection.d.ts", + "lib.es2015.core.d.ts", + "lib.es2015.d.ts", + "lib.es2015.generator.d.ts", + "lib.es2015.iterable.d.ts", + "lib.es2015.promise.d.ts", + "lib.es2015.proxy.d.ts", + "lib.es2015.reflect.d.ts", + "lib.es2015.symbol.d.ts", + "lib.es2015.symbol.wellknown.d.ts", + "lib.es2016.array.include.d.ts", + "lib.es2016.d.ts", + "lib.es2016.full.d.ts", + "lib.es2017.d.ts", + "lib.es2017.full.d.ts", + "lib.es2017.intl.d.ts", + "lib.es2017.object.d.ts", + "lib.es2017.sharedmemory.d.ts", + "lib.es2017.string.d.ts", + "lib.es2017.typedarrays.d.ts", + "lib.es2018.asyncgenerator.d.ts", + "lib.es2018.asynciterable.d.ts", + "lib.es2018.d.ts", + "lib.es2018.full.d.ts", + "lib.es2018.intl.d.ts", + "lib.es2018.promise.d.ts", + "lib.es2018.regexp.d.ts", + "lib.es2019.array.d.ts", + "lib.es2019.d.ts", + "lib.es2019.full.d.ts", + "lib.es2019.object.d.ts", + "lib.es2019.string.d.ts", + "lib.es2019.symbol.d.ts", + "lib.es2020.d.ts", + "lib.es2020.full.d.ts", + "lib.es2020.string.d.ts", + "lib.es2020.symbol.wellknown.d.ts", + "lib.es2020.bigint.d.ts", + "lib.es2020.promise.d.ts", + "lib.esnext.array.d.ts", + "lib.esnext.asynciterable.d.ts", + "lib.esnext.bigint.d.ts", + "lib.esnext.d.ts", + "lib.esnext.full.d.ts", + "lib.esnext.intl.d.ts", + "lib.esnext.symbol.d.ts", ] const targetToCut = ts.ScriptTarget[target] - const matches = files.filter((f) => f.startsWith(`lib.${targetToCut.toLowerCase()}`)) + const matches = files.filter(f => f.startsWith(`lib.${targetToCut.toLowerCase()}`)) const targetCutIndex = files.indexOf(matches.pop()!) const getMax = (array: number[]) => array && array.length ? array.reduce((max, current) => (current > max ? current : max)) : undefined // Find the index for everything in - const indexesForCutting = lib.map((lib) => { - const matches = files.filter((f) => f.startsWith(`lib.${lib.toLowerCase()}`)) + const indexesForCutting = lib.map(lib => { + const matches = files.filter(f => f.startsWith(`lib.${lib.toLowerCase()}`)) if (matches.length === 0) return 0 const cutIndex = files.indexOf(matches.pop()!) @@ -168,19 +168,19 @@ export const knownLibFilesForCompilerOptions = (compilerOptions: CompilerOptions * the local copy of typescript via the file system. */ export const createDefaultMapFromNodeModules = (compilerOptions: CompilerOptions) => { - const ts = require('typescript') - const path = require('path') - const fs = require('fs') + const ts = require("typescript") + const path = require("path") + const fs = require("fs") const getLib = (name: string) => { - const lib = path.dirname(require.resolve('typescript')) - return fs.readFileSync(path.join(lib, name), 'utf8') + const lib = path.dirname(require.resolve("typescript")) + return fs.readFileSync(path.join(lib, name), "utf8") } const libs = knownLibFilesForCompilerOptions(compilerOptions, ts) const fsMap = new Map() - libs.forEach((lib) => { - fsMap.set('/' + lib, getLib(lib)) + libs.forEach(lib => { + fsMap.set("/" + lib, getLib(lib)) }) return fsMap } @@ -202,7 +202,7 @@ export const createDefaultMapFromCDN = ( version: string, cache: boolean, ts: TS, - lzstring?: typeof import('lz-string'), + lzstring?: typeof import("lz-string"), fetcher?: typeof fetch, storer?: typeof localStorage ) => { @@ -222,31 +222,31 @@ export const createDefaultMapFromCDN = ( // Map the known libs to a node fetch promise, then return the contents function uncached() { - return Promise.all(files.map((lib) => fetchlike(prefix + lib).then((resp) => resp.text()))).then((contents) => { - contents.forEach((text, index) => fsMap.set('/' + files[index], text)) + return Promise.all(files.map(lib => fetchlike(prefix + lib).then(resp => resp.text()))).then(contents => { + contents.forEach((text, index) => fsMap.set("/" + files[index], text)) }) } // A localstorage and lzzip aware version of the lib files function cached() { const keys = Object.keys(localStorage) - keys.forEach((key) => { + keys.forEach(key => { // Remove anything which isn't from this version - if (key.startsWith('ts-lib-') && !key.startsWith('ts-lib-' + version)) { + if (key.startsWith("ts-lib-") && !key.startsWith("ts-lib-" + version)) { storelike.removeItem(key) } }) return Promise.all( - files.map((lib) => { + files.map(lib => { const cacheKey = `ts-lib-${version}-${lib}` const content = storelike.getItem(cacheKey) if (!content) { // Make the API call and store the text concent in the cache return fetchlike(prefix + lib) - .then((resp) => resp.text()) - .then((t) => { + .then(resp => resp.text()) + .then(t => { storelike.setItem(cacheKey, zip(t)) return t }) @@ -254,9 +254,9 @@ export const createDefaultMapFromCDN = ( return Promise.resolve(unzip(content)) } }) - ).then((contents) => { + ).then(contents => { contents.forEach((text, index) => { - const name = '/' + files[index] + const name = "/" + files[index] fsMap.set(name, text) }) }) @@ -279,16 +279,16 @@ function audit( return (...args) => { const res = fn(...args) - const smallres = typeof res === 'string' ? res.slice(0, 80) + '...' : res - debugLog('> ' + name, ...args) - debugLog('< ' + smallres) + const smallres = typeof res === "string" ? res.slice(0, 80) + "..." : res + debugLog("> " + name, ...args) + debugLog("< " + smallres) return res } } /** The default compiler options if TypeScript could ever change the compiler options */ -const defaultCompilerOptions = (ts: typeof import('typescript')): CompilerOptions => { +const defaultCompilerOptions = (ts: typeof import("typescript")): CompilerOptions => { return { ...ts.getDefaultCompilerOptions(), jsx: ts.JsxEmit.React, @@ -303,32 +303,31 @@ const defaultCompilerOptions = (ts: typeof import('typescript')): CompilerOption } // "/DOM.d.ts" => "/lib.dom.d.ts" -const libize = (path: string) => path.replace('/', '/lib.').toLowerCase() +const libize = (path: string) => path.replace("/", "/lib.").toLowerCase() /** * Creates an in-memory System object which can be used in a TypeScript program, this * is what provides read/write aspects of the virtual fs */ export function createSystem(files: Map): System { - files = new Map(files) return { args: [], - createDirectory: () => notImplemented('createDirectory'), + createDirectory: () => notImplemented("createDirectory"), // TODO: could make a real file tree - directoryExists: audit('directoryExists', (directory) => { - return Array.from(files.keys()).some((path) => path.startsWith(directory)) + directoryExists: audit("directoryExists", directory => { + return Array.from(files.keys()).some(path => path.startsWith(directory)) }), - exit: () => notImplemented('exit'), - fileExists: audit('fileExists', (fileName) => files.has(fileName) || files.has(libize(fileName))), - getCurrentDirectory: () => '/', + exit: () => notImplemented("exit"), + fileExists: audit("fileExists", fileName => files.has(fileName) || files.has(libize(fileName))), + getCurrentDirectory: () => "/", getDirectories: () => [], - getExecutingFilePath: () => notImplemented('getExecutingFilePath'), - readDirectory: audit('readDirectory', (directory) => (directory === '/' ? Array.from(files.keys()) : [])), - readFile: audit('readFile', (fileName) => files.get(fileName) || files.get(libize(fileName))), - resolvePath: (path) => path, - newLine: '\n', + getExecutingFilePath: () => notImplemented("getExecutingFilePath"), + readDirectory: audit("readDirectory", directory => (directory === "/" ? Array.from(files.keys()) : [])), + readFile: audit("readFile", fileName => files.get(fileName) || files.get(libize(fileName))), + resolvePath: path => path, + newLine: "\n", useCaseSensitiveFileNames: true, - write: () => notImplemented('write'), + write: () => notImplemented("write"), writeFile: (fileName, contents) => { files.set(fileName, contents) }, @@ -355,12 +354,12 @@ export function createVirtualCompilerHost(sys: System, compilerOptions: Compiler const vHost: Return = { compilerHost: { ...sys, - getCanonicalFileName: (fileName) => fileName, - getDefaultLibFileName: () => '/' + ts.getDefaultLibFileName(compilerOptions), // '/lib.d.ts', + getCanonicalFileName: fileName => fileName, + getDefaultLibFileName: () => "/" + ts.getDefaultLibFileName(compilerOptions), // '/lib.d.ts', // getDefaultLibLocation: () => '/', getDirectories: () => [], getNewLine: () => sys.newLine, - getSourceFile: (fileName) => { + getSourceFile: fileName => { return ( sourceFiles.get(fileName) || save( @@ -375,7 +374,7 @@ export function createVirtualCompilerHost(sys: System, compilerOptions: Compiler }, useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, }, - updateFile: (sourceFile) => { + updateFile: sourceFile => { const alreadyExists = sourceFiles.has(sourceFile.fileName) sys.writeFile(sourceFile.fileName, sourceFile.text) sourceFiles.set(sourceFile.fileName, sourceFile) @@ -403,27 +402,27 @@ export function createVirtualLanguageServiceHost( getProjectVersion: () => projectVersion.toString(), getCompilationSettings: () => compilerOptions, getScriptFileNames: () => fileNames, - getScriptSnapshot: (fileName) => { + getScriptSnapshot: fileName => { const contents = sys.readFile(fileName) if (contents) { return ts.ScriptSnapshot.fromString(contents) } return }, - getScriptVersion: (fileName) => { - return fileVersions.get(fileName) || '0' + getScriptVersion: fileName => { + return fileVersions.get(fileName) || "0" }, writeFile: sys.writeFile, } type Return = { languageServiceHost: LanguageServiceHost - updateFile: (sourceFile: import('typescript').SourceFile) => void + updateFile: (sourceFile: import("typescript").SourceFile) => void } const lsHost: Return = { languageServiceHost, - updateFile: (sourceFile) => { + updateFile: sourceFile => { projectVersion++ fileVersions.set(sourceFile.fileName, projectVersion.toString()) if (!fileNames.includes(sourceFile.fileName)) { diff --git a/packages/typescript-vfs/test/index.test.ts b/packages/typescript-vfs/test/index.test.ts index d1ee1fdb340b..8344c0f3a278 100644 --- a/packages/typescript-vfs/test/index.test.ts +++ b/packages/typescript-vfs/test/index.test.ts @@ -4,21 +4,21 @@ import { createDefaultMapFromNodeModules, createDefaultMapFromCDN, knownLibFilesForCompilerOptions, -} from '../src' +} from "../src" -import ts from 'typescript' +import ts from "typescript" -it('runs a virtual environment and gets the right results from the LSP', () => { +it("runs a virtual environment and gets the right results from the LSP", () => { const fsMap = createDefaultMapFromNodeModules({}) - fsMap.set('index.ts', "const hello = 'hi'") + fsMap.set("index.ts", "const hello = 'hi'") const system = createSystem(fsMap) const compilerOpts = {} - const env = createVirtualTypeScriptEnvironment(system, ['index.ts'], ts, compilerOpts) + const env = createVirtualTypeScriptEnvironment(system, ["index.ts"], ts, compilerOpts) // You can then interact with tqhe languageService to introspect the code - const definitions = env.languageService.getDefinitionAtPosition('index.ts', 7) + const definitions = env.languageService.getDefinitionAtPosition("index.ts", 7) expect(definitions).toMatchInlineSnapshot(` Array [ Object { @@ -42,67 +42,81 @@ it('runs a virtual environment and gets the right results from the LSP', () => { }) // Previously lib.dom.d.ts was not included -it('runs a virtual environment with the default globals', () => { +it("runs a virtual environment with the default globals", () => { const fsMap = createDefaultMapFromNodeModules({}) - fsMap.set('index.ts', "console.log('Hi!'')") + fsMap.set("index.ts", "console.log('Hi!'')") const system = createSystem(fsMap) const compilerOpts = {} - const env = createVirtualTypeScriptEnvironment(system, ['index.ts'], ts, compilerOpts) + const env = createVirtualTypeScriptEnvironment(system, ["index.ts"], ts, compilerOpts) - const definitions = env.languageService.getDefinitionAtPosition('index.ts', 7)! + const definitions = env.languageService.getDefinitionAtPosition("index.ts", 7)! expect(definitions.length).toBeGreaterThan(0) }) // Ensures that people can include something lib es2015 etc it("handles 'lib' in compiler options", () => { const compilerOpts = { - lib: ['es2015', 'ES2020'], + lib: ["es2015", "ES2020"], } const fsMap = createDefaultMapFromNodeModules(compilerOpts) - fsMap.set('index.ts', 'Object.keys(console)') + fsMap.set("index.ts", "Object.keys(console)") const system = createSystem(fsMap) - const env = createVirtualTypeScriptEnvironment(system, ['index.ts'], ts, compilerOpts) + const env = createVirtualTypeScriptEnvironment(system, ["index.ts"], ts, compilerOpts) - const definitions = env.languageService.getDefinitionAtPosition('index.ts', 7)! + const definitions = env.languageService.getDefinitionAtPosition("index.ts", 7)! expect(definitions.length).toBeGreaterThan(0) }) // -it('compiles in the right DTS files', () => { +it("compiles in the right DTS files", () => { const opts = { target: ts.ScriptTarget.ES2015 } const fsMap = createDefaultMapFromNodeModules(opts) - fsMap.set('index.ts', '[1,3,5,6].find(a => a === 2)') + fsMap.set("index.ts", "[1,3,5,6].find(a => a === 2)") const system = createSystem(fsMap) - const env = createVirtualTypeScriptEnvironment(system, ['index.ts'], ts, opts) + const env = createVirtualTypeScriptEnvironment(system, ["index.ts"], ts, opts) - const semDiags = env.languageService.getSemanticDiagnostics('index.ts') + const semDiags = env.languageService.getSemanticDiagnostics("index.ts") expect(semDiags.length).toBe(0) }) -it('creates a map from the CDN without cache', async () => { +// Hrm, it would be great to get this working +it.skip("emits new files to the fsMap", () => { + const fsMap = createDefaultMapFromNodeModules({}) + fsMap.set("index.ts", "console.log('Hi!'')") + + const system = createSystem(fsMap) + const compilerOpts = {} + const env = createVirtualTypeScriptEnvironment(system, ["index.ts"], ts, compilerOpts) + const emitted = env.languageService.getProgram()?.emit() + + expect(emitted!.emitSkipped).toEqual(false) + expect(Array.from(fsMap.keys())).toContain("index.js") +}) + +it("creates a map from the CDN without cache", async () => { const fetcher = jest.fn() - fetcher.mockResolvedValue({ text: () => Promise.resolve('// Contents of file') }) + fetcher.mockResolvedValue({ text: () => Promise.resolve("// Contents of file") }) const store = jest.fn() as any const compilerOpts = { target: ts.ScriptTarget.ES5 } const libs = knownLibFilesForCompilerOptions(compilerOpts, ts) expect(libs.length).toBeGreaterThan(0) - const map = await createDefaultMapFromCDN(compilerOpts, '3.7.3', false, ts, undefined, fetcher, store) + const map = await createDefaultMapFromCDN(compilerOpts, "3.7.3", false, ts, undefined, fetcher, store) expect(map.size).toBeGreaterThan(0) libs.forEach(l => { - expect(map.get('/' + l)).toBeDefined() + expect(map.get("/" + l)).toBeDefined() }) }) -it('creates a map from the CDN and stores it in local storage cache', async () => { +it("creates a map from the CDN and stores it in local storage cache", async () => { const fetcher = jest.fn() - fetcher.mockResolvedValue({ text: () => Promise.resolve('// Contents of file') }) + fetcher.mockResolvedValue({ text: () => Promise.resolve("// Contents of file") }) const store: any = { getItem: jest.fn(), @@ -113,17 +127,17 @@ it('creates a map from the CDN and stores it in local storage cache', async () = const libs = knownLibFilesForCompilerOptions(compilerOpts, ts) expect(libs.length).toBeGreaterThan(0) - const map = await createDefaultMapFromCDN(compilerOpts, '3.7.3', true, ts, undefined, fetcher, store) + const map = await createDefaultMapFromCDN(compilerOpts, "3.7.3", true, ts, undefined, fetcher, store) expect(map.size).toBeGreaterThan(0) - libs.forEach(l => expect(map.get('/' + l)).toBeDefined()) + libs.forEach(l => expect(map.get("/" + l)).toBeDefined()) expect(store.setItem).toBeCalledTimes(libs.length) }) -it('creates a map from the CDN and uses the existing local storage cache', async () => { +it("creates a map from the CDN and uses the existing local storage cache", async () => { const fetcher = jest.fn() - fetcher.mockResolvedValue({ text: () => Promise.resolve('// Contents of file') }) + fetcher.mockResolvedValue({ text: () => Promise.resolve("// Contents of file") }) const store: any = { getItem: jest.fn(), @@ -131,48 +145,48 @@ it('creates a map from the CDN and uses the existing local storage cache', async } // Once return a value from the store - store.getItem.mockReturnValueOnce('// From Cache') + store.getItem.mockReturnValueOnce("// From Cache") const compilerOpts = { target: ts.ScriptTarget.ES5 } const libs = knownLibFilesForCompilerOptions(compilerOpts, ts) expect(libs.length).toBeGreaterThan(0) - const map = await createDefaultMapFromCDN(compilerOpts, '3.7.3', true, ts, undefined, fetcher, store) + const map = await createDefaultMapFromCDN(compilerOpts, "3.7.3", true, ts, undefined, fetcher, store) expect(map.size).toBeGreaterThan(0) - libs.forEach(l => expect(map.get('/' + l)).toBeDefined()) + libs.forEach(l => expect(map.get("/" + l)).toBeDefined()) // Should be one less fetch, and the first item would be from the cache instead expect(store.setItem).toBeCalledTimes(libs.length - 1) - expect(map.get('/' + libs[0])).toMatchInlineSnapshot(`"// From Cache"`) + expect(map.get("/" + libs[0])).toMatchInlineSnapshot(`"// From Cache"`) }) describe(knownLibFilesForCompilerOptions, () => { - it('handles blank', () => { + it("handles blank", () => { const libs = knownLibFilesForCompilerOptions({}, ts) expect(libs.length).toBeGreaterThan(0) }) - it('handles a target', () => { + it("handles a target", () => { const baseline = knownLibFilesForCompilerOptions({}, ts) const libs = knownLibFilesForCompilerOptions({ target: ts.ScriptTarget.ES2017 }, ts) expect(libs.length).toBeGreaterThan(baseline.length) }) - it('handles lib', () => { + it("handles lib", () => { const baseline = knownLibFilesForCompilerOptions({}, ts) - const libs = knownLibFilesForCompilerOptions({ lib: ['ES2020'] }, ts) + const libs = knownLibFilesForCompilerOptions({ lib: ["ES2020"] }, ts) expect(libs.length).toBeGreaterThan(baseline.length) }) - it('handles both', () => { + it("handles both", () => { const baseline = knownLibFilesForCompilerOptions({ target: ts.ScriptTarget.ES2016 }, ts) - const libs = knownLibFilesForCompilerOptions({ lib: ['ES2020'], target: ts.ScriptTarget.ES2016 }, ts) + const libs = knownLibFilesForCompilerOptions({ lib: ["ES2020"], target: ts.ScriptTarget.ES2016 }, ts) expect(libs.length).toBeGreaterThan(baseline.length) }) - it('actually includes the right things', () => { + it("actually includes the right things", () => { const baseline = knownLibFilesForCompilerOptions({ target: ts.ScriptTarget.ES2016 }, ts) - expect(baseline).toContain('lib.es2016.d.ts') + expect(baseline).toContain("lib.es2016.d.ts") }) }) diff --git a/packages/typescriptlang-org/.prettierrc b/packages/typescriptlang-org/.prettierrc index 48e90e8d4025..f673c1308b95 100644 --- a/packages/typescriptlang-org/.prettierrc +++ b/packages/typescriptlang-org/.prettierrc @@ -3,5 +3,6 @@ "semi": false, "singleQuote": false, "tabWidth": 2, - "trailingComma": "es5" + "trailingComma": "es5", + "arrowParens": "avoid" } diff --git a/packages/typescriptlang-org/src/components/devNav.tsx b/packages/typescriptlang-org/src/components/devNav.tsx index 98a216298f51..b5d1625ffce9 100644 --- a/packages/typescriptlang-org/src/components/devNav.tsx +++ b/packages/typescriptlang-org/src/components/devNav.tsx @@ -22,11 +22,14 @@ export const DevNav = (props: DevNavProps) => { Twoslash
  • - TypeScript VFS + TypeScript VFS
  • Playground Plugins
  • +
  • + Bug Workbench +
  • } diff --git a/packages/typescriptlang-org/src/components/workbench/plugins/assertions.ts b/packages/typescriptlang-org/src/components/workbench/plugins/assertions.ts new file mode 100644 index 000000000000..17632ddc6145 --- /dev/null +++ b/packages/typescriptlang-org/src/components/workbench/plugins/assertions.ts @@ -0,0 +1,54 @@ +type TwoSlashReturns = import("@typescript/twoslash").TwoSlashReturn + +export const workbenchAssertionsPlugin: import("../../../../static/js/playground").PluginFactory = ( + i, + utils +) => { + let pluginContainer: HTMLDivElement + return { + id: "assertions", + displayName: "Assertions", + didMount: (sandbox, container) => { + pluginContainer = container + }, + noResults: () => {}, + getResults: (sandbox: any, results: TwoSlashReturns) => { + const ds = utils.createDesignSystem(pluginContainer) + ds.clear() + + ds.subtitle("Assertions Found") + + const queriesAsDiags = results.queries.map(t => { + const diag: import("typescript").DiagnosticRelatedInformation = { + category: 3, // ts.DiagnosticCategory.Message, + code: 0, + file: undefined, + length: t.length, + messageText: t.text, + start: t.start, + } + return diag + }) + ds.listDiags(sandbox, sandbox.getModel(), queriesAsDiags) + + const errorsAsDiags = results.errors.map(t => { + const diag: import("typescript").DiagnosticRelatedInformation = { + category: 1, // ts.DiagnosticCategory.Message, + code: t.code, + file: undefined, + length: t.length, + messageText: t.renderedMessage, + start: t.start, + } + return diag + }) + + ds.listDiags(sandbox, sandbox.getModel(), errorsAsDiags) + + ds.subtitle("TLDR") + ds.p( + "You can highlight code which doesn't work as you expect by starting a comment and then adding ^? under the code which is wrong." + ) + }, + } +} diff --git a/packages/typescriptlang-org/src/components/workbench/plugins/emits.ts b/packages/typescriptlang-org/src/components/workbench/plugins/emits.ts new file mode 100644 index 000000000000..6ec9111eccc4 --- /dev/null +++ b/packages/typescriptlang-org/src/components/workbench/plugins/emits.ts @@ -0,0 +1,41 @@ +type TwoSlashReturns = import("@typescript/twoslash").TwoSlashReturn +type PluginFactory = import("../../../../static/js/playground").PluginFactory + +export const workbenchEmitPlugin: PluginFactory = (i, utils) => { + let pluginContainer: HTMLDivElement + + return { + id: "emit", + displayName: "Emit", + didMount: (sandbox, container) => { + pluginContainer = container + }, + noResults: () => {}, + getResults: ( + sandbox: any, + results: TwoSlashReturns, + dtsMap: Map + ) => { + const ds = utils.createDesignSystem(pluginContainer) + if (!dtsMap) { + ds.showEmptyScreen("No emit yet") + return + } + ds.clear() + + // prettier-ignore + ds.p("This section is a WIP, and will eventually show the emitted files from your twoslash run.") + + const files = Array.from(dtsMap.keys()).reverse() + files.forEach(filename => { + if (filename.startsWith("/lib.")) { + // Do something? + ds.subtitle(filename) + } else { + ds.subtitle(filename) + ds.code(dtsMap.get(filename)!.trim()) + } + }) + }, + } +} diff --git a/packages/typescriptlang-org/src/components/workbench/plugins/help.ts b/packages/typescriptlang-org/src/components/workbench/plugins/help.ts new file mode 100644 index 000000000000..79e897c65cdd --- /dev/null +++ b/packages/typescriptlang-org/src/components/workbench/plugins/help.ts @@ -0,0 +1,72 @@ +type Factory = import("../../../../static/js/playground").PluginFactory + +const intro = ` +The bug workbench uses Twoslash to help you create accurate bug reports. +Twoslash is an markup format for TypeScript files which lets you highlight code, handle-multiple files and +show the files the TypeScript compiler creates. +`.trim() + +const why = ` +This means we can make reproductions of bugs which are trivial to verify against many different versions of TypeScript over time. +`.trim() + +const examples = [ + { + issue: 37231, + name: "Incorrect Type Inference Example", + blurb: + "Using // ^? to highlight how inference gives different results at different locations", + code: `// @noImplicitAny: false + +type Entity = { + someDate: Date | null; +} & ({ id: string; } | { id: number; }) + +type RowRendererMeta = { + [key in keyof TInput]: { key: key; caption: string; formatter?: (value: TInput[key]) => string; }; +} +type RowRenderer = RowRendererMeta[keyof RowRendererMeta]; + +const test: RowRenderer = { + key: 'someDate', + caption: 'My Date', + formatter: (value) => value ? value.toString() : '-' // value: any +// ^? +} + +const thisIsNotTheIssue: Partial> = { + someDate: { + key: 'someDate', + caption: 'My Date', + formatter: (value) => value ? value.toString() : '-' // value: Date | null +// ^? + } +}`, + }, +] +export const workbenchHelpPlugin: Factory = (i, utils) => { + return { + id: "help", + displayName: "Help", + didMount: (sandbox, container) => { + const ds = utils.createDesignSystem(container) + + ds.subtitle("Twoslash Overview") + ds.p(intro) + + ds.p(why) + + ds.title("Examples") + + examples.forEach(e => { + // prettier-ignore + ds.subtitle(e.name + ` ${e.issue}`) + ds.p(e.blurb) + const button = document.createElement("button") + button.textContent = "Show example" + button.onclick = () => sandbox.setText(e.code) + container.appendChild(button) + }) + }, + } +} diff --git a/packages/typescriptlang-org/src/components/workbench/plugins/results.ts b/packages/typescriptlang-org/src/components/workbench/plugins/results.ts new file mode 100644 index 000000000000..8971276676e4 --- /dev/null +++ b/packages/typescriptlang-org/src/components/workbench/plugins/results.ts @@ -0,0 +1,38 @@ +type TwoSlashReturns = import("@typescript/twoslash").TwoSlashReturn +type PluginFactory = import("../../../../static/js/playground").PluginFactory + +export const workbenchResultsPlugin: PluginFactory = (i, utils) => { + let pluginContainer: HTMLDivElement + + return { + id: "results", + displayName: "Results", + didMount: (sandbox, container) => { + pluginContainer = container + }, + noResults: () => {}, + getResults: (sandbox: any, results: TwoSlashReturns) => { + const ds = utils.createDesignSystem(pluginContainer) + ds.clear() + + ds.subtitle(`Output Code as ${results.extension}`) + ds.code(results.code) + + // This is a lot of stuff + const showInfo = !!localStorage.getItem("bug-workbench-show-quick-infos") + if (!showInfo) { + // @ts-ignore + results.staticQuickInfos = ["..."] + } + + ds.subtitle(`Twoslash JSON`) + ds.code(JSON.stringify(results, null, " ")) + + ds.localStorageOption({ + display: "Show static quick infos", + flag: "bug-workbench-show-quick-infos", + blurb: "Include the extra info used for showing the hovers", + }) + }, + } +} diff --git a/packages/typescriptlang-org/src/copy/en/playground.ts b/packages/typescriptlang-org/src/copy/en/playground.ts index 254af022c3cc..3f93d793585b 100644 --- a/packages/typescriptlang-org/src/copy/en/playground.ts +++ b/packages/typescriptlang-org/src/copy/en/playground.ts @@ -5,6 +5,7 @@ export const playCopy = { play_subnav_examples: "Examples", play_subnav_examples_close: "Close", play_subnav_whatsnew: "What's New", + play_subnav_settings: "Settings", play_downloading_typescript: "Downloading TypeScript...", // when loading play_downloading_version: "Version...", // when loading play_toolbar_run: "Run", @@ -23,15 +24,16 @@ export const playCopy = { play_sidebar_options_disable_save: "Disable Save-On-Type", play_sidebar_options_disable_save_copy: "Disable changing the URL when you type.", - play_sidebar_options_external: "3rd Party Plugins", - play_sidebar_options_external_warning: + play_sidebar_plugins: "Plugins", + play_sidebar_plugins_options_external: "3rd Party Plugins", + play_sidebar_plugins_options_external_warning: "Warning: Code from plugins comes from third-parties.", - play_sidebar_options_modules: "Custom npm Modules", - play_sidebar_options_modules_placeholder: "Module name from npm.", - play_sidebar_options_plugin_dev: "Plugin Dev", - play_sidebar_options_plugin_dev_option: + play_sidebar_plugins_options_modules: "Custom npm Modules", + play_sidebar_plugins_options_modules_placeholder: "Module name from npm.", + play_sidebar_plugins_plugin_dev: "Plugin Dev", + play_sidebar_plugins_plugin_dev_option: "Connect to localhost:5000", - play_sidebar_options_plugin_dev_copy: + play_sidebar_plugins_plugin_dev_copy: "Automatically try connect to a playground plugin in development mode. You can read more here.", play_export_report_issue: "Report GitHub issue on TypeScript", play_export_copy_md: "Copy as Markdown Issue", diff --git a/packages/typescriptlang-org/src/copy/es/playground.ts b/packages/typescriptlang-org/src/copy/es/playground.ts index 1e9cd349d87c..776986ec1162 100644 --- a/packages/typescriptlang-org/src/copy/es/playground.ts +++ b/packages/typescriptlang-org/src/copy/es/playground.ts @@ -23,15 +23,16 @@ export const playCopy = { play_sidebar_options_disable_save: "Deshabilita el guardado automático", play_sidebar_options_disable_save_copy: "Deshabilita cambiar la URL cuando escribes.", - play_sidebar_options_external: "Complementos externos", - play_sidebar_options_external_warning: + play_sidebar_plugins_options_external: "Complementos externos", + play_sidebar_plugins_options_external_warning: "Advertencia: Código proveniente de complementos son originarios por terceros.", - play_sidebar_options_modules: "Módulos personalizados", - play_sidebar_options_modules_placeholder: "Módulo proveniente de npm.", - play_sidebar_options_plugin_dev: "Desarrollo de complementos", - play_sidebar_options_plugin_dev_option: + play_sidebar_plugins_options_modules: "Módulos personalizados", + play_sidebar_plugins_options_modules_placeholder: + "Módulo proveniente de npm.", + play_sidebar_plugins_plugin_dev: "Desarrollo de complementos", + play_sidebar_plugins_plugin_dev_option: "Conectar a localhost:5000/index.js", - play_sidebar_options_plugin_dev_copy: + play_sidebar_plugins_plugin_dev_copy: "Automaticamente intenta conectar a un complemento para sitio de pruebas en modo desarrollo. Puedes leer más aquí.", play_export_report_issue: "Reporta una incidencia en el repositorio GitHub de TypeScript", diff --git a/packages/typescriptlang-org/src/copy/ja/playground.ts b/packages/typescriptlang-org/src/copy/ja/playground.ts index df8334293562..af5bc5f65f88 100644 --- a/packages/typescriptlang-org/src/copy/ja/playground.ts +++ b/packages/typescriptlang-org/src/copy/ja/playground.ts @@ -23,15 +23,15 @@ export const playCopy = { play_sidebar_options_disable_save: "Save-On-Typeを無効にする", play_sidebar_options_disable_save_copy: "入力時にURLが変更される機能を無効にします。", - play_sidebar_options_external: "外部プラグイン", - play_sidebar_options_external_warning: + play_sidebar_plugins_options_external: "外部プラグイン", + play_sidebar_plugins_options_external_warning: "注意: プラグインのコードはサードパーティのものです。", - play_sidebar_options_modules: "カスタムモジュール", - play_sidebar_options_modules_placeholder: "npmモジュール名", - play_sidebar_options_plugin_dev: "Plugin Dev", - play_sidebar_options_plugin_dev_option: + play_sidebar_plugins_options_modules: "カスタムモジュール", + play_sidebar_plugins_options_modules_placeholder: "npmモジュール名", + play_sidebar_plugins_plugin_dev: "Plugin Dev", + play_sidebar_plugins_plugin_dev_option: "localhost:5000/index.jsに接続する", - play_sidebar_options_plugin_dev_copy: + play_sidebar_plugins_plugin_dev_copy: "ローカルで開発中のプレイグラウンドプラグインに接続します。詳しくはこちら。", play_export_report_issue: "TypeScriptのエラーをGitHub issueとして報告", play_export_copy_md: "Markdown Issueとしてコピー", diff --git a/packages/typescriptlang-org/src/copy/zh/playground.ts b/packages/typescriptlang-org/src/copy/zh/playground.ts index ed7c76ca74be..f6f6c35cab81 100644 --- a/packages/typescriptlang-org/src/copy/zh/playground.ts +++ b/packages/typescriptlang-org/src/copy/zh/playground.ts @@ -22,14 +22,14 @@ export const playCopy = { "禁用 require 或 import 的自动类型获取。", play_sidebar_options_disable_save: "禁用即时保存", play_sidebar_options_disable_save_copy: "禁用输入时改变 URL", - play_sidebar_options_external: "外部插件", - play_sidebar_options_external_warning: "警告: 外部插件来自第三方", - play_sidebar_options_modules: "自定义模块", - play_sidebar_options_modules_placeholder: "npm 上的模块。", - play_sidebar_options_plugin_dev: "插件开发", - play_sidebar_options_plugin_dev_option: + play_sidebar_plugins_options_external: "外部插件", + play_sidebar_plugins_options_external_warning: "警告: 外部插件来自第三方", + play_sidebar_plugins_options_modules: "自定义模块", + play_sidebar_plugins_options_modules_placeholder: "npm 上的模块。", + play_sidebar_plugins_plugin_dev: "插件开发", + play_sidebar_plugins_plugin_dev_option: "访问 localhost:5000/index.js", - play_sidebar_options_plugin_dev_copy: + play_sidebar_plugins_plugin_dev_copy: "在开发模式下自动尝试连接到游乐场的插件。你可以在这里查看更多。", play_export_report_issue: "为 TypeScript 提交 Github issue。", play_export_copy_md: "复制为 Markdown 格式的 issue 模板", diff --git a/packages/typescriptlang-org/src/lib/release-info.json b/packages/typescriptlang-org/src/lib/release-info.json index 41a1f2e6abe5..0579078d3950 100644 --- a/packages/typescriptlang-org/src/lib/release-info.json +++ b/packages/typescriptlang-org/src/lib/release-info.json @@ -23,8 +23,8 @@ "vs2019_download": "https://marketplace.visualstudio.com/items?itemName=TypeScriptTeam.typescript-39beta" }, "rc": { - "vs2017_download": "https://marketplace.visualstudio.com/items?itemName=TypeScriptTeam.typescript-383", - "vs2019_download": "https://marketplace.visualstudio.com/items?itemName=TypeScriptTeam.typescript-383" + "vs2017_download": "https://marketplace.visualstudio.com/items?itemName=TypeScriptTeam.typescript-39beta", + "vs2019_download": "https://marketplace.visualstudio.com/items?itemName=TypeScriptTeam.typescript-39beta" } } } diff --git a/packages/typescriptlang-org/src/pages/dev/bug-workbench.tsx b/packages/typescriptlang-org/src/pages/dev/bug-workbench.tsx new file mode 100644 index 000000000000..eec2edb1d3ee --- /dev/null +++ b/packages/typescriptlang-org/src/pages/dev/bug-workbench.tsx @@ -0,0 +1,243 @@ +import React, { useEffect } from "react" +import ReactDOM from "react-dom" +import { Layout } from "../../components/layout" +import { withPrefix, graphql } from "gatsby" +import { BugWorkbenchQuery } from "../../__generated__/gatsby-types" + +import "../../templates/play.scss" + +import { useIntl } from "react-intl"; +import { createInternational } from "../../lib/createInternational" +import { headCopy } from "../../copy/en/head-seo" +import { playCopy } from "../../copy/en/playground" + +import { Intl } from "../../components/Intl" + +import { workbenchHelpPlugin } from "../../components/workbench/plugins/help" +import { workbenchResultsPlugin } from "../../components/workbench/plugins/results" +import { workbenchEmitPlugin } from "../../components/workbench/plugins/emits" +import { workbenchAssertionsPlugin } from "../../components/workbench/plugins/assertions" +import { createDefaultMapFromCDN } from "@typescript/vfs" +import { twoslasher, TwoSlashReturn } from "@typescript/twoslash" + +type Props = { + data: BugWorkbenchQuery +} + + +const Play: React.FC = (props) => { + const i = createInternational(useIntl()) + let dtsMap: Map = new Map() + + useEffect(() => { + if ("playgroundLoaded" in window) return + window["playgroundLoaded"] = true + + // @ts-ignore - so the config options can use localized descriptions + window.optionsSummary = props.pageContext.optionsSummary + // @ts-ignore - for React-based plugins + window.react = React + // @ts-ignore - for React-based plugins + window.reactDOM = ReactDOM + // @ts-ignore - so that plugins etc can use local functions + window.i = i + + const getLoaderScript = document.createElement('script'); + getLoaderScript.src = withPrefix("/js/vs.loader.js"); + getLoaderScript.async = true; + getLoaderScript.onload = () => { + const params = new URLSearchParams(location.search) + // nothing || Nightly -> next || original ts param + const supportedVersion = !params.get("ts") ? undefined : params.get("ts") === "Nightly" ? "next" : params.get("ts") + const tsVersion = supportedVersion || "next" + + // @ts-ignore + const re = global.require + re.config({ + paths: { + vs: `https://typescript.azureedge.net/cdn/${tsVersion}/monaco/min/vs`, + "typescript-sandbox": withPrefix('/js/sandbox'), + "typescript-playground": withPrefix('/js/playground'), + "unpkg": "https://unpkg.com/", + "local": "http://localhost:5000" + }, + ignoreDuplicateModules: ["vs/editor/editor.main"], + }); + + re(["vs/editor/editor.main", "vs/language/typescript/tsWorker", "typescript-sandbox/index", "typescript-playground/index"], async (main: typeof import("monaco-editor"), tsWorker: any, sandbox: typeof import("typescript-sandbox"), playground: typeof import("typescript-playground")) => { + // Importing "vs/language/typescript/tsWorker" will set ts as a global + const ts = (global as any).ts + const isOK = main && ts && sandbox && playground + if (isOK) { + document.getElementById("loader")!.parentNode?.removeChild(document.getElementById("loader")!) + } else { + console.error("Errr") + console.error("main", !!main, "ts", !!ts, "sandbox", !!sandbox, "playground", !!playground) + } + + // Set the height of monaco to be either your window height or 600px - whichever is smallest + const container = document.getElementById("playground-container")! + container.style.display = "flex" + const height = Math.max(window.innerHeight, 600) + container.style.height = `${height - Math.round(container.getClientRects()[0].top) - 18}px` + + // Create the sandbox + const sandboxEnv = await sandbox.createTypeScriptSandbox({ + text: localStorage.getItem('sandbox-history') || i("play_default_code_sample"), + compilerOptions: {}, + domID: "monaco-editor-embed", + useJavaScript: !!params.get("useJavaScript"), + acquireTypes: !localStorage.getItem("disable-ata") + }, main, ts) + + const playgroundConfig = { + lang: "en", + prefix: withPrefix("/"), + supportCustomPlugins: false, + plugins: [ + workbenchAssertionsPlugin, + workbenchResultsPlugin, + workbenchEmitPlugin, + workbenchHelpPlugin, + ] + } + + const playgroundEnv = playground.setupPlayground(sandboxEnv, main, playgroundConfig, i as any, React) + + const updateDTSEnv = (opts) => { + createDefaultMapFromCDN(opts, tsVersion, true, ts, sandboxEnv.lzstring as any).then((defaultMap) => { + dtsMap = defaultMap + runTwoslash() + }) + } + + // When the compiler notices a twoslash compiler flag change, this will get triggered and reset the DTS map + sandboxEnv.setDidUpdateCompilerSettings(updateDTSEnv) + updateDTSEnv(sandboxEnv.getCompilerOptions()) + + let debouncingTimer = false + sandboxEnv.editor.onDidChangeModelContent(_event => { + // This needs to be last in the function + if (debouncingTimer) return + debouncingTimer = true + setTimeout(() => { + debouncingTimer = false + if (dtsMap) runTwoslash() + }, 500) + }) + + let currentTwoslashResults: Error | TwoSlashReturn | undefined = undefined + let currentDTSMap: Map | undefined = undefined + + let isError = (e: any) => e && e.stack && e.message; + + playgroundEnv.setDidUpdateTab((newPlugin) => { + if (!isError(currentTwoslashResults) && "getResults" in newPlugin) { + // @ts-ignore + newPlugin.getResults(sandboxEnv, currentTwoslashResults, currentDTSMap) + } else if ("noResults" in newPlugin) { + // @ts-ignore + newPlugin.noResults(currentTwoslashResults) + } + }) + + const runTwoslash = () => { + const code = sandboxEnv.getText() + + try { + currentDTSMap = new Map(dtsMap) + const twoslashConfig = { noStaticSemanticInfo: false, emit: true, noErrorValidation: true } as const + const ext = sandboxEnv.filepath.split(".")[1] + const twoslash = twoslasher(code, ext, twoslashConfig, ts, sandboxEnv.lzstring as any, currentDTSMap) + currentTwoslashResults = twoslash + + const currentPlugin = playgroundEnv.getCurrentPlugin() + if ("getResults" in currentPlugin) { + // @ts-ignore + + currentPlugin.getResults(sandboxEnv, twoslash, currentDTSMap) + } + + } catch (error) { + const err = error as Error + console.log(err) + currentTwoslashResults = err + const currentPlugin = playgroundEnv.getCurrentPlugin() + if ("noResults" in currentPlugin) { + // @ts-ignore + currentPlugin.noResults(sandboxEnv, err) + } + } + } + + // Dark mode faff + const darkModeEnabled = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)') + if (darkModeEnabled.matches) { + sandboxEnv.monaco.editor.setTheme("sandbox-dark"); + } + + // On the chance you change your dark mode settings + darkModeEnabled.addListener((e) => { + const darkModeOn = e.matches; + const newTheme = darkModeOn ? "sandbox-dark" : "sandbox-light" + sandboxEnv.monaco.editor.setTheme(newTheme); + }); + + sandboxEnv.editor.focus() + sandboxEnv.editor.layout() + }); + } + + document.body.appendChild(getLoaderScript); + }, []) + + + return ( + + {/** This is the top nav, which is outside of the editor */} + + +
    +
    +
    +

    {i("play_downloading_typescript")}

    +
    +
    +
    + + { /** This is the div which monaco is added into **/} +
    +
    +
    +
    + + ) +} + + +export default (props: Props) => + +export const query = graphql` + query BugWorkbench { + ...AllSitePage + } +` diff --git a/packages/typescriptlang-org/src/pages/dev/twoslash.tsx b/packages/typescriptlang-org/src/pages/dev/twoslash.tsx index 083adf45e174..9b5f20cc2de1 100644 --- a/packages/typescriptlang-org/src/pages/dev/twoslash.tsx +++ b/packages/typescriptlang-org/src/pages/dev/twoslash.tsx @@ -63,7 +63,7 @@ const Index: React.FC = (props) => { mapWithLibFiles.set("index.ts", newContent) try { - const newResults = twoslasher(newContent, "tsx", ts, sandbox.lzstring as any, mapWithLibFiles) + const newResults = twoslasher(newContent, "tsx", ts, undefined, sandbox.lzstring as any, mapWithLibFiles) const codeAsFakeShikiTokens = newResults.code.split("\n").map(line => [{ content: line }]) const html = renderToHTML(codeAsFakeShikiTokens, {}, newResults) diff --git a/packages/typescriptlang-org/src/templates/play.scss b/packages/typescriptlang-org/src/templates/play.scss index 2e1059a70a1d..822e7ac854a9 100644 --- a/packages/typescriptlang-org/src/templates/play.scss +++ b/packages/typescriptlang-org/src/templates/play.scss @@ -223,7 +223,7 @@ } } -// The subnav which starts with "TypeScript" +// The subnav which starts with "Playground" main > nav { position: relative; @@ -238,7 +238,8 @@ main > nav { > ul { li { - &:hover { + &:hover, + &.open { background-color: $ts-light-bg-grey-highlight-color; @media (prefers-color-scheme: dark) { @@ -329,7 +330,6 @@ main #editor-toolbar { flex-grow: 1; display: flex; flex-direction: column; - overflow-x: hidden; @media (prefers-color-scheme: dark) { background-color: #1e1e1e; @@ -345,6 +345,7 @@ main #editor-toolbar { margin-right: -2px; margin-bottom: 10px; + overflow-x: hidden; position: relative; ul.right { @@ -467,6 +468,10 @@ main #editor-toolbar { } } + .playground-settings-container { + margin: 0; + } + pre { margin: 0; } @@ -549,7 +554,7 @@ main #editor-toolbar { } } - #compiler-errors { + ul.compiler-diagnostics { font-family: $font-code; margin: 0; padding: 0; diff --git a/packages/typescriptlang-org/src/templates/play.tsx b/packages/typescriptlang-org/src/templates/play.tsx index 43eca1c0bee4..57f4e0d9217b 100644 --- a/packages/typescriptlang-org/src/templates/play.tsx +++ b/packages/typescriptlang-org/src/templates/play.tsx @@ -94,7 +94,8 @@ const Play: React.FC = (props) => { const playgroundConfig = { lang: props.pageContext.lang, - prefix: withPrefix("/") + prefix: withPrefix("/"), + supportCustomPlugins: true } playground.setupPlayground(sandboxEnv, main, playgroundConfig, i as any, React) @@ -168,9 +169,9 @@ const Play: React.FC = (props) => {
      +
    • Settings
    • {/** -
    • About
    • GitHub
    • diff --git a/yarn.lock b/yarn.lock index 6f9c41342c78..bcd495e09870 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2619,6 +2619,15 @@ semver "^6.3.0" tsutils "^3.17.1" +"@typescript/twoslash@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@typescript/twoslash/-/twoslash-0.2.0.tgz#b09a138ca7c92ce38f6f5437195d52abba269ad6" + integrity sha512-UoXJEAZvujwb315y5xFTys7FSnKwMpsXkQzy9oyoOL/IONfk3y5DSWoZnaLSciPeypgOwckOoDX0O4m7eM/N6w== + dependencies: + "@typescript/vfs" "^1.0.0" + debug "^4.1.1" + lz-string "^1.4.4" + "@uifabric/azure-themes@^7.0.28": version "7.0.28" resolved "https://registry.yarnpkg.com/@uifabric/azure-themes/-/azure-themes-7.0.28.tgz#075770107ceb4b5c771b03941475b894f2267daf"