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