`)
+
+await rm(outputDirectory, { force: true, recursive: true })
+await mkdir(outputDirectory, { recursive: true })
+const libFileNames = (await readdir(libDirectory)).filter(fileName => /^lib(?:\..+)?\.d\.ts$/.test(fileName)).sort()
+const libFiles = Object.fromEntries(
+ await Promise.all(
+ libFileNames.map(async fileName => [
+ `/${basename(fileName)}`,
+ await readFile(resolve(libDirectory, fileName), "utf8"),
+ ])
+ )
+)
+const libFilesJSON = JSON.stringify(libFiles)
+const [wasmBytes, schemaBytes] = await Promise.all([readFile(wasmFile), readFile(configSchema)])
+const assetCacheVersion = createHash("sha256")
+ .update(wasmBytes)
+ .update(libFilesJSON)
+ .update(schemaBytes)
+ .digest("hex")
+ .slice(0, 16)
+await Promise.all([
+ writeFile(resolve(outputDirectory, "index.html"), indexHtml),
+ cp(coiServiceWorker, resolve(outputDirectory, "coi-serviceworker.min.js")),
+ cp(configSchema, resolve(outputDirectory, "tsconfig.schema.json")),
+ cp(releaseIndex, resolve(outputDirectory, "versions.json")),
+ cp(examplesIndex, resolve(outputDirectory, "examples.json")),
+ cp(helpIndex, resolve(outputDirectory, "help.json")),
+ cp(wasmFile, resolve(outputDirectory, "tsc.wasm")),
+ writeFile(resolve(outputDirectory, "lib-files.json"), libFilesJSON),
+])
+
+const buildContext = await context({
+ absWorkingDir: packageDirectory,
+ bundle: true,
+ conditions: ["browser", "default"],
+ define: {
+ __ASSET_CACHE_VERSION__: JSON.stringify(assetCacheVersion),
+ __LOAD_ASSET_SIZES__: JSON.stringify({
+ libraries: Buffer.byteLength(libFilesJSON),
+ schema: schemaBytes.byteLength,
+ wasm: wasmBytes.byteLength,
+ }),
+ __TS_VERSION__: JSON.stringify(version),
+ },
+ entryNames: "[name]",
+ entryPoints: {
+ main: resolve(packageDirectory, "src/main.ts"),
+ "editor.worker": editorWorker,
+ "strada.worker": resolve(packageDirectory, "src/strada.worker.ts"),
+ "tsgo-lsp.worker": resolve(packageDirectory, "src/tsgo-lsp.worker.ts"),
+ },
+ format: "esm",
+ loader: {
+ ".ttf": "file",
+ ".woff": "file",
+ ".woff2": "file",
+ },
+ outdir: outputDirectory,
+ platform: "browser",
+ sourcemap: true,
+ target: ["es2022"],
+})
+
+if (serve) {
+ await buildContext.watch()
+ const server = await buildContext.serve({
+ host: "127.0.0.1",
+ port: 4173,
+ servedir: outputDirectory,
+ })
+ console.log(`TypeScript Playground v2: http://${server.host}:${server.port}`)
+} else {
+ await buildContext.rebuild()
+ await buildContext.dispose()
+ await Promise.all([
+ rm(websiteStaticDirectory, { force: true, recursive: true }),
+ rm(legacyWebsiteStaticDirectory, { force: true, recursive: true }),
+ ])
+ await cp(outputDirectory, websiteStaticDirectory, { recursive: true })
+ await mkdir(legacyWebsiteStaticDirectory, { recursive: true })
+ await writeFile(
+ resolve(legacyWebsiteStaticDirectory, "index.html"),
+ `
+
+TypeScript Playground v2
+
+Continue to TypeScript Playground v2
+`
+ )
+ console.log(`Built TypeScript ${version} playground in ${outputDirectory}`)
+}
diff --git a/packages/playground-v2/scripts/smoke-test.mjs b/packages/playground-v2/scripts/smoke-test.mjs
new file mode 100644
index 000000000000..f0ae896ceb86
--- /dev/null
+++ b/packages/playground-v2/scripts/smoke-test.mjs
@@ -0,0 +1,65 @@
+import assert from "node:assert/strict"
+import { readFile, readdir } from "node:fs/promises"
+import { resolve } from "node:path"
+import { API } from "@typescript/typescript/unstable/sync"
+import { instantiateWasm, WasmTransport, wasmURL } from "@typescript/typescript-wasip1-wasm"
+
+const packageDirectory = resolve(import.meta.dirname, "..")
+const wasm = await readFile(wasmURL)
+const module = await WebAssembly.compile(wasm)
+const instance = await instantiateWasm(module)
+const transport = new WasmTransport({ instance, cwd: "/workspace" })
+const api = new API({ transport })
+
+try {
+ const libDirectory = resolve(packageDirectory, "vendor/lib")
+ const libFileNames = (await readdir(libDirectory)).filter(fileName => /^lib(?:\..+)?\.d\.ts$/.test(fileName))
+ for (const fileName of libFileNames) {
+ transport.setFile(`/${fileName}`, await readFile(resolve(libDirectory, fileName), "utf8"))
+ }
+
+ const files = {
+ "/workspace/tsconfig.json": JSON.stringify({
+ compilerOptions: {
+ declaration: true,
+ module: "CommonJS",
+ strict: true,
+ target: "ES2022",
+ },
+ include: ["./src/**/*"],
+ }),
+ "/workspace/src/greet.ts": "export const greet = (name: string) => `Hello, ${name}!`;",
+ "/workspace/src/index.ts": 'import { greet } from "./greet"; console.log(greet("TS7"));',
+ }
+ for (const [fileName, source] of Object.entries(files)) {
+ transport.setFile(fileName, source)
+ }
+
+ const config = api.readConfigFile("/workspace/tsconfig.json")
+ assert.equal(config.error, undefined)
+ const parsed = api.parseJsonConfigFileContent(config.config, {
+ configFileName: "/workspace/tsconfig.json",
+ })
+ assert.deepEqual(parsed.fileNames, ["/workspace/src/greet.ts", "/workspace/src/index.ts"])
+ const program = api.createProgram(parsed.fileNames, parsed.options, {
+ projectReferences: parsed.projectReferences,
+ configFileParsingDiagnostics: parsed.errors,
+ })
+ try {
+ assert.equal(program.getSyntacticDiagnostics().length, 0)
+ assert.equal(program.getSemanticDiagnostics().length, 0)
+ const emit = program.emitToString()
+ assert.equal(emit.emitSkipped, false)
+ assert.deepEqual(
+ [...emit.outputFiles.keys()],
+ ["/workspace/src/greet.d.ts", "/workspace/src/greet.js", "/workspace/src/index.d.ts", "/workspace/src/index.js"]
+ )
+ assert.match(emit.outputFiles.get("/workspace/src/index.js").text, /require\("\.\/greet"\)/)
+ } finally {
+ program.dispose()
+ }
+} finally {
+ api.close()
+}
+
+console.log("TypeScript WASM API smoke test passed")
diff --git a/packages/playground-v2/scripts/vendor-resources.mjs b/packages/playground-v2/scripts/vendor-resources.mjs
new file mode 100644
index 000000000000..ad875bc8563c
--- /dev/null
+++ b/packages/playground-v2/scripts/vendor-resources.mjs
@@ -0,0 +1,101 @@
+import { cp, mkdir, readFile, writeFile } from "node:fs/promises"
+import { dirname, resolve } from "node:path"
+import { fileURLToPath } from "node:url"
+
+const packageDirectory = resolve(dirname(fileURLToPath(import.meta.url)), "..")
+const websiteDirectory = resolve(packageDirectory, "../..")
+const examplesDirectory = resolve(websiteDirectory, "packages/typescriptlang-org/static/js/examples")
+const examplesIndex = JSON.parse(await readFile(resolve(examplesDirectory, "en.json"), "utf8"))
+const helpIndex = {
+ docs: [
+ {
+ title: "Projects and files",
+ legacyIndexes: [6, 7],
+ html: "The playground is a virtual project rooted at /workspace. Use + File to add source or configuration files, select files in the project tree, and use Delete to remove the active project file.
The project is saved in the page URL and browser storage as you edit.
",
+ },
+ {
+ title: "Compiler versions",
+ legacyIndexes: [8],
+ html: "The selector offers TypeScript 7.1, the latest patch release for each previous major.minor line, and custom playground-CDN build IDs.
Changing compiler versions reloads the page while preserving the project.
",
+ },
+ {
+ title: "Compiler settings",
+ legacyIndexes: [0],
+ html: "Edit tsconfig.json directly. The editor provides schema completion, hover documentation, and validation for compiler options.
Emit and diagnostics use the files and options selected by that configuration.
",
+ },
+ {
+ title: "Package types",
+ legacyIndexes: [4],
+ html: "Imports from npm packages automatically acquire bundled declarations or matching @types packages. Downloaded declarations are cached and available to diagnostics, completion, hover, and go-to-definition.
Package JavaScript is not downloaded, so Run still supports only emitted project files and relative CommonJS imports.
",
+ },
+ {
+ title: "Editor navigation",
+ legacyIndexes: [11],
+ html: "Use hover, completion, references, rename, formatting, quick fixes, and F12 go-to-definition as in an editor. Back and Forward return between project and declaration files.
Place // ^? beneath an expression to display its inferred type.
",
+ },
+ {
+ title: "Emit and Run",
+ legacyIndexes: [2, 3],
+ html: "The Emit panel shows every generated JavaScript and declaration file. Run executes the emitted project with an in-browser CommonJS loader and captures console output.
Set compilerOptions.module to CommonJS when running a project.
",
+ },
+ {
+ title: "Examples",
+ legacyIndexes: [1],
+ html: "Use the Examples button to browse the bundled TypeScript and JavaScript examples. Choosing an example replaces the current project, applies its compiler settings, and selects its requested compiler version.
Links written as // example:example-id are clickable in the editor.
",
+ },
+ {
+ title: "URLs and compatibility",
+ legacyIndexes: [9, 10],
+ html: "New projects use the versioned #code/v2/ format. The playground also accepts old #code/ source links, #src=, compiler-option query parameters, selections, and filename directives.
Legacy example and handbook hashes are migrated when opened.
",
+ },
+ {
+ title: "Settings",
+ legacyIndexes: [5],
+ html: "Use Settings to control automatic package type acquisition, URL updates while typing, font size, tab size, theme, desktop word wrapping, the minimap, and font ligatures.
Changing automatic type acquisition or the theme reloads the page so the language-service filesystem and editor colors are initialized consistently.
",
+ },
+ ],
+}
+
+const examples = await Promise.all(
+ examplesIndex.examples.map(async example => {
+ const fileName = resolve(examplesDirectory, example.lang, ...example.path, example.name)
+ let code = await readFile(fileName, "utf8")
+ if (code.startsWith("//// {")) code = code.split(/\r?\n/).slice(1).join("\n").trim()
+ return { ...example, code }
+ })
+)
+
+const vendorDirectory = resolve(packageDirectory, "vendor")
+const ataVendorDirectory = resolve(vendorDirectory, "ata")
+await mkdir(vendorDirectory, { recursive: true })
+await mkdir(ataVendorDirectory, { recursive: true })
+await Promise.all([
+ writeFile(
+ resolve(vendorDirectory, "examples.json"),
+ `${JSON.stringify({
+ examples,
+ sections: examplesIndex.sections,
+ sortedSubSections: examplesIndex.sortedSubSections,
+ })}\n`
+ ),
+ writeFile(resolve(vendorDirectory, "help.json"), `${JSON.stringify(helpIndex)}\n`),
+ cp(resolve(websiteDirectory, "packages/ata/dist/index.js"), resolve(ataVendorDirectory, "index.js")),
+ cp(resolve(websiteDirectory, "packages/ata/dist/src/index.d.ts"), resolve(ataVendorDirectory, "index.d.ts")),
+ cp(resolve(websiteDirectory, "LICENSE-CODE"), resolve(ataVendorDirectory, "LICENSE.txt")),
+ writeFile(
+ resolve(ataVendorDirectory, "package.json"),
+ `${JSON.stringify(
+ {
+ name: "@typescript/ata",
+ version: "0.9.8",
+ type: "module",
+ main: "./index.js",
+ types: "./index.d.ts",
+ },
+ undefined,
+ 2
+ )}\n`
+ ),
+])
+
+console.log(`Vendored ${examples.length} playground examples and ${helpIndex.docs.length} help topics`)
diff --git a/packages/playground-v2/scripts/vendor-typescript.mjs b/packages/playground-v2/scripts/vendor-typescript.mjs
new file mode 100644
index 000000000000..faa704617765
--- /dev/null
+++ b/packages/playground-v2/scripts/vendor-typescript.mjs
@@ -0,0 +1,97 @@
+import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"
+import { dirname, resolve } from "node:path"
+import { fileURLToPath } from "node:url"
+import { spawnSync } from "node:child_process"
+
+const packageDirectory = resolve(dirname(fileURLToPath(import.meta.url)), "..")
+const websiteDirectory = resolve(packageDirectory, "../..")
+const typescriptDirectory = resolve(
+ process.env.TYPESCRIPT_REPO || resolve(websiteDirectory, "../TypeScript"),
+)
+const vendorDirectory = resolve(packageDirectory, "vendor")
+const typescriptPackage = resolve(typescriptDirectory, "packages/typescript")
+const wasmPackage = resolve(typescriptDirectory, "packages/typescript-wasip1-wasm")
+const libDirectory = resolve(typescriptDirectory, "built/local")
+const packageNames = ["typescript", "typescript-wasip1-wasm"]
+const legalFiles = ["LICENSE.txt", "NOTICE.txt"]
+
+await Promise.all([
+ rm(resolve(vendorDirectory, "lib"), { force: true, recursive: true }),
+ rm(resolve(vendorDirectory, "typescript"), { force: true, recursive: true }),
+ rm(resolve(vendorDirectory, "typescript-wasip1-wasm"), { force: true, recursive: true }),
+ rm(resolve(vendorDirectory, "version.txt"), { force: true }),
+])
+await Promise.all(
+ packageNames.map(packageName => mkdir(resolve(vendorDirectory, packageName), { recursive: true })),
+)
+await Promise.all([
+ cp(resolve(typescriptPackage, "dist"), resolve(vendorDirectory, "typescript/dist"), {
+ recursive: true,
+ }),
+ cp(resolve(wasmPackage, "dist"), resolve(vendorDirectory, "typescript-wasip1-wasm/dist"), {
+ recursive: true,
+ }),
+ writeVendorManifest(typescriptPackage, "typescript"),
+ writeVendorManifest(wasmPackage, "typescript-wasip1-wasm"),
+ ...packageNames.flatMap(packageName =>
+ legalFiles.map(fileName =>
+ cp(
+ resolve(typescriptDirectory, fileName),
+ resolve(vendorDirectory, packageName, fileName),
+ )
+ )
+ ),
+])
+
+const vendorLibDirectory = resolve(vendorDirectory, "lib")
+await mkdir(vendorLibDirectory, { recursive: true })
+const libFileNames = (await readdir(libDirectory))
+ .filter(fileName => /^lib(?:\..+)?\.d\.ts$/.test(fileName))
+await Promise.all(
+ libFileNames.map(fileName =>
+ cp(resolve(libDirectory, fileName), resolve(vendorLibDirectory, fileName))
+ ),
+)
+
+const versionResult = spawnSync(
+ resolve(typescriptDirectory, "built/local/tsc"),
+ ["--version"],
+ { encoding: "utf8" },
+)
+if (versionResult.status !== 0) {
+ throw new Error(versionResult.stderr || "Unable to read the TypeScript version")
+}
+const version = versionResult.stdout.trim().replace(/^Version\s+/, "")
+await writeFile(resolve(vendorDirectory, "version.txt"), `${version}\n`)
+
+console.log(`Vendored TypeScript ${version} from ${typescriptDirectory}`)
+
+async function writeVendorManifest(sourceDirectory, vendorName) {
+ const manifest = JSON.parse(await readFile(resolve(sourceDirectory, "package.json"), "utf8"))
+ const exports = { ...manifest.exports }
+ const imports = { ...manifest.imports }
+ if (vendorName === "typescript") {
+ delete exports["."]
+ delete imports["#getExePath"]
+ delete imports["#vscode-jsonrpc/node"]
+ imports["#asyncClient"] = "./dist/api/async/browserClient.js"
+ imports["#syncClient"] = "./dist/api/sync/browserClient.js"
+ imports["#enums/*"] = {
+ types: "./dist/enums/*.enum.d.ts",
+ default: "./dist/enums/*.js",
+ }
+ }
+ const vendoredManifest = {
+ name: manifest.name,
+ version: manifest.version,
+ license: manifest.license,
+ type: manifest.type,
+ files: ["dist", ...legalFiles],
+ exports,
+ imports,
+ }
+ await writeFile(
+ resolve(vendorDirectory, vendorName, "package.json"),
+ `${JSON.stringify(vendoredManifest, undefined, 2)}\n`,
+ )
+}
diff --git a/packages/playground-v2/src/compiler-overrides.ts b/packages/playground-v2/src/compiler-overrides.ts
new file mode 100644
index 000000000000..953a8a8ca6c6
--- /dev/null
+++ b/packages/playground-v2/src/compiler-overrides.ts
@@ -0,0 +1,155 @@
+import { applyEdits, findNodeAtLocation, modify, parse, parseTree, type Node, type ParseError } from "jsonc-parser"
+
+export type CompilerOverride = {
+ applied: boolean
+ end: number
+ fileName: string
+ lineNumber: number
+ option: string
+ rawValue: string
+ start: number
+ tsconfigValue: unknown
+ value: unknown
+}
+
+export type OverrideDiagnostic = {
+ end: number
+ fileName: string
+ message: string
+ start: number
+}
+
+export type CompilerOverrideState = {
+ diagnostics: OverrideDiagnostic[]
+ effectiveConfigText: string
+ overrides: CompilerOverride[]
+}
+
+const reservedDirectives = new Set(["errors", "filename", "noerrors", "showemit", "showemittedfile", "showoutput"])
+const directivePattern = /^(\s*\/\/\s*)@([A-Za-z][\w-]*):\s*(.*?)\s*$/
+const formattingOptions = {
+ insertSpaces: true,
+ tabSize: 2,
+}
+
+export function computeCompilerOverrides(
+ files: ReadonlyMap,
+ configText: string
+): CompilerOverrideState {
+ const configErrors: ParseError[] = []
+ const config = parse(configText, configErrors, { allowTrailingComma: true, disallowComments: false }) ?? {}
+ const compilerOptions =
+ config.compilerOptions && typeof config.compilerOptions === "object" ? config.compilerOptions : {}
+ const overrides: CompilerOverride[] = []
+ const diagnostics: OverrideDiagnostic[] = []
+
+ for (const [fileName, text] of [...files].sort(([left], [right]) => left.localeCompare(right))) {
+ if (!/\.[cm]?[jt]sx?$/i.test(fileName)) continue
+ let lineNumber = 0
+ for (const lineMatch of text.matchAll(/[^\r\n]*(?:\r\n|\r|\n|$)/g)) {
+ if (lineMatch[0] === "") break
+ lineNumber++
+ const offset = lineMatch.index
+ const line = lineMatch[0].replace(/\r\n$|\r$|\n$/, "")
+ const match = directivePattern.exec(line)
+ const lineEnd = offset + line.length
+ if (match) {
+ const option = match[2]
+ if (!reservedDirectives.has(option.toLowerCase())) {
+ const rawValue = match[3]
+ if (rawValue === "") {
+ diagnostics.push({
+ end: lineEnd,
+ fileName,
+ message: `Compiler override @${option} requires a value.`,
+ start: offset,
+ })
+ } else {
+ overrides.push({
+ applied: false,
+ end: lineEnd,
+ fileName,
+ lineNumber,
+ option,
+ rawValue,
+ start: offset,
+ tsconfigValue: compilerOptions[option],
+ value: parseDirectiveValue(rawValue),
+ })
+ }
+ }
+ }
+ }
+ }
+
+ const byOption = new Map()
+ for (const override of overrides) {
+ const key = override.option.toLowerCase()
+ const entries = byOption.get(key) ?? []
+ entries.push(override)
+ byOption.set(key, entries)
+ }
+ let effectiveConfigText = configText
+ for (const duplicates of byOption.values()) {
+ if (duplicates.length > 1) {
+ for (const duplicate of duplicates) {
+ diagnostics.push({
+ end: duplicate.end,
+ fileName: duplicate.fileName,
+ message: `Compiler option @${duplicate.option} is overridden in multiple files. Keep only one project-wide directive.`,
+ start: duplicate.start,
+ })
+ }
+ continue
+ }
+
+ const override = duplicates[0]
+ try {
+ effectiveConfigText = setCompilerOption(effectiveConfigText, override.option, override.value)
+ override.applied = true
+ } catch {
+ diagnostics.push({
+ end: override.end,
+ fileName: override.fileName,
+ message: `Compiler override @${override.option} cannot be applied while tsconfig.json is invalid.`,
+ start: override.start,
+ })
+ }
+ }
+
+ return {
+ diagnostics,
+ effectiveConfigText,
+ overrides,
+ }
+}
+
+export function setCompilerOption(configText: string, option: string, value: unknown) {
+ return applyEdits(
+ configText,
+ modify(configText, ["compilerOptions", option], value, {
+ formattingOptions,
+ })
+ )
+}
+
+export function configOptionNode(configText: string, option: string): Node | undefined {
+ const tree = parseTree(configText, [], { allowTrailingComma: true, disallowComments: false })
+ return tree ? findNodeAtLocation(tree, ["compilerOptions", option]) : undefined
+}
+
+export function compilerOptionsNode(configText: string): Node | undefined {
+ const tree = parseTree(configText, [], { allowTrailingComma: true, disallowComments: false })
+ return tree ? findNodeAtLocation(tree, ["compilerOptions"]) : undefined
+}
+
+function parseDirectiveValue(rawValue: string) {
+ if (/^(?:true|false|null|-?\d+(?:\.\d+)?)$/.test(rawValue) || /^["[{]/.test(rawValue)) {
+ try {
+ return JSON.parse(rawValue)
+ } catch {
+ // Treat invalid JSON-like values as compiler option strings so the selected compiler reports them.
+ }
+ }
+ return rawValue
+}
diff --git a/packages/playground-v2/src/config-schema.ts b/packages/playground-v2/src/config-schema.ts
new file mode 100644
index 000000000000..2614d27c2487
--- /dev/null
+++ b/packages/playground-v2/src/config-schema.ts
@@ -0,0 +1,209 @@
+import {
+ CompletionItemKind,
+ DiagnosticSeverity,
+ getLanguageService,
+ InsertTextFormat,
+ MarkupKind,
+ type CompletionItem,
+ type Diagnostic,
+ type Hover,
+ type JSONSchema,
+ type MarkedString,
+ type MarkupContent,
+ type Range,
+} from "vscode-json-languageservice"
+import { TextDocument } from "vscode-languageserver-textdocument"
+import { monaco } from "./tsgo-lsp"
+
+const schemaUri = "https://json.schemastore.org/tsconfig"
+const markerOwner = "tsconfig-schema"
+
+export function registerConfigSchema(schema: JSONSchema) {
+ const service = getLanguageService({})
+ service.configure({
+ allowComments: true,
+ schemas: [
+ {
+ fileMatch: ["**/tsconfig.json", "**/jsconfig.json"],
+ schema,
+ uri: schemaUri,
+ },
+ ],
+ validate: true,
+ })
+
+ monaco.languages.registerCompletionItemProvider("json", {
+ triggerCharacters: ['"', ":"],
+ async provideCompletionItems(model, position) {
+ if (!isConfigModel(model)) return { suggestions: [] }
+ const document = createDocument(model)
+ const jsonDocument = service.parseJSONDocument(document)
+ const completions = await service.doComplete(document, toLspPosition(position), jsonDocument)
+ return {
+ incomplete: completions?.isIncomplete,
+ suggestions: (completions?.items ?? []).map(item => toMonacoCompletion(model, position, item)),
+ }
+ },
+ })
+
+ monaco.languages.registerHoverProvider("json", {
+ async provideHover(model, position) {
+ if (!isConfigModel(model)) return undefined
+ const document = createDocument(model)
+ const hover = await service.doHover(document, toLspPosition(position), service.parseJSONDocument(document))
+ return hover ? toMonacoHover(hover) : undefined
+ },
+ })
+
+ const timers = new Map()
+ const registerModel = (model: monaco.editor.ITextModel) => {
+ if (!isConfigModel(model)) return
+ const validate = () => {
+ window.clearTimeout(timers.get(model.uri.toString()))
+ timers.set(
+ model.uri.toString(),
+ window.setTimeout(async () => {
+ if (model.isDisposed()) return
+ const document = createDocument(model)
+ const diagnostics = (
+ await service.doValidation(document, service.parseJSONDocument(document), {
+ comments: "ignore",
+ trailingCommas: "ignore",
+ })
+ ).filter(diagnostic => !isCaseInsensitiveEnumMatch(document, diagnostic))
+ monaco.editor.setModelMarkers(model, markerOwner, diagnostics.map(toMonacoDiagnostic))
+ }, 150)
+ )
+ }
+ model.onDidChangeContent(validate)
+ validate()
+ }
+
+ monaco.editor.getModels().forEach(registerModel)
+ monaco.editor.onDidCreateModel(registerModel)
+}
+
+function isCaseInsensitiveEnumMatch(document: TextDocument, diagnostic: Diagnostic) {
+ const message = typeof diagnostic.message === "string" ? diagnostic.message : diagnostic.message.value
+ if (!message.startsWith("Value is not accepted. Valid values:")) return false
+ const source = document.getText(diagnostic.range)
+ let value: unknown
+ try {
+ value = JSON.parse(source)
+ } catch {
+ return false
+ }
+ if (typeof value !== "string") return false
+ const allowed = [...message.matchAll(/"([^"]+)"/g)].map(match => match[1])
+ return allowed.some(candidate => candidate.toLowerCase() === value.toLowerCase())
+}
+
+function isConfigModel(model: monaco.editor.ITextModel) {
+ return /\/(?:js|ts)config\.json$/i.test(model.uri.path)
+}
+
+function createDocument(model: monaco.editor.ITextModel) {
+ return TextDocument.create(model.uri.toString(), "json", model.getVersionId(), model.getValue())
+}
+
+function toMonacoCompletion(
+ model: monaco.editor.ITextModel,
+ position: monaco.Position,
+ item: CompletionItem
+): monaco.languages.CompletionItem {
+ const word = model.getWordUntilPosition(position)
+ const fallbackRange = new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn)
+ const textEdit = item.textEdit && "range" in item.textEdit ? item.textEdit : undefined
+ return {
+ detail: item.detail,
+ documentation: toMarkdown(item.documentation),
+ filterText: item.filterText,
+ insertText: textEdit?.newText ?? item.insertText ?? item.label,
+ insertTextRules:
+ item.insertTextFormat === InsertTextFormat.Snippet
+ ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
+ : undefined,
+ kind: toMonacoCompletionKind(item.kind),
+ label: item.label,
+ range: textEdit ? toMonacoRange(textEdit.range) : fallbackRange,
+ sortText: item.sortText,
+ }
+}
+
+function toMonacoCompletionKind(kind: CompletionItemKind | undefined) {
+ switch (kind) {
+ case CompletionItemKind.Value:
+ return monaco.languages.CompletionItemKind.Value
+ case CompletionItemKind.Enum:
+ case CompletionItemKind.EnumMember:
+ return monaco.languages.CompletionItemKind.Enum
+ case CompletionItemKind.Keyword:
+ return monaco.languages.CompletionItemKind.Keyword
+ case CompletionItemKind.Property:
+ case CompletionItemKind.Field:
+ return monaco.languages.CompletionItemKind.Property
+ default:
+ return monaco.languages.CompletionItemKind.Text
+ }
+}
+
+function toMonacoHover(hover: Hover): monaco.languages.Hover {
+ return {
+ contents: Array.isArray(hover.contents)
+ ? hover.contents.map(toMarkdown).filter(isMarkdown)
+ : [toMarkdown(hover.contents)].filter(isMarkdown),
+ range: hover.range ? toMonacoRange(hover.range) : undefined,
+ }
+}
+
+function toMarkdown(value: string | MarkedString | MarkupContent | undefined): monaco.IMarkdownString | undefined {
+ if (value === undefined) return undefined
+ if (typeof value === "string") return { value }
+ if ("language" in value) {
+ return { value: `\`\`\`${value.language}\n${value.value}\n\`\`\`` }
+ }
+ return {
+ value: value.kind === MarkupKind.Markdown ? value.value : value.value.replace(/[\\`*_{}[\]()#+\-.!]/g, "\\$&"),
+ }
+}
+
+function isMarkdown(value: monaco.IMarkdownString | undefined): value is monaco.IMarkdownString {
+ return value !== undefined
+}
+
+function toMonacoDiagnostic(diagnostic: Diagnostic): monaco.editor.IMarkerData {
+ return {
+ code: diagnostic.code?.toString(),
+ endColumn: diagnostic.range.end.character + 1,
+ endLineNumber: diagnostic.range.end.line + 1,
+ message: typeof diagnostic.message === "string" ? diagnostic.message : diagnostic.message.value,
+ severity: toMonacoSeverity(diagnostic.severity),
+ source: diagnostic.source ?? "TSConfig",
+ startColumn: diagnostic.range.start.character + 1,
+ startLineNumber: diagnostic.range.start.line + 1,
+ }
+}
+
+function toMonacoSeverity(severity: DiagnosticSeverity | undefined) {
+ switch (severity) {
+ case DiagnosticSeverity.Error:
+ return monaco.MarkerSeverity.Error
+ case DiagnosticSeverity.Warning:
+ return monaco.MarkerSeverity.Warning
+ case DiagnosticSeverity.Information:
+ return monaco.MarkerSeverity.Info
+ default:
+ return monaco.MarkerSeverity.Hint
+ }
+}
+
+function toLspPosition(position: monaco.Position) {
+ return {
+ character: position.column - 1,
+ line: position.lineNumber - 1,
+ }
+}
+
+function toMonacoRange(range: Range) {
+ return new monaco.Range(range.start.line + 1, range.start.character + 1, range.end.line + 1, range.end.character + 1)
+}
diff --git a/packages/playground-v2/src/global.d.ts b/packages/playground-v2/src/global.d.ts
new file mode 100644
index 000000000000..a06f797e5c12
--- /dev/null
+++ b/packages/playground-v2/src/global.d.ts
@@ -0,0 +1,16 @@
+declare module "*.css"
+declare module "*.json" {
+ const value: any
+ export default value
+}
+declare module "monaco-editor/editor/contrib/links/browser/links.js"
+
+declare module "monaco-editor/languages/definitions/javascript/javascript.js" {
+ export const conf: import("monaco-editor-core").languages.LanguageConfiguration
+ export const language: import("monaco-editor-core").languages.IMonarchLanguage
+}
+
+declare module "monaco-editor/languages/definitions/typescript/typescript.js" {
+ export const conf: import("monaco-editor-core").languages.LanguageConfiguration
+ export const language: import("monaco-editor-core").languages.IMonarchLanguage
+}
diff --git a/packages/playground-v2/src/index.html b/packages/playground-v2/src/index.html
new file mode 100644
index 000000000000..75943342ab04
--- /dev/null
+++ b/packages/playground-v2/src/index.html
@@ -0,0 +1,340 @@
+
+
+
+
+
+
+
+
+
+
+ TypeScript Playground v2
+
+
+
+
+
+
+
+
+
+
Project
+ /workspace
+
+
+ Reset
+ + File
+
+
+
+
+
+
+
+
+
+ ←
+
+ →
+
+
+
index.ts
+
+
+ Type query: align ^? below an expression
+ Apply overrides
+ Hide files
+ Hide output
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Downloading TypeScript...
+
+
Preparing downloads
+
+
+
+
+
+
+
+
+
diff --git a/packages/playground-v2/src/main.ts b/packages/playground-v2/src/main.ts
new file mode 100644
index 000000000000..38ed60f3b1dc
--- /dev/null
+++ b/packages/playground-v2/src/main.ts
@@ -0,0 +1,3148 @@
+import { API, DiagnosticCategory, type Diagnostic } from "@typescript/typescript/unstable/sync"
+import { instantiateWasm, WasmTransport } from "@typescript/typescript-wasip1-wasm"
+import "monaco-editor/editor/contrib/links/browser/links.js"
+import LZString from "lz-string"
+import "reflect-metadata"
+import examplesCatalog from "../vendor/examples.json"
+import {
+ compilerOptionsNode,
+ computeCompilerOverrides,
+ configOptionNode,
+ setCompilerOption,
+ type CompilerOverride,
+ type CompilerOverrideState,
+} from "./compiler-overrides"
+import { registerConfigSchema } from "./config-schema"
+import { StradaBackend } from "./strada"
+import { createTypeAcquisition, hasPackageImports } from "./type-acquisition"
+import { monaco, registerPlaygroundLanguages, startTsgoLsp, type TsgoLspController, type TsgoStatus } from "./tsgo-lsp"
+import "./styles.css"
+
+declare const __TS_VERSION__: string
+declare const __ASSET_CACHE_VERSION__: string
+declare const __LOAD_ASSET_SIZES__: {
+ libraries: number
+ schema: number
+ wasm: number
+}
+
+function remapCompilerOverrideDiagnostics(diagnostics: readonly Diagnostic[]) {
+ return diagnostics.map(diagnostic => {
+ const text = diagnostic.text.toLowerCase()
+ const matchingOverride = compilerOverrideState.overrides.find(override => {
+ if (!override.applied) return false
+ if (
+ text.includes(`'${override.option.toLowerCase()}'`) ||
+ text.includes(`"--${override.option.toLowerCase()}"`) ||
+ text.includes(`'--${override.option.toLowerCase()}'`)
+ ) {
+ return true
+ }
+ if (diagnostic.fileName !== configFileName) return false
+ const node = configOptionNode(compilerOverrideState.effectiveConfigText, override.option)
+ return node !== undefined && diagnostic.pos <= node.offset + node.length && diagnostic.end >= node.offset
+ })
+ if (!matchingOverride) return diagnostic
+ return {
+ ...diagnostic,
+ end: matchingOverride.end,
+ fileName: matchingOverride.fileName,
+ pos: matchingOverride.start,
+ }
+ })
+}
+
+type CompilerNode = {
+ forEachChild(visitor: (node: CompilerNode) => T): T | undefined
+ getEnd(): number
+ getFullStart(): number
+}
+
+type TypeQuery = {
+ lineNumber: number
+ column: number
+ label: string
+}
+
+type ProjectFile = {
+ path: string
+ language: "javascript" | "json" | "typescript"
+ text: string
+}
+
+type ProjectState = {
+ activeFile?: string
+ files: Record
+ selection?: {
+ positionColumn: number
+ positionLineNumber: number
+ selectionStartColumn: number
+ selectionStartLineNumber: number
+ }
+ useDefaults?: boolean
+}
+
+type VersionedProjectState = ProjectState & {
+ version: 2
+}
+
+type PlaygroundExample = {
+ code: string
+ compilerSettings?: Record
+ id: string
+ name: string
+ path: string[]
+ title: string
+}
+
+type PlaygroundExamples = {
+ examples: PlaygroundExample[]
+}
+
+type PlaygroundHelp = {
+ docs: Array<{ html: string; legacyIndexes?: number[]; title: string }>
+}
+
+type LayoutState = {
+ emitVisible: boolean
+ filesVisible: boolean
+ filesWidth: number
+ outputVisible: boolean
+ outputWidth: number
+ runVisible: boolean
+}
+
+type PlaygroundSettings = {
+ automaticTypeAcquisition: boolean
+ fontLigatures: boolean
+ fontSize: number
+ minimap: boolean
+ saveToUrl: boolean
+ tabSize: number
+ theme: "dark" | "light" | "system"
+ wordWrap: boolean
+}
+
+type RuntimeLog = {
+ level: "debug" | "error" | "info" | "log" | "warn"
+ text: string
+}
+
+declare global {
+ interface Window {
+ ts: any
+ }
+}
+
+;(
+ self as typeof self & {
+ MonacoEnvironment: { getWorker(): Worker }
+ }
+).MonacoEnvironment = {
+ getWorker() {
+ return new Worker(new URL("./editor.worker.js", import.meta.url), { type: "module" })
+ },
+}
+
+const projectRoot = "/workspace"
+const configFileName = `${projectRoot}/tsconfig.json`
+const entryFileName = `${projectRoot}/src/index.ts`
+const storageKey = "ts7-playground-project"
+const projectHashPrefix = "#code/v2/"
+const legacyCodeHashPrefix = "#code/"
+const projectStateVersion = 2
+const bundledExamples = examplesCatalog as PlaygroundExamples
+const initialHash = location.hash
+const initialLegacyExample = legacyExampleFromHash(initialHash)
+if (initialLegacyExample?.compilerSettings?.ts && !new URLSearchParams(location.search).has("ts")) {
+ const url = new URL(location.href)
+ url.searchParams.set("ts", String(initialLegacyExample.compilerSettings.ts))
+ history.replaceState({}, "", url)
+}
+const selectedCompiler = new URLSearchParams(location.search).get("ts")
+const useNativeCompiler = isNativeCompilerVersion(selectedCompiler)
+const defaultFiles: ProjectFile[] = [
+ {
+ path: configFileName,
+ language: "json",
+ text: `{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "CommonJS",
+ "strict": true,
+ "declaration": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true
+ },
+ "include": ["./src/**/*"]
+}
+`,
+ },
+ {
+ path: entryFileName,
+ language: "typescript",
+ text: `import { greet } from "./greet"
+
+const message = greet("TypeScript Playground v2")
+// ^?
+
+console.log(message)
+`,
+ },
+ {
+ path: `${projectRoot}/src/greet.ts`,
+ language: "typescript",
+ text: `export function greet(name: string) {
+ return \`Hello, \${name}!\`
+}
+`,
+ },
+]
+
+const inputElement = getElement("input-editor")
+const workspace = document.querySelector(".workspace")!
+const fileExplorer = getElement("file-explorer")
+const fileList = getElement("file-list")
+const fileResizer = getElement("file-resizer")
+const outputResizer = getElement("output-resizer")
+const compilerVersion = getElement("compiler-version")
+const compilerVersionButton = getElement("compiler-version-button")
+const compilerVersionLabel = getElement("compiler-version-label")
+const compilerVersionMenu = getElement("compiler-version-menu")
+const newFileButton = getElement("new-file-button")
+const resetProjectButton = getElement("reset-project-button")
+const applyCompilerOverridesButton = getElement("apply-compiler-overrides-button")
+const toggleFilesButton = getElement("toggle-files-button")
+const toggleOutputButton = getElement("toggle-output-button")
+const toggleEmitButton = getElement("toggle-emit-button")
+const toggleRunOutputButton = getElement("toggle-run-output-button")
+const navigateBackButton = getElement("navigate-back-button")
+const navigateForwardButton = getElement("navigate-forward-button")
+const currentFile = getElement("current-file")
+const editorHint = getElement("editor-hint")
+const emitOutput = getElement("emit-output")
+const emitSummary = getElement("emit-summary")
+const diagnosticsPanel = getElement("diagnostics-panel")
+const diagnosticsSummary = getElement("diagnostics-summary")
+const diagnosticsList = getElement("diagnostics-list")
+const runButton = getElement("run-button")
+const examplesButton = getElement("examples-button")
+const helpButton = getElement("help-button")
+const settingsButton = getElement("settings-button")
+const settingsDialog = getElement("settings-dialog")
+const settingsForm = getElement("settings-form")
+const settingsCancelButton = getElement("settings-cancel-button")
+const settingsResetButton = getElement("settings-reset-button")
+const settingAta = getElement("setting-ata")
+const settingSaveUrl = getElement("setting-save-url")
+const settingFontSize = getElement("setting-font-size")
+const settingTabSize = getElement("setting-tab-size")
+const settingTheme = getElement("setting-theme")
+const settingWordWrap = getElement("setting-word-wrap")
+const settingMinimap = getElement("setting-minimap")
+const settingLigatures = getElement("setting-ligatures")
+const confirmationDialog = getElement("confirmation-dialog")
+const confirmationForm = getElement("confirmation-form")
+const confirmationTitle = getElement("confirmation-title")
+const confirmationMessage = getElement("confirmation-message")
+const confirmationCancelButton = getElement("confirmation-cancel-button")
+const confirmationSubmitButton = getElement("confirmation-submit-button")
+const textInputDialog = getElement("text-input-dialog")
+const textInputForm = getElement("text-input-form")
+const textInputTitle = getElement("text-input-title")
+const textInputLabel = getElement("text-input-label")
+const textInputValue = getElement("text-input-value")
+const textInputError = getElement("text-input-error")
+const textInputCancelButton = getElement("text-input-cancel-button")
+const textInputSubmitButton = getElement("text-input-submit-button")
+const resourcesDialog = getElement("resources-dialog")
+const resourcesTitle = getElement("resources-title")
+const resourcesCloseButton = getElement("resources-close-button")
+const newFileDialog = getElement("new-file-dialog")
+const newFileForm = getElement("new-file-form")
+const newFilePath = getElement("new-file-path")
+const newFileError = getElement("new-file-error")
+const newFileCancelButton = getElement("new-file-cancel-button")
+const examplesView = getElement("examples-view")
+const examplesSearch = getElement("examples-search")
+const examplesList = getElement("examples-list")
+const helpView = getElement("help-view")
+const helpList = getElement("help-list")
+const helpDocument = getElement("help-document")
+const helpBackButton = getElement("help-back-button")
+const helpContent = getElement("help-content")
+const clearRunOutput = getElement("clear-run-output")
+const runOutput = getElement("run-output")
+const runLog = getElement("run-log")
+const status = getElement("status")
+const loader = getElement("loader")
+const loadingAnimation = getElement("loading-animation")
+const loadingCopy = getElement("loading-copy")
+const loadingMessage = getElement("loading-message")
+const loadingProgress = getElement("loading-progress")
+const loadingDetail = getElement("loading-detail")
+const downloadConsent = getElement("download-consent")
+const downloadSize = getElement("download-size")
+const rememberDownloadConsent = getElement("remember-download-consent")
+const confirmDownloadButton = getElement("confirm-download-button")
+
+let compilerReady = false
+let lspReady = false
+let lspStatus: TsgoStatus = "mounting files"
+let lspServerInfo: string | undefined
+let diagnosticCount = 0
+let compilerFailure: string | undefined
+let lspFailure: string | undefined
+let projectFailure: string | undefined
+let compilerTransport: WasmTransport | undefined
+let languageServer: TsgoLspController | undefined
+let stradaBackend: StradaBackend | undefined
+let stradaCompilerNamespace: typeof import("typescript") | undefined
+let compileActiveProject: (() => Promise | void) | undefined
+let emittedFiles = new Map()
+let emitRenderVersion = 0
+let hasShownDiagnostics = false
+let typeAcquisitionFailure: string | undefined
+let typeAcquisitionCompilerPromise: Promise | undefined
+let acquireTypes: ((source: string) => Promise) | undefined
+let typeAcquisitionQueue = Promise.resolve()
+let typeAcquisitionTimer = 0
+const acquiredTypeFiles = new Map()
+const pendingAcquiredTypeModels = new Set()
+let examplesPromise: Promise | undefined
+let helpPromise: Promise | undefined
+let confirmationResolver: ((value: boolean) => void) | undefined
+let textInputResolver: ((value: string | null) => void) | undefined
+const downloadedAssets = new Map()
+const cachedAssets = new Map()
+const assetCachePrefix = "ts7-playground-assets-"
+let assetCachePromise: Promise | undefined
+const layoutStorageKey = "ts7-playground-layout"
+const downloadConsentStorageKey = "ts7-playground-skip-download-warning"
+const settingsStorageKey = "ts7-playground-settings"
+let compilerOverrideState: CompilerOverrideState
+let playgroundSettings = loadPlaygroundSettings()
+
+const darkMode =
+ playgroundSettings.theme === "dark" ||
+ (playgroundSettings.theme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
+monaco.editor.defineTheme("typescript-playground", {
+ base: darkMode ? "vs-dark" : "vs",
+ inherit: true,
+ rules: [
+ { token: "comment", foreground: darkMode ? "7caf3d" : "6c6f2d" },
+ { token: "keyword", foreground: darkMode ? "7fc7ff" : "2b45c7" },
+ { token: "type", foreground: darkMode ? "4ec9b0" : "1142af" },
+ { token: "class", foreground: darkMode ? "4ec9b0" : "267f99" },
+ { token: "enum", foreground: darkMode ? "4ec9b0" : "267f99" },
+ { token: "interface", foreground: darkMode ? "4ec9b0" : "267f99" },
+ { token: "function", foreground: darkMode ? "dcdcaa" : "795e26" },
+ { token: "method", foreground: darkMode ? "dcdcaa" : "795e26" },
+ { token: "parameter", foreground: darkMode ? "9cdcfe" : "001080" },
+ { token: "property", foreground: darkMode ? "9cdcfe" : "001080" },
+ { token: "variable", foreground: darkMode ? "9cdcfe" : "001080" },
+ ],
+ colors: {
+ "editor.background": darkMode ? "#1e1e1e" : "#fafafa",
+ "editor.inactiveSelectionBackground": darkMode ? "#3a3d41" : "#e8e8e8",
+ "editor.selectionBackground": darkMode ? "#264f78" : "#dbeafe",
+ "editorInlayHint.background": darkMode ? "#333333" : "#eeeeee",
+ "editorInlayHint.foreground": darkMode ? "#d4d4d4" : "#333333",
+ },
+})
+
+registerPlaygroundLanguages()
+registerExampleLinks()
+const initialState = loadProjectState()
+const initialFiles =
+ initialState.useDefaults === false
+ ? { ...initialState.files }
+ : {
+ ...Object.fromEntries(defaultFiles.map(file => [file.path, file.text])),
+ ...initialState.files,
+ }
+applyLegacyCompilerOptions(initialFiles)
+const projectModels = new Map(
+ Object.entries(initialFiles).map(([fileName, text]) => {
+ const model = monaco.editor.createModel(text, languageForFile(fileName), monaco.Uri.parse(`file://${fileName}`))
+ return [fileName, model] as const
+ })
+)
+compilerOverrideState = computeCompilerOverrides(
+ projectTextMap(),
+ projectModels.get(configFileName)?.getValue() ?? "{}"
+)
+const fileButtons = new Map()
+const inputEditor = monaco.editor.create(inputElement, {
+ automaticLayout: true,
+ fontFamily: "Hack, monospace",
+ fontLigatures: playgroundSettings.fontLigatures,
+ fontSize: playgroundSettings.fontSize,
+ inlayHints: { enabled: "on" },
+ minimap: { enabled: playgroundSettings.minimap },
+ model: projectModels.get(initialState.activeFile ?? entryFileName) ?? projectModels.get(entryFileName),
+ padding: { top: 10 },
+ scrollBeyondLastLine: false,
+ "semanticHighlighting.enabled": true,
+ tabSize: playgroundSettings.tabSize,
+ theme: "typescript-playground",
+})
+let effectiveConfigModel: monaco.editor.ITextModel | undefined
+
+type EditorLocation = {
+ selection: monaco.Selection
+ uri: string
+}
+
+const backLocations: EditorLocation[] = []
+const forwardLocations: EditorLocation[] = []
+let trackedEditorLocation: EditorLocation | undefined
+let applyingEditorNavigation = false
+const mobileLayout = matchMedia("(max-width: 700px), (max-width: 900px) and (max-height: 600px)")
+const layoutState = loadLayoutState()
+applyLayoutState()
+setupWorkspaceResizer(fileResizer, "files")
+setupWorkspaceResizer(outputResizer, "output")
+window.addEventListener("resize", applyLayoutState)
+updateResponsiveEditorOptions()
+mobileLayout.addEventListener("change", updateResponsiveEditorOptions)
+
+renderFileList()
+restoreProjectSelection(initialState)
+restoreLegacySelection()
+updateActiveFile()
+trackedEditorLocation = getEditorLocation()
+updateNavigationButtons()
+inputEditor.onDidChangeModel(() => {
+ const nextLocation = getEditorLocation()
+ if (
+ !applyingEditorNavigation &&
+ trackedEditorLocation &&
+ nextLocation &&
+ trackedEditorLocation.uri !== nextLocation.uri
+ ) {
+ pushEditorLocation(backLocations, trackedEditorLocation)
+ forwardLocations.length = 0
+ }
+ trackedEditorLocation = nextLocation
+ updateActiveFile()
+ updateNavigationButtons()
+})
+inputEditor.onDidChangeCursorSelection(() => {
+ trackedEditorLocation = getEditorLocation()
+ window.clearTimeout(selectionPersistTimer)
+ selectionPersistTimer = window.setTimeout(persistProjectState, 150)
+})
+inputEditor.addAction({
+ id: "run-project",
+ label: "Run Project",
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
+ run: runProject,
+})
+inputEditor.onMouseDown(event => {
+ if (!event.target.position || (!event.event.ctrlKey && !event.event.metaKey)) {
+ return
+ }
+ const position = event.target.position
+ const example = exampleAtPosition(inputEditor.getModel(), position)
+ if (example) {
+ navigateToExample(example)
+ return
+ }
+ window.setTimeout(() => {
+ inputEditor.setPosition(position)
+ if (stradaBackend) {
+ const model = inputEditor.getModel()
+ if (model) void stradaBackend.goToDefinition(model, position)
+ } else {
+ void inputEditor.getAction("editor.action.revealDefinition")?.run()
+ }
+ })
+})
+
+const inlayEmitter = new monaco.Emitter()
+const typeQueries = new Map()
+const compilerOverrideHints = new Map()
+const overrideCodeLensEmitter = new monaco.Emitter()
+let overrideCodeLensProvider: monaco.languages.CodeLensProvider | undefined
+registerCompilerOverrideFeatures()
+refreshCompilerOverrides()
+
+function loadLayoutState(): LayoutState {
+ const defaults: LayoutState = {
+ emitVisible: true,
+ filesVisible: true,
+ filesWidth: 208,
+ outputVisible: true,
+ outputWidth: 440,
+ runVisible: true,
+ }
+ try {
+ const stored = JSON.parse(localStorage.getItem(layoutStorageKey) ?? "{}")
+ return {
+ emitVisible: typeof stored.emitVisible === "boolean" ? stored.emitVisible : defaults.emitVisible,
+ filesVisible: typeof stored.filesVisible === "boolean" ? stored.filesVisible : defaults.filesVisible,
+ filesWidth: typeof stored.filesWidth === "number" ? stored.filesWidth : defaults.filesWidth,
+ outputVisible: typeof stored.outputVisible === "boolean" ? stored.outputVisible : defaults.outputVisible,
+ outputWidth: typeof stored.outputWidth === "number" ? stored.outputWidth : defaults.outputWidth,
+ runVisible: typeof stored.runVisible === "boolean" ? stored.runVisible : defaults.runVisible,
+ }
+ } catch {
+ return defaults
+ }
+}
+
+function defaultPlaygroundSettings(): PlaygroundSettings {
+ return {
+ automaticTypeAcquisition: true,
+ fontLigatures: true,
+ fontSize: 14,
+ minimap: false,
+ saveToUrl: true,
+ tabSize: 2,
+ theme: "system",
+ wordWrap: false,
+ }
+}
+
+function loadPlaygroundSettings() {
+ const defaults = defaultPlaygroundSettings()
+ try {
+ const stored = JSON.parse(localStorage.getItem(settingsStorageKey) ?? "{}")
+ return {
+ automaticTypeAcquisition:
+ typeof stored.automaticTypeAcquisition === "boolean"
+ ? stored.automaticTypeAcquisition
+ : defaults.automaticTypeAcquisition,
+ fontLigatures: typeof stored.fontLigatures === "boolean" ? stored.fontLigatures : defaults.fontLigatures,
+ fontSize: [12, 14, 16, 18, 20].includes(stored.fontSize) ? stored.fontSize : defaults.fontSize,
+ minimap: typeof stored.minimap === "boolean" ? stored.minimap : defaults.minimap,
+ saveToUrl: typeof stored.saveToUrl === "boolean" ? stored.saveToUrl : defaults.saveToUrl,
+ tabSize: stored.tabSize === 4 ? 4 : defaults.tabSize,
+ theme: stored.theme === "light" || stored.theme === "dark" ? stored.theme : defaults.theme,
+ wordWrap: typeof stored.wordWrap === "boolean" ? stored.wordWrap : defaults.wordWrap,
+ } satisfies PlaygroundSettings
+ } catch {
+ return defaults
+ }
+}
+
+function openSettings() {
+ populateSettingsForm(playgroundSettings)
+ settingsDialog.showModal()
+ settingAta.focus()
+}
+
+function populateSettingsForm(settings: PlaygroundSettings) {
+ settingAta.checked = settings.automaticTypeAcquisition
+ settingSaveUrl.checked = settings.saveToUrl
+ settingFontSize.value = String(settings.fontSize)
+ settingTabSize.value = String(settings.tabSize)
+ settingTheme.value = settings.theme
+ settingWordWrap.checked = settings.wordWrap
+ settingMinimap.checked = settings.minimap
+ settingLigatures.checked = settings.fontLigatures
+}
+
+function saveSettings() {
+ const next: PlaygroundSettings = {
+ automaticTypeAcquisition: settingAta.checked,
+ fontLigatures: settingLigatures.checked,
+ fontSize: Number(settingFontSize.value),
+ minimap: settingMinimap.checked,
+ saveToUrl: settingSaveUrl.checked,
+ tabSize: Number(settingTabSize.value),
+ theme: settingTheme.value === "light" || settingTheme.value === "dark" ? settingTheme.value : "system",
+ wordWrap: settingWordWrap.checked,
+ }
+ const ataChanged = next.automaticTypeAcquisition !== playgroundSettings.automaticTypeAcquisition
+ const themeChanged = next.theme !== playgroundSettings.theme
+ playgroundSettings = next
+ try {
+ localStorage.setItem(settingsStorageKey, JSON.stringify(playgroundSettings))
+ } catch (error) {
+ console.warn("Could not save playground settings", error)
+ }
+ settingsDialog.close()
+ applyEditorSettings()
+ if (playgroundSettings.saveToUrl) persistProjectState()
+ if (ataChanged || themeChanged) {
+ if (playgroundSettings.saveToUrl) {
+ location.reload()
+ } else {
+ const url = new URL(location.href)
+ url.hash = ""
+ location.replace(url)
+ }
+ }
+}
+
+function applyEditorSettings() {
+ inputEditor.updateOptions({
+ fontLigatures: playgroundSettings.fontLigatures,
+ fontSize: playgroundSettings.fontSize,
+ minimap: { enabled: playgroundSettings.minimap },
+ tabSize: playgroundSettings.tabSize,
+ })
+ updateResponsiveEditorOptions()
+}
+
+function requestConfirmation(options: {
+ cancelLabel?: string | null
+ confirmLabel: string
+ danger?: boolean
+ message: string
+ title: string
+}) {
+ if (confirmationResolver) finishConfirmation(false)
+ confirmationTitle.textContent = options.title
+ confirmationMessage.textContent = options.message
+ confirmationCancelButton.hidden = options.cancelLabel === null
+ confirmationCancelButton.textContent = options.cancelLabel ?? "Cancel"
+ confirmationSubmitButton.textContent = options.confirmLabel
+ confirmationSubmitButton.classList.toggle("danger", options.danger === true)
+ confirmationDialog.showModal()
+ confirmationSubmitButton.focus()
+ return new Promise(resolve => {
+ confirmationResolver = resolve
+ })
+}
+
+function finishConfirmation(value: boolean) {
+ const resolve = confirmationResolver
+ confirmationResolver = undefined
+ if (confirmationDialog.open) confirmationDialog.close()
+ resolve?.(value)
+}
+
+function requestTextInput(options: { initialValue?: string; label: string; submitLabel: string; title: string }) {
+ if (textInputResolver) finishTextInput(null)
+ textInputTitle.textContent = options.title
+ textInputLabel.textContent = options.label
+ textInputValue.value = options.initialValue ?? ""
+ textInputError.hidden = true
+ textInputError.textContent = ""
+ textInputSubmitButton.textContent = options.submitLabel
+ textInputDialog.showModal()
+ textInputValue.focus()
+ textInputValue.select()
+ return new Promise(resolve => {
+ textInputResolver = resolve
+ })
+}
+
+function finishTextInput(value: string | null) {
+ const resolve = textInputResolver
+ textInputResolver = undefined
+ if (textInputDialog.open) textInputDialog.close()
+ resolve?.(value)
+}
+
+function updateResponsiveEditorOptions() {
+ const mobile = mobileLayout.matches
+ inputEditor.updateOptions({
+ folding: !mobile,
+ lineNumbersMinChars: mobile ? 3 : 5,
+ scrollbar: {
+ horizontal: mobile ? "hidden" : "auto",
+ horizontalScrollbarSize: mobile ? 0 : 12,
+ verticalScrollbarSize: mobile ? 8 : 14,
+ },
+ scrollBeyondLastColumn: mobile ? 0 : 5,
+ wordWrap: mobile || playgroundSettings.wordWrap ? "on" : "off",
+ wrappingIndent: "indent",
+ })
+ requestAnimationFrame(() => inputEditor.layout())
+}
+
+function applyLayoutState() {
+ if (!matchMedia("(max-width: 1000px)").matches) {
+ layoutState.filesWidth = clampPanelWidth("files", layoutState.filesWidth)
+ layoutState.outputWidth = clampPanelWidth("output", layoutState.outputWidth)
+ }
+ workspace.style.setProperty("--files-width", layoutState.filesVisible ? `${layoutState.filesWidth}px` : "0px")
+ workspace.style.setProperty("--output-width", layoutState.outputVisible ? `${layoutState.outputWidth}px` : "0px")
+ workspace.dataset.filesCollapsed = String(!layoutState.filesVisible)
+ workspace.dataset.outputCollapsed = String(!layoutState.outputVisible)
+ emitOutput.hidden = !layoutState.emitVisible
+ runOutput.dataset.collapsed = String(!layoutState.runVisible)
+
+ toggleFilesButton.textContent = layoutState.filesVisible ? "Hide files" : "Show files"
+ toggleFilesButton.setAttribute("aria-expanded", String(layoutState.filesVisible))
+ toggleOutputButton.textContent = layoutState.outputVisible ? "Hide output" : "Show output"
+ toggleOutputButton.setAttribute("aria-expanded", String(layoutState.outputVisible))
+ toggleEmitButton.textContent = layoutState.emitVisible ? "▾" : "▸"
+ toggleEmitButton.title = layoutState.emitVisible ? "Collapse Emit" : "Expand Emit"
+ toggleEmitButton.setAttribute("aria-label", toggleEmitButton.title)
+ toggleEmitButton.setAttribute("aria-expanded", String(layoutState.emitVisible))
+ toggleRunOutputButton.textContent = layoutState.runVisible ? "▾" : "▸"
+ toggleRunOutputButton.title = layoutState.runVisible ? "Collapse Run output" : "Expand Run output"
+ toggleRunOutputButton.setAttribute("aria-label", toggleRunOutputButton.title)
+ toggleRunOutputButton.setAttribute("aria-expanded", String(layoutState.runVisible))
+
+ fileResizer.setAttribute("aria-valuenow", String(Math.round(layoutState.filesWidth)))
+ outputResizer.setAttribute("aria-valuenow", String(Math.round(layoutState.outputWidth)))
+ requestAnimationFrame(() => inputEditor.layout())
+}
+
+function persistLayoutState() {
+ try {
+ localStorage.setItem(layoutStorageKey, JSON.stringify(layoutState))
+ } catch (error) {
+ console.warn("Could not save playground layout", error)
+ }
+}
+
+function setupWorkspaceResizer(element: HTMLElement, target: "files" | "output") {
+ element.addEventListener("pointerdown", event => {
+ if (matchMedia("(max-width: 1000px)").matches) return
+ event.preventDefault()
+ const startX = event.clientX
+ const startWidth = target === "files" ? layoutState.filesWidth : layoutState.outputWidth
+ element.setPointerCapture(event.pointerId)
+ document.body.dataset.resizing = target
+
+ const move = (moveEvent: PointerEvent) => {
+ const delta = moveEvent.clientX - startX
+ const width = target === "files" ? startWidth + delta : startWidth - delta
+ if (target === "files") layoutState.filesWidth = width
+ else layoutState.outputWidth = width
+ applyLayoutState()
+ }
+ const stop = () => {
+ element.removeEventListener("pointermove", move)
+ element.removeEventListener("pointerup", stop)
+ element.removeEventListener("pointercancel", stop)
+ delete document.body.dataset.resizing
+ persistLayoutState()
+ }
+ element.addEventListener("pointermove", move)
+ element.addEventListener("pointerup", stop)
+ element.addEventListener("pointercancel", stop)
+ })
+
+ element.addEventListener("keydown", event => {
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return
+ event.preventDefault()
+ const direction = event.key === "ArrowRight" ? 1 : -1
+ if (target === "files") layoutState.filesWidth += direction * 16
+ else layoutState.outputWidth -= direction * 16
+ applyLayoutState()
+ persistLayoutState()
+ })
+}
+
+function clampPanelWidth(target: "files" | "output", width: number) {
+ const workspaceWidth = workspace.clientWidth || innerWidth
+ if (target === "files") {
+ const maximum = Math.max(160, workspaceWidth - (layoutState.outputVisible ? layoutState.outputWidth : 0) - 420)
+ return Math.min(Math.max(width, 144), Math.min(360, maximum))
+ }
+ const maximum = Math.max(240, workspaceWidth - (layoutState.filesVisible ? layoutState.filesWidth : 0) - 480)
+ return Math.min(Math.max(width, 240), Math.min(640, maximum))
+}
+
+for (const language of ["javascript", "typescript"]) {
+ monaco.languages.registerInlayHintsProvider(language, {
+ onDidChangeInlayHints: inlayEmitter.event,
+ provideInlayHints(model) {
+ return {
+ hints: [
+ ...(typeQueries.get(model.uri.toString()) ?? []).map(query => ({
+ kind: monaco.languages.InlayHintKind.Type,
+ position: new monaco.Position(query.lineNumber, query.column),
+ label: query.label,
+ paddingLeft: true,
+ })),
+ ...(compilerOverrideHints.get(model.uri.toString()) ?? []),
+ ],
+ dispose() {},
+ }
+ },
+ })
+}
+monaco.languages.registerInlayHintsProvider("json", {
+ onDidChangeInlayHints: inlayEmitter.event,
+ provideInlayHints(model) {
+ return {
+ dispose() {},
+ hints: compilerOverrideHints.get(model.uri.toString()) ?? [],
+ }
+ },
+})
+
+let updateTimer = 0
+let selectionPersistTimer = 0
+for (const model of projectModels.values()) {
+ registerProjectModel(model)
+}
+newFileButton.addEventListener("click", createNewFile)
+newFileCancelButton.addEventListener("click", () => newFileDialog.close())
+newFileForm.addEventListener("submit", event => {
+ event.preventDefault()
+ finishCreatingFile()
+})
+resetProjectButton.addEventListener("click", () => void resetProject())
+applyCompilerOverridesButton.addEventListener("click", applyCompilerOverridesToConfig)
+toggleFilesButton.addEventListener("click", () => {
+ layoutState.filesVisible = !layoutState.filesVisible
+ applyLayoutState()
+ persistLayoutState()
+})
+toggleOutputButton.addEventListener("click", () => {
+ layoutState.outputVisible = !layoutState.outputVisible
+ applyLayoutState()
+ persistLayoutState()
+})
+toggleEmitButton.addEventListener("click", () => {
+ layoutState.emitVisible = !layoutState.emitVisible
+ applyLayoutState()
+ persistLayoutState()
+})
+toggleRunOutputButton.addEventListener("click", () => {
+ layoutState.runVisible = !layoutState.runVisible
+ applyLayoutState()
+ persistLayoutState()
+})
+navigateBackButton.addEventListener("click", navigateBack)
+navigateForwardButton.addEventListener("click", navigateForward)
+runButton.addEventListener("click", runProject)
+examplesButton.addEventListener("click", () => void openExamples())
+helpButton.addEventListener("click", () => void openHelp())
+settingsButton.addEventListener("click", openSettings)
+settingsCancelButton.addEventListener("click", () => settingsDialog.close())
+settingsResetButton.addEventListener("click", () => populateSettingsForm(defaultPlaygroundSettings()))
+settingsForm.addEventListener("submit", event => {
+ event.preventDefault()
+ saveSettings()
+})
+resourcesCloseButton.addEventListener("click", () => resourcesDialog.close())
+examplesSearch.addEventListener("input", () => void renderExamples())
+helpBackButton.addEventListener("click", showHelpTopics)
+clearRunOutput.addEventListener("click", () => renderRunLogs([]))
+confirmationCancelButton.addEventListener("click", () => finishConfirmation(false))
+confirmationForm.addEventListener("submit", event => {
+ event.preventDefault()
+ finishConfirmation(true)
+})
+confirmationDialog.addEventListener("cancel", event => {
+ event.preventDefault()
+ finishConfirmation(false)
+})
+textInputCancelButton.addEventListener("click", () => finishTextInput(null))
+textInputForm.addEventListener("submit", event => {
+ event.preventDefault()
+ const value = textInputValue.value.trim()
+ if (value === "") {
+ textInputError.textContent = "Enter a value."
+ textInputError.hidden = false
+ textInputValue.focus()
+ return
+ }
+ finishTextInput(value)
+})
+textInputDialog.addEventListener("cancel", event => {
+ event.preventDefault()
+ finishTextInput(null)
+})
+
+void openLegacyResourceRoute(initialHash)
+void initializeVersionSelector()
+void (useNativeCompiler ? initializeNativeCompiler() : initializeStradaCompiler(selectedCompiler!))
+
+async function initializeNativeCompiler() {
+ try {
+ await confirmLargeDownloadIfNeeded()
+ setLoadingProgress(0, "Downloading TypeScript...", "Preparing downloads")
+ const [wasmBytes, libFilesBytes, configSchemaBytes] = await Promise.all([
+ downloadAsset("wasm", new URL("./tsc.wasm", import.meta.url)),
+ downloadAsset("libraries", new URL("./lib-files.json", import.meta.url)),
+ downloadAsset("schema", new URL("./tsconfig.schema.json", import.meta.url)),
+ ])
+
+ setLoadingIndeterminate("Compiling TypeScript...", "")
+ const module = await WebAssembly.compile(wasmBytes)
+ setLoadingProgress(78, "Starting compiler API...", "Instantiating WebAssembly")
+ const libFiles = JSON.parse(new TextDecoder().decode(libFilesBytes)) as Record
+ const configSchema = JSON.parse(new TextDecoder().decode(configSchemaBytes))
+ registerConfigSchema(configSchema)
+ const instance = await instantiateWasm(module)
+ const transport = new WasmTransport({ instance, cwd: projectRoot })
+ compilerTransport = transport
+ const api = new API({ transport })
+ const libraries = Object.entries(libFiles)
+ for (const [index, [fileName, content]] of libraries.entries()) {
+ transport.setFile(fileName, content)
+ setLoadingProgress(
+ 82 + ((index + 1) / libraries.length) * 8,
+ "Mounting TypeScript libraries...",
+ `${index + 1} of ${libraries.length} files`
+ )
+ }
+ window.ts = Object.assign(api, {
+ API,
+ DiagnosticCategory,
+ version: __TS_VERSION__,
+ })
+ await refreshTypeAcquisition(true)
+ for (const [fileName, text] of acquiredTypeFiles) {
+ transport.setFile(fileName, text)
+ }
+ compileActiveProject = () => compileNativeProject(api)
+ compilerReady = true
+ startLanguageServer(module, libFiles)
+ compileNativeProject(api)
+ inputEditor.focus()
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error)
+ compilerFailure = message
+ renderStatus()
+ console.error(error)
+ }
+}
+
+async function initializeStradaCompiler(requestedVersion: string) {
+ try {
+ setLoadingIndeterminate("Loading TypeScript...", requestedVersion)
+ const version = await resolveStradaVersion(requestedVersion)
+ const compilerUrl = `https://playgroundcdn.typescriptlang.org/cdn/${version}/typescript/lib/typescript.js`
+ const compilerResponse = await fetch(compilerUrl)
+ if (!compilerResponse.ok) {
+ throw new Error(`Unable to load TypeScript ${version}: ${compilerResponse.status} ${compilerResponse.statusText}`)
+ }
+ const compilerSource = await compilerResponse.text()
+ const classicTS = new Function(`${compilerSource}\nreturn ts;`)()
+ stradaCompilerNamespace = classicTS
+ window.ts = classicTS
+ await refreshTypeAcquisition(true)
+ stradaBackend = await StradaBackend.create({
+ baseUrl: `https://playgroundcdn.typescriptlang.org/cdn/${version}/typescript/lib/`,
+ compilerSource,
+ editor: inputEditor,
+ files: compilerFileContents,
+ models: projectModels,
+ onNavigate: navigateToModel,
+ version,
+ })
+ compileActiveProject = compileStradaProject
+ compilerReady = true
+ lspReady = true
+ lspServerInfo = `TypeScript ${version}`
+ await compileStradaProject()
+ renderStatus()
+ inputEditor.focus()
+ } catch (error) {
+ compilerFailure = error instanceof Error ? error.message : String(error)
+ renderStatus()
+ console.error(error)
+ }
+}
+
+function startLanguageServer(module: WebAssembly.Module, libraries: Record) {
+ try {
+ languageServer = startTsgoLsp({
+ configFileName,
+ editor: inputEditor,
+ effectiveConfigText: compilerOverrideState.effectiveConfigText,
+ extraFiles: Object.fromEntries(acquiredTypeFiles),
+ libraries,
+ models: [...projectModels.values()],
+ module,
+ onError(message) {
+ lspFailure = message
+ renderStatus()
+ },
+ onNavigate: navigateToModel,
+ onStatus(nextStatus, serverInfo) {
+ lspStatus = nextStatus
+ lspReady = nextStatus === "ready"
+ if (lspReady) {
+ for (const fileName of pendingAcquiredTypeModels) {
+ const text = acquiredTypeFiles.get(fileName)
+ if (text !== undefined) mountAcquiredTypeModel(fileName, text)
+ }
+ pendingAcquiredTypeModels.clear()
+ }
+ lspServerInfo = serverInfo ?? lspServerInfo
+ const progress = {
+ "mounting files": 92,
+ "starting tsc.wasm": 95,
+ "initializing LSP": 98,
+ ready: 100,
+ }[nextStatus]
+ setLoadingProgress(
+ progress,
+ nextStatus === "ready" ? "TypeScript is ready" : `Starting language server: ${nextStatus}`,
+ nextStatus === "ready" ? lspServerInfo ?? __TS_VERSION__ : ""
+ )
+ renderStatus()
+ },
+ })
+ } catch (error) {
+ lspFailure = error instanceof Error ? error.message : String(error)
+ renderStatus()
+ }
+}
+
+async function initializeVersionSelector() {
+ compilerVersionButton.setAttribute("aria-disabled", "true")
+ try {
+ const response = await fetch(new URL("./versions.json", import.meta.url))
+ if (!response.ok) throw new Error(`Could not load versions: ${response.status}`)
+ const releases = (await response.json()) as { versions: string[] }
+ const unsupported = new Set(["3.1.6", "3.0.1", "2.8.1", "2.7.2", "2.4.1"])
+ const seenMinorVersions = new Set()
+ const versions: Array<{ label: string; value: string }> = [{ label: __TS_VERSION__, value: "native" }]
+ for (const version of releases.versions) {
+ if (unsupported.has(version)) continue
+ const minorVersion = version.split(".").slice(0, 2).join(".")
+ if (seenMinorVersions.has(minorVersion)) continue
+ seenMinorVersions.add(minorVersion)
+ versions.push({ label: version, value: version })
+ }
+ const selectedValue = useNativeCompiler ? "native" : normalizeRequestedVersion(selectedCompiler!)
+ if (!versions.some(version => version.value === selectedValue)) {
+ versions.push({ label: selectedCompiler!, value: selectedValue })
+ }
+ compilerVersionMenu.replaceChildren()
+ for (const version of versions) {
+ compilerVersionMenu.appendChild(createVersionMenuItem(version.label, version.value, selectedValue))
+ }
+ const separator = document.createElement("div")
+ separator.className = "version-menu-separator"
+ separator.setAttribute("role", "separator")
+ compilerVersionMenu.appendChild(separator)
+ compilerVersionMenu.appendChild(createVersionMenuItem("Custom / PR build…", "__custom__", selectedValue))
+
+ compilerVersionLabel.textContent =
+ versions.find(version => version.value === selectedValue)?.label ?? selectedCompiler ?? __TS_VERSION__
+ compilerVersion.addEventListener("toggle", () => {
+ compilerVersionButton.setAttribute("aria-expanded", String(compilerVersion.open))
+ if (compilerVersion.open) {
+ compilerVersionMenu.querySelector('[aria-checked="true"]')?.focus()
+ }
+ })
+ compilerVersionMenu.addEventListener("keydown", handleVersionMenuKeydown)
+ document.addEventListener("pointerdown", event => {
+ if (compilerVersion.open && !compilerVersion.contains(event.target as Node)) compilerVersion.open = false
+ })
+ } catch (error) {
+ console.warn("Could not initialize the compiler version selector", error)
+ } finally {
+ compilerVersionButton.removeAttribute("aria-disabled")
+ }
+}
+
+function createVersionMenuItem(label: string, value: string, selectedValue: string) {
+ const button = document.createElement("button")
+ button.type = "button"
+ button.className = "version-menu-item"
+ button.dataset.value = value
+ button.setAttribute("role", "menuitemradio")
+ button.setAttribute("aria-checked", String(value === selectedValue))
+ button.appendChild(createText("span", label))
+ button.addEventListener("click", () => void selectCompilerVersion(value))
+ return button
+}
+
+async function selectCompilerVersion(value: string) {
+ compilerVersion.open = false
+ const url = new URL(location.href)
+ if (value === "native") {
+ url.searchParams.delete("ts")
+ } else if (value === "__custom__") {
+ const custom = await requestTextInput({
+ label: "Playground CDN build ID",
+ submitLabel: "Load build",
+ title: "Custom TypeScript build",
+ })
+ if (!custom) {
+ compilerVersionButton.focus()
+ return
+ }
+ url.searchParams.set("ts", custom)
+ } else {
+ url.searchParams.set("ts", value)
+ }
+ location.href = url.href
+}
+
+function handleVersionMenuKeydown(event: KeyboardEvent) {
+ const items = [...compilerVersionMenu.querySelectorAll(".version-menu-item")]
+ const current = items.indexOf(document.activeElement as HTMLButtonElement)
+ let next = current
+ if (event.key === "ArrowDown") next = (current + 1) % items.length
+ else if (event.key === "ArrowUp") next = (current - 1 + items.length) % items.length
+ else if (event.key === "Home") next = 0
+ else if (event.key === "End") next = items.length - 1
+ else if (event.key === "Escape") {
+ compilerVersion.open = false
+ compilerVersionButton.focus()
+ event.preventDefault()
+ return
+ } else {
+ return
+ }
+ items[next]?.focus()
+ event.preventDefault()
+}
+
+function isNativeCompilerVersion(version: string | null) {
+ return (
+ version === null ||
+ version === "" ||
+ version === "native" ||
+ version === "7" ||
+ version === "7.1" ||
+ version === __TS_VERSION__
+ )
+}
+
+function normalizeRequestedVersion(version: string) {
+ return version === "Nightly" ? "next" : version
+}
+
+async function resolveStradaVersion(requestedVersion: string) {
+ const normalized = normalizeRequestedVersion(requestedVersion)
+ if (normalized !== "next" && normalized !== "latest") return normalized
+ const index = normalized === "next" ? "next.json" : "releases.json"
+ const response = await fetch(`https://playgroundcdn.typescriptlang.org/indexes/${index}`, { cache: "no-cache" })
+ if (!response.ok) {
+ throw new Error(`Could not resolve TypeScript ${requestedVersion}: ${response.status}`)
+ }
+ const result = await response.json()
+ return normalized === "next"
+ ? (result.version as string)
+ : [...(result.versions as string[])].sort(compareVersions).at(-1)!
+}
+
+function compareVersions(left: string, right: string) {
+ const leftParts = left.split(/[.-]/).map(part => Number(part) || 0)
+ const rightParts = right.split(/[.-]/).map(part => Number(part) || 0)
+ for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index++) {
+ const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0)
+ if (difference !== 0) return difference
+ }
+ return 0
+}
+
+async function openExamples() {
+ resourcesTitle.textContent = "Examples"
+ examplesView.hidden = false
+ helpView.hidden = true
+ resourcesDialog.showModal()
+ await renderExamples()
+ examplesSearch.focus()
+}
+
+async function renderExamples() {
+ try {
+ const examples = await getExamples()
+ const query = examplesSearch.value.trim().toLowerCase()
+ const filtered = examples.examples
+ .filter(example => {
+ const searchText = `${example.title} ${example.path.join(" ")}`.toLowerCase()
+ return query === "" || searchText.includes(query)
+ })
+ .sort(
+ (left, right) =>
+ left.path.join("/").localeCompare(right.path.join("/")) || left.title.localeCompare(right.title)
+ )
+ examplesList.replaceChildren()
+ if (filtered.length === 0) {
+ examplesList.appendChild(createText("p", "No matching examples.", "empty-message"))
+ return
+ }
+ for (const example of filtered) {
+ const button = document.createElement("button")
+ button.type = "button"
+ button.appendChild(createText("strong", example.title))
+ button.appendChild(createText("small", example.path.join(" / ")))
+ button.addEventListener("click", () => void loadExample(example))
+ examplesList.appendChild(button)
+ }
+ } catch (error) {
+ examplesList.replaceChildren(
+ createText("p", error instanceof Error ? error.message : String(error), "resource-error")
+ )
+ }
+}
+
+async function openHelp() {
+ resourcesTitle.textContent = "Playground help"
+ examplesView.hidden = true
+ helpView.hidden = false
+ resourcesDialog.showModal()
+ showHelpTopics()
+ try {
+ const help = await getHelp()
+ helpList.replaceChildren()
+ for (const topic of help.docs) {
+ const button = document.createElement("button")
+ button.type = "button"
+ button.appendChild(createText("strong", topic.title))
+ button.addEventListener("click", () => showHelpDocument(topic))
+ helpList.appendChild(button)
+ }
+ } catch (error) {
+ helpList.replaceChildren(createText("p", error instanceof Error ? error.message : String(error), "resource-error"))
+ }
+}
+
+async function openLegacyResourceRoute(hash: string) {
+ if (hash === "#show-examples") {
+ await openExamples()
+ return
+ }
+ const handbook = /^#handbook(?:-(\d+))?$/.exec(hash)
+ if (!handbook) return
+ await openHelp()
+ const index = Number(handbook[1] ?? 0)
+ const help = await getHelp()
+ const topic = help.docs.find(candidate => candidate.legacyIndexes?.includes(index))
+ if (topic) showHelpDocument(topic)
+}
+
+function showHelpTopics() {
+ resourcesTitle.textContent = "Playground help"
+ helpList.hidden = false
+ helpDocument.hidden = true
+ helpContent.replaceChildren()
+}
+
+function showHelpDocument(topic: PlaygroundHelp["docs"][number]) {
+ helpList.hidden = true
+ helpDocument.hidden = false
+ resourcesTitle.textContent = topic.title
+ helpContent.innerHTML = topic.html
+}
+
+async function loadExample(example: PlaygroundExample) {
+ const confirmed = await requestConfirmation({
+ confirmLabel: "Replace project",
+ danger: true,
+ message: `Replace the current project with “${example.title}”? Unsaved project files will be replaced.`,
+ title: "Open example",
+ })
+ if (!confirmed) return
+ navigateToExample(example)
+}
+
+function createExampleProjectState(example: PlaygroundExample) {
+ const settings = example.compilerSettings ?? {}
+ const fileType = exampleFileType(example, settings)
+ const state = createLegacyProjectState(example.code, fileType)
+ const config = JSON.parse(state.files[configFileName] ?? defaultFiles[0].text)
+ config.compilerOptions ??= {}
+ for (const [originalKey, value] of Object.entries(settings)) {
+ if (originalKey === "ts" || originalKey === "useJavaScript" || originalKey === "filetype") continue
+ const key = originalKey === "checkJS" ? "checkJs" : originalKey
+ config.compilerOptions[key] = parseLegacyCompilerOption(key, String(value))
+ }
+ if (fileType === "js" || fileType === "jsx") {
+ config.compilerOptions.allowJs = true
+ config.compilerOptions.checkJs ??= true
+ }
+ state.files[configFileName] = `${JSON.stringify(config, undefined, 2)}\n`
+ return state
+}
+
+function navigateToExample(example: PlaygroundExample) {
+ const settings = example.compilerSettings ?? {}
+ const state = createExampleProjectState(example)
+ const serialized = serializeProjectState(state)
+ localStorage.setItem(storageKey, serialized)
+ const url = new URL(location.pathname, location.origin)
+ const requestedVersion = typeof settings.ts === "string" ? settings.ts : selectedCompiler
+ if (requestedVersion && !isNativeCompilerVersion(requestedVersion)) {
+ url.searchParams.set("ts", requestedVersion)
+ }
+ url.hash = `${projectHashPrefix.slice(1)}${LZString.compressToEncodedURIComponent(serialized)}`
+ history.replaceState({}, "", url)
+ location.reload()
+}
+
+function exampleFileType(example: PlaygroundExample, settings: PlaygroundExample["compilerSettings"]) {
+ if (settings?.useJavaScript === true) return example.name.endsWith("x") ? "jsx" : "js"
+ const match = /(\.d\.[cm]?ts|\.d\.ts|\.tsx|\.ts|\.jsx|\.js)$/i.exec(example.name)
+ return match?.[1].replace(/^\./, "") ?? "ts"
+}
+
+function getExamples() {
+ examplesPromise ??= Promise.resolve(bundledExamples)
+ return examplesPromise
+}
+
+function getHelp() {
+ helpPromise ??= fetch(new URL("./help.json", import.meta.url)).then(async response => {
+ if (!response.ok) throw new Error(`Could not load help: ${response.status} ${response.statusText}`)
+ return response.json() as Promise
+ })
+ return helpPromise
+}
+
+function registerExampleLinks() {
+ const examplesById = new Map(bundledExamples.examples.map(example => [example.id, example]))
+ for (const language of ["javascript", "typescript"]) {
+ monaco.languages.registerLinkProvider(language, {
+ provideLinks(model) {
+ const links: monaco.languages.ILink[] = []
+ for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) {
+ const line = model.getLineContent(lineNumber)
+ const match = /\/\/\s*example:\s*([\w-]+)/i.exec(line)
+ if (!match) continue
+ const example = examplesById.get(match[1])
+ if (!example) continue
+ const startColumn = match.index + match[0].lastIndexOf(match[1]) + 1
+ const url = legacyExampleUrl(example)
+ links.push({
+ range: new monaco.Range(lineNumber, startColumn, lineNumber, startColumn + match[1].length),
+ tooltip: `Open example: ${example.title}`,
+ url: monaco.Uri.parse(url.href),
+ })
+ }
+ return { links }
+ },
+ })
+ }
+}
+
+function exampleAtPosition(model: monaco.editor.ITextModel | null, position: monaco.Position) {
+ if (!model) return undefined
+ const line = model.getLineContent(position.lineNumber)
+ const match = /\/\/\s*example:\s*([\w-]+)/i.exec(line)
+ if (!match) return undefined
+ const startColumn = match.index + match[0].lastIndexOf(match[1]) + 1
+ if (position.column < startColumn || position.column > startColumn + match[1].length) return undefined
+ return bundledExamples.examples.find(example => example.id === match[1])
+}
+
+function legacyExampleUrl(example: PlaygroundExample) {
+ const url = new URL(location.pathname, location.origin)
+ for (const [key, value] of Object.entries(example.compilerSettings ?? {})) {
+ url.searchParams.set(key, String(value))
+ }
+ if (!url.searchParams.has("ts") && selectedCompiler && !isNativeCompilerVersion(selectedCompiler)) {
+ url.searchParams.set("ts", selectedCompiler)
+ }
+ url.hash = `example/${example.id}`
+ return url
+}
+
+function projectFileContents() {
+ return Object.fromEntries([...projectModels].map(([fileName, model]) => [fileName, model.getValue()]))
+}
+
+function projectTextMap() {
+ return new Map([...projectModels].map(([fileName, model]) => [fileName, model.getValue()]))
+}
+
+function compilerFileContents() {
+ const files = {
+ ...Object.fromEntries(acquiredTypeFiles),
+ ...projectFileContents(),
+ }
+ files[configFileName] = compilerOverrideState.effectiveConfigText
+ return files
+}
+
+function refreshCompilerOverrides() {
+ compilerOverrideState = computeCompilerOverrides(
+ projectTextMap(),
+ projectModels.get(configFileName)?.getValue() ?? defaultFiles[0].text
+ )
+ languageServer?.updateEffectiveConfig(compilerOverrideState.effectiveConfigText)
+ renderCompilerOverrides()
+}
+
+function renderCompilerOverrides() {
+ const byFile = new Map()
+ for (const override of compilerOverrideState.overrides) {
+ const entries = byFile.get(override.fileName) ?? []
+ entries.push(override)
+ byFile.set(override.fileName, entries)
+ }
+
+ for (const [fileName, model] of projectModels) {
+ const hints: monaco.languages.InlayHint[] = []
+ for (const override of byFile.get(fileName) ?? []) {
+ if (!override.applied) continue
+ const end = model.getPositionAt(override.end)
+ const baseValue =
+ override.tsconfigValue === undefined
+ ? "not set in tsconfig"
+ : `tsconfig: ${formatOverrideValue(override.tsconfigValue)}`
+ hints.push({
+ kind: monaco.languages.InlayHintKind.Type,
+ label: `· overrides ${baseValue}`,
+ paddingLeft: true,
+ position: end,
+ })
+ }
+ if (fileName === configFileName) {
+ for (const override of compilerOverrideState.overrides) {
+ if (!override.applied) continue
+ const node = configOptionNode(model.getValue(), override.option)
+ if (!node) continue
+ const end = model.getPositionAt(node.offset + node.length)
+ hints.push({
+ kind: monaco.languages.InlayHintKind.Type,
+ label: `· overridden by ${relativeProjectPath(override.fileName)}:${
+ override.lineNumber
+ } → ${formatOverrideValue(override.value)}`,
+ paddingLeft: true,
+ position: end,
+ })
+ }
+ }
+ compilerOverrideHints.set(model.uri.toString(), hints)
+ const markers = compilerOverrideState.diagnostics
+ .filter(diagnostic => diagnostic.fileName === fileName)
+ .map(diagnostic => {
+ const start = model.getPositionAt(diagnostic.start)
+ const end = model.getPositionAt(Math.max(diagnostic.start + 1, diagnostic.end))
+ return {
+ code: "PLAYGROUND_OVERRIDE",
+ endColumn: end.column,
+ endLineNumber: end.lineNumber,
+ message: diagnostic.message,
+ severity: monaco.MarkerSeverity.Error,
+ source: "Playground",
+ startColumn: start.column,
+ startLineNumber: start.lineNumber,
+ }
+ })
+ monaco.editor.setModelMarkers(model, "compiler-overrides", markers)
+ }
+
+ if (effectiveConfigModel && effectiveConfigModel.getValue() !== compilerOverrideState.effectiveConfigText) {
+ effectiveConfigModel.setValue(compilerOverrideState.effectiveConfigText)
+ }
+ const activeOverrides = compilerOverrideState.overrides.filter(override => override.applied)
+ applyCompilerOverridesButton.hidden = activeOverrides.length === 0
+ applyCompilerOverridesButton.textContent = `Apply ${activeOverrides.length} override${
+ activeOverrides.length === 1 ? "" : "s"
+ }`
+ inlayEmitter.fire()
+ if (overrideCodeLensProvider) overrideCodeLensEmitter.fire(overrideCodeLensProvider)
+}
+
+function registerCompilerOverrideFeatures() {
+ monaco.editor.registerCommand("playground.showEffectiveConfig", () => {
+ const uri = monaco.Uri.parse("playground:///effective-tsconfig.json")
+ effectiveConfigModel ??= monaco.editor.createModel(compilerOverrideState.effectiveConfigText, "json", uri)
+ inputEditor.setModel(effectiveConfigModel)
+ inputEditor.focus()
+ })
+ monaco.editor.registerCommand(
+ "playground.goToCompilerOverride",
+ (_accessor, fileName: string, start: number, end: number) => {
+ const model = projectModels.get(fileName)
+ if (!model) return
+ navigateToModel(fileName, spanRange(model, start, end))
+ }
+ )
+ monaco.editor.registerCommand("playground.goToConfigOption", (_accessor, option: string) => {
+ const model = projectModels.get(configFileName)
+ if (!model) return
+ const node = configOptionNode(model.getValue(), option) ?? compilerOptionsNode(model.getValue())
+ const range = node ? spanRange(model, node.offset, node.offset + node.length) : model.getFullModelRange()
+ navigateToModel(configFileName, range)
+ })
+ for (const language of ["javascript", "typescript"]) {
+ monaco.languages.registerHoverProvider(language, {
+ provideHover(model, position) {
+ const override = compilerOverrideAt(model, model.getOffsetAt(position))
+ if (!override) return undefined
+ return {
+ contents: [
+ { value: `**Playground compiler override**` },
+ {
+ value: `\`${override.option}\`: ${formatOverrideValue(override.tsconfigValue)} → **${formatOverrideValue(
+ override.value
+ )}**`,
+ },
+ { value: "This project-wide directive takes precedence over `tsconfig.json`." },
+ ],
+ range: spanRange(model, override.start, override.end),
+ }
+ },
+ })
+ monaco.languages.registerCompletionItemProvider(language, {
+ triggerCharacters: ["@"],
+ async provideCompletionItems(model, position) {
+ const line = model.getLineContent(position.lineNumber).slice(0, position.column - 1)
+ const match = /\/\/\s*@([\w-]*)$/.exec(line)
+ if (!match) return { suggestions: [] }
+ const metadata = await getCompilerOptionMetadata()
+ const range = new monaco.Range(
+ position.lineNumber,
+ position.column - match[1].length,
+ position.lineNumber,
+ position.column
+ )
+ return {
+ suggestions: [...metadata].map(([name, info]) => ({
+ detail: info.description,
+ insertText: `${name}: `,
+ kind: monaco.languages.CompletionItemKind.Property,
+ label: name,
+ range,
+ })),
+ }
+ },
+ })
+ monaco.languages.registerCodeActionProvider(language, {
+ provideCodeActions(model, range) {
+ const override = compilerOverrideAt(model, model.getOffsetAt(range.getStartPosition()))
+ if (!override) return { actions: [], dispose() {} }
+ const removeEdit = removeOverrideEdit(model, override)
+ const actions: monaco.languages.CodeAction[] = [
+ {
+ edit: { edits: [removeEdit] },
+ kind: "quickfix",
+ title: `Remove @${override.option} override`,
+ },
+ {
+ command: {
+ arguments: [override.option],
+ id: "playground.goToConfigOption",
+ title: "Go to tsconfig option",
+ },
+ kind: "quickfix",
+ title: `Go to ${override.option} in tsconfig.json`,
+ },
+ ]
+ if (override.applied) {
+ const configModel = projectModels.get(configFileName)
+ if (configModel) {
+ actions.unshift({
+ edit: {
+ edits: [
+ {
+ resource: configModel.uri,
+ textEdit: {
+ range: configModel.getFullModelRange(),
+ text: setCompilerOption(configModel.getValue(), override.option, override.value),
+ },
+ versionId: configModel.getVersionId(),
+ },
+ removeEdit,
+ ],
+ },
+ isPreferred: true,
+ kind: "quickfix",
+ title: `Move @${override.option} to tsconfig.json`,
+ })
+ }
+ }
+ return { actions, dispose() {} }
+ },
+ })
+ }
+
+ monaco.languages.registerHoverProvider("json", {
+ provideHover(model, position) {
+ if (model.uri.path !== configFileName) return undefined
+ const offset = model.getOffsetAt(position)
+ const override = compilerOverrideState.overrides.find(candidate => {
+ if (!candidate.applied) return false
+ const node = configOptionNode(model.getValue(), candidate.option)
+ return node && offset >= node.offset && offset <= node.offset + node.length
+ })
+ if (!override) return undefined
+ return {
+ contents: [
+ { value: `**Overridden by ${relativeProjectPath(override.fileName)}:${override.lineNumber}**` },
+ {
+ value: `Effective \`${override.option}\`: **${formatOverrideValue(override.value)}**`,
+ },
+ ],
+ }
+ },
+ })
+ overrideCodeLensProvider = {
+ onDidChange: overrideCodeLensEmitter.event,
+ provideCodeLenses(model) {
+ if (model.uri.path !== configFileName) return { lenses: [], dispose() {} }
+ const active = compilerOverrideState.overrides.filter(override => override.applied)
+ if (active.length === 0) return { lenses: [], dispose() {} }
+ const node = compilerOptionsNode(model.getValue())
+ const position = node ? model.getPositionAt(node.offset) : new monaco.Position(1, 1)
+ return {
+ dispose() {},
+ lenses: [
+ {
+ command: {
+ id: "playground.showEffectiveConfig",
+ title: `${active.length} inline compiler override${
+ active.length === 1 ? "" : "s"
+ } active · View effective config`,
+ },
+ range: new monaco.Range(position.lineNumber, 1, position.lineNumber, 1),
+ },
+ ],
+ }
+ },
+ }
+ monaco.languages.registerCodeLensProvider("json", overrideCodeLensProvider)
+ monaco.languages.registerCodeActionProvider("json", {
+ provideCodeActions(model, range) {
+ if (model.uri.path !== configFileName) return { actions: [], dispose() {} }
+ const offset = model.getOffsetAt(range.getStartPosition())
+ const override = compilerOverrideState.overrides.find(candidate => {
+ if (!candidate.applied) return false
+ const node = configOptionNode(model.getValue(), candidate.option)
+ return node && offset >= node.offset && offset <= node.offset + node.length
+ })
+ if (!override) return { actions: [], dispose() {} }
+ const sourceModel = projectModels.get(override.fileName)
+ return {
+ actions: [
+ {
+ command: {
+ arguments: [override.fileName, override.start, override.end],
+ id: "playground.goToCompilerOverride",
+ title: "Go to overriding directive",
+ },
+ kind: "quickfix",
+ title: `Go to @${override.option} override`,
+ },
+ ...(sourceModel
+ ? [
+ {
+ edit: { edits: [removeOverrideEdit(sourceModel, override)] },
+ kind: "quickfix",
+ title: `Use tsconfig value and remove @${override.option}`,
+ } satisfies monaco.languages.CodeAction,
+ ]
+ : []),
+ ],
+ dispose() {},
+ }
+ },
+ })
+}
+
+let compilerOptionMetadataPromise: Promise> | undefined
+
+function getCompilerOptionMetadata() {
+ compilerOptionMetadataPromise ??= fetch(new URL("./tsconfig.schema.json", import.meta.url)).then(async response => {
+ if (!response.ok) throw new Error(`Could not load TSConfig options: ${response.status}`)
+ const schema = await response.json()
+ const properties = schema.definitions?.compilerOptionsDefinition?.properties?.compilerOptions?.properties ?? {}
+ return new Map(
+ Object.entries(properties).map(([name, value]: [string, any]) => [
+ name,
+ { description: value.markdownDescription ?? value.description ?? "" },
+ ])
+ )
+ })
+ return compilerOptionMetadataPromise
+}
+
+function compilerOverrideAt(model: monaco.editor.ITextModel, offset: number) {
+ return compilerOverrideState.overrides.find(
+ override => override.fileName === model.uri.path && offset >= override.start && offset <= override.end
+ )
+}
+
+function removeOverrideEdit(model: monaco.editor.ITextModel, override: CompilerOverride) {
+ const start = model.getPositionAt(override.start)
+ const end =
+ start.lineNumber < model.getLineCount()
+ ? new monaco.Position(start.lineNumber + 1, 1)
+ : model.getPositionAt(override.end)
+ return {
+ resource: model.uri,
+ textEdit: {
+ range: new monaco.Range(start.lineNumber, 1, end.lineNumber, end.column),
+ text: "",
+ },
+ versionId: model.getVersionId(),
+ }
+}
+
+function applyCompilerOverridesToConfig() {
+ const overrides = compilerOverrideState.overrides.filter(override => override.applied)
+ const configModel = projectModels.get(configFileName)
+ if (overrides.length === 0 || !configModel) return
+
+ let configText = configModel.getValue()
+ for (const override of overrides) {
+ configText = setCompilerOption(configText, override.option, override.value)
+ }
+ configModel.pushEditOperations([], [{ range: configModel.getFullModelRange(), text: configText }], () => null)
+
+ const byFile = new Map()
+ for (const override of overrides) {
+ const entries = byFile.get(override.fileName) ?? []
+ entries.push(override)
+ byFile.set(override.fileName, entries)
+ }
+ for (const [fileName, fileOverrides] of byFile) {
+ const model = projectModels.get(fileName)
+ if (!model) continue
+ const edits = [...fileOverrides]
+ .sort((left, right) => right.start - left.start)
+ .map(override => {
+ const edit = removeOverrideEdit(model, override).textEdit
+ return { range: edit.range, text: edit.text }
+ })
+ model.pushEditOperations([], edits, () => null)
+ }
+}
+
+function spanRange(model: monaco.editor.ITextModel, start: number, end: number) {
+ const startPosition = model.getPositionAt(start)
+ const endPosition = model.getPositionAt(end)
+ return new monaco.Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column)
+}
+
+function formatOverrideValue(value: unknown) {
+ if (value === undefined) return "(unset)"
+ return typeof value === "string" ? value : JSON.stringify(value)
+}
+
+function compilerOverrideDiagnostics(): Diagnostic[] {
+ return compilerOverrideState.diagnostics.map((diagnostic, index) => ({
+ category: DiagnosticCategory.Error,
+ code: 98000 + index,
+ end: diagnostic.end,
+ fileName: diagnostic.fileName,
+ pos: diagnostic.start,
+ source: "Playground",
+ text: diagnostic.message,
+ }))
+}
+
+function scheduleTypeAcquisition() {
+ if (!playgroundSettings.automaticTypeAcquisition) return
+ window.clearTimeout(typeAcquisitionTimer)
+ typeAcquisitionTimer = window.setTimeout(() => {
+ typeAcquisitionQueue = typeAcquisitionQueue.then(() => refreshTypeAcquisition(false))
+ }, 900)
+}
+
+async function refreshTypeAcquisition(initial: boolean) {
+ if (!playgroundSettings.automaticTypeAcquisition) {
+ typeAcquisitionFailure = undefined
+ if (compilerReady) renderStatus()
+ return
+ }
+ const source = [...projectModels.values()]
+ .filter(model => model.getLanguageId() === "javascript" || model.getLanguageId() === "typescript")
+ .map(model => model.getValue())
+ .join("\n")
+ if (!hasPackageImports(source)) {
+ typeAcquisitionFailure = undefined
+ if (compilerReady) renderStatus()
+ return
+ }
+
+ try {
+ if (!acquireTypes) {
+ const typescript = stradaCompilerNamespace ?? (await getTypeAcquisitionCompiler())
+ acquireTypes = createTypeAcquisition({
+ onFile(fileName, text) {
+ acquiredTypeFiles.set(fileName, text)
+ compilerTransport?.setFile(fileName, text)
+ if (lspReady) mountAcquiredTypeModel(fileName, text)
+ else if (languageServer) pendingAcquiredTypeModels.add(fileName)
+ },
+ onProgress(downloaded, total) {
+ const detail = `${downloaded} of ${total} declaration files`
+ if (compilerReady) setStatus(`Loading package types · ${detail}`, "loading")
+ else setLoadingIndeterminate("Loading package types...", detail)
+ },
+ onStart() {
+ if (compilerReady) setStatus("Loading package types...", "loading")
+ else setLoadingIndeterminate("Loading package types...", "Resolving npm imports")
+ },
+ typescript,
+ })
+ }
+
+ const addedFiles = await acquireTypes(source)
+ typeAcquisitionFailure = undefined
+ if (addedFiles > 0 && stradaBackend) {
+ await compileStradaProject()
+ } else if (addedFiles > 0 && useNativeCompiler) {
+ await compileActiveProject?.()
+ }
+ renderStatus()
+ } catch (error) {
+ typeAcquisitionFailure = error instanceof Error ? error.message : String(error)
+ console.error("Could not acquire package types", error)
+ renderStatus()
+ }
+}
+
+function mountAcquiredTypeModel(fileName: string, text: string) {
+ const uri = monaco.Uri.file(fileName)
+ const existing = monaco.editor.getModel(uri)
+ if (existing) {
+ if (existing.getValue() !== text) existing.setValue(text)
+ return
+ }
+ monaco.editor.createModel(text, languageForFile(fileName), uri)
+}
+
+async function getTypeAcquisitionCompiler() {
+ typeAcquisitionCompilerPromise ??= (async () => {
+ const version = await resolveStradaVersion("latest")
+ const url = `https://playgroundcdn.typescriptlang.org/cdn/${version}/typescript/lib/typescript.js`
+ const response = await fetch(url)
+ if (!response.ok) {
+ throw new Error(`Could not load TypeScript for package type acquisition: ${response.status}`)
+ }
+ const source = await response.text()
+ return new Function(`${source}\nreturn ts;`)() as typeof import("typescript")
+ })()
+ return typeAcquisitionCompilerPromise
+}
+
+function navigateToModel(fileName: string, range?: monaco.IRange) {
+ const model = monaco.editor.getModel(monaco.Uri.file(fileName))
+ if (!model) return
+ if (inputEditor.getModel() === model && !range) {
+ inputEditor.focus()
+ return
+ }
+ const currentLocation = getEditorLocation()
+ if (currentLocation) {
+ pushEditorLocation(backLocations, currentLocation)
+ forwardLocations.length = 0
+ }
+ applyingEditorNavigation = true
+ try {
+ inputEditor.setModel(model)
+ if (range) {
+ inputEditor.setSelection(range)
+ inputEditor.revealRangeInCenter(range, monaco.editor.ScrollType.Immediate)
+ }
+ } finally {
+ applyingEditorNavigation = false
+ }
+ trackedEditorLocation = getEditorLocation()
+ updateNavigationButtons()
+ inputEditor.focus()
+}
+
+function getEditorLocation(): EditorLocation | undefined {
+ const model = inputEditor.getModel()
+ const selection = inputEditor.getSelection()
+ return model && selection ? { selection, uri: model.uri.toString() } : undefined
+}
+
+function pushEditorLocation(stack: EditorLocation[], location: EditorLocation) {
+ const previous = stack.at(-1)
+ if (previous?.uri === location.uri && sameSelection(previous.selection, location.selection)) return
+ stack.push(location)
+}
+
+function sameSelection(left: monaco.Selection, right: monaco.Selection) {
+ return (
+ left.selectionStartLineNumber === right.selectionStartLineNumber &&
+ left.selectionStartColumn === right.selectionStartColumn &&
+ left.positionLineNumber === right.positionLineNumber &&
+ left.positionColumn === right.positionColumn
+ )
+}
+
+function updateNavigationButtons() {
+ navigateBackButton.disabled = backLocations.length === 0
+ navigateForwardButton.disabled = forwardLocations.length === 0
+}
+
+function navigateBack() {
+ navigateThroughHistory(backLocations, forwardLocations)
+}
+
+function navigateForward() {
+ navigateThroughHistory(forwardLocations, backLocations)
+}
+
+function navigateThroughHistory(source: EditorLocation[], destination: EditorLocation[]) {
+ let target: EditorLocation | undefined
+ while ((target = source.pop())) {
+ if (monaco.editor.getModel(monaco.Uri.parse(target.uri))) break
+ }
+ if (!target) {
+ updateNavigationButtons()
+ return
+ }
+ const current = getEditorLocation()
+ if (current) pushEditorLocation(destination, current)
+ applyEditorLocation(target)
+}
+
+function applyEditorLocation(location: EditorLocation) {
+ const model = monaco.editor.getModel(monaco.Uri.parse(location.uri))
+ if (!model) return
+ applyingEditorNavigation = true
+ try {
+ inputEditor.setModel(model)
+ inputEditor.setSelection(location.selection)
+ inputEditor.revealRangeInCenter(location.selection, monaco.editor.ScrollType.Immediate)
+ } finally {
+ applyingEditorNavigation = false
+ }
+ trackedEditorLocation = getEditorLocation()
+ updateNavigationButtons()
+ inputEditor.focus()
+}
+
+async function downloadAsset(name: keyof typeof __LOAD_ASSET_SIZES__, url: URL): Promise> {
+ url.searchParams.set("v", __ASSET_CACHE_VERSION__)
+ const request = new Request(url)
+ const cache = await getAssetCache()
+ let response = await cache?.match(request)
+ cachedAssets.set(name, response !== undefined)
+ let cacheWrite: Promise | undefined
+ if (!response) {
+ response = await fetch(request)
+ if (cache) cacheWrite = cache.put(request, response.clone())
+ }
+ if (!response.ok) {
+ throw new Error(`Unable to load ${url.pathname}: ${response.status} ${response.statusText}`)
+ }
+ if (!response.body) {
+ const bytes = new Uint8Array(await response.arrayBuffer())
+ await cacheWrite
+ downloadedAssets.set(name, bytes.length)
+ updateDownloadProgress()
+ return bytes
+ }
+
+ const chunks: Uint8Array[] = []
+ const reader = response.body.getReader()
+ let length = 0
+ for (;;) {
+ const { done, value } = await reader.read()
+ if (done) break
+ chunks.push(value)
+ length += value.length
+ downloadedAssets.set(name, length)
+ updateDownloadProgress()
+ }
+
+ const bytes = new Uint8Array(length)
+ let offset = 0
+ for (const chunk of chunks) {
+ bytes.set(chunk, offset)
+ offset += chunk.length
+ }
+ await cacheWrite
+ return bytes
+}
+
+function getAssetCache() {
+ assetCachePromise ??= openAssetCache()
+ return assetCachePromise
+}
+
+async function confirmLargeDownloadIfNeeded() {
+ if (downloadWarningSuppressed() || (await compilerAssetsAreCached())) return
+
+ const totalBytes = Object.values(__LOAD_ASSET_SIZES__).reduce((total, value) => total + value, 0)
+ downloadSize.textContent = formatBytes(totalBytes)
+ rememberDownloadConsent.checked = true
+ loadingAnimation.hidden = true
+ loadingCopy.hidden = true
+ downloadConsent.hidden = false
+ loader.hidden = false
+ loader.dataset.state = "consent"
+
+ await new Promise(resolve => {
+ confirmDownloadButton.addEventListener(
+ "click",
+ () => {
+ try {
+ if (rememberDownloadConsent.checked) localStorage.setItem(downloadConsentStorageKey, "true")
+ else localStorage.removeItem(downloadConsentStorageKey)
+ } catch (error) {
+ console.warn("Could not save the download warning preference", error)
+ }
+ resolve()
+ },
+ { once: true }
+ )
+ confirmDownloadButton.focus()
+ })
+
+ downloadConsent.hidden = true
+ loadingAnimation.hidden = false
+ loadingCopy.hidden = false
+ delete loader.dataset.state
+}
+
+function downloadWarningSuppressed() {
+ try {
+ return localStorage.getItem(downloadConsentStorageKey) === "true"
+ } catch {
+ return false
+ }
+}
+
+async function compilerAssetsAreCached() {
+ const cache = await getAssetCache()
+ if (!cache) return false
+ const urls = [
+ new URL("./tsc.wasm", import.meta.url),
+ new URL("./lib-files.json", import.meta.url),
+ new URL("./tsconfig.schema.json", import.meta.url),
+ ]
+ const matches = await Promise.all(
+ urls.map(url => {
+ url.searchParams.set("v", __ASSET_CACHE_VERSION__)
+ return cache.match(new Request(url))
+ })
+ )
+ return matches.every(response => response !== undefined)
+}
+
+async function openAssetCache() {
+ if (!("caches" in globalThis)) return undefined
+ const cacheName = `${assetCachePrefix}${__ASSET_CACHE_VERSION__}`
+ try {
+ const cacheNames = await caches.keys()
+ await Promise.all(
+ cacheNames
+ .filter(name => name.startsWith(assetCachePrefix) && name !== cacheName)
+ .map(name => caches.delete(name))
+ )
+ return caches.open(cacheName)
+ } catch (error) {
+ console.warn("Could not open the TypeScript asset cache", error)
+ return undefined
+ }
+}
+
+function updateDownloadProgress() {
+ const totalBytes = Object.values(__LOAD_ASSET_SIZES__).reduce((total, value) => total + value, 0)
+ const downloadedBytes = [...downloadedAssets.values()].reduce((total, value) => total + value, 0)
+ const loadingFromNetwork = [...cachedAssets.values()].some(cached => !cached)
+ setLoadingProgress(
+ Math.min(70, (downloadedBytes / totalBytes) * 70),
+ loadingFromNetwork ? "Downloading TypeScript..." : "Loading cached TypeScript...",
+ `${formatBytes(downloadedBytes)} of ${formatBytes(totalBytes)}`
+ )
+}
+
+function setLoadingProgress(value: number, message: string, detail: string) {
+ loadingProgress.value = Math.max(loadingProgress.value, value)
+ loadingMessage.textContent = message
+ loadingDetail.textContent = detail
+}
+
+function setLoadingIndeterminate(message: string, detail: string) {
+ loadingProgress.removeAttribute("value")
+ loadingMessage.textContent = message
+ loadingDetail.textContent = detail
+}
+
+function formatBytes(bytes: number) {
+ if (bytes < 1024) return `${bytes} B`
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`
+}
+
+function compileNativeProject(api: API) {
+ setStatus("Checking project...", "loading")
+ runButton.disabled = true
+
+ try {
+ const transport = compilerTransport
+ if (!transport) throw new Error("The compiler transport is not initialized")
+ for (const [fileName, text] of Object.entries(compilerFileContents())) {
+ transport.setFile(fileName, text)
+ }
+
+ const config = api.readConfigFile(configFileName)
+ const parsed = api.parseJsonConfigFileContent(config.config, { configFileName })
+ const program = api.createProgram(parsed.fileNames, parsed.options, {
+ projectReferences: parsed.projectReferences,
+ configFileParsingDiagnostics: parsed.errors,
+ })
+
+ try {
+ const emit = program.emitToString()
+ const diagnostics = deduplicateDiagnostics(
+ remapCompilerOverrideDiagnostics([
+ ...(config.error ? [config.error] : []),
+ ...parsed.errors,
+ ...program.getSyntacticDiagnostics(),
+ ...program.getSemanticDiagnostics(),
+ ...program.getConfigFileParsingDiagnostics(),
+ ...emit.diagnostics,
+ ...compilerOverrideDiagnostics(),
+ ])
+ )
+ diagnosticCount = diagnostics.length
+ setDiagnostics(diagnostics)
+
+ emittedFiles = new Map([...emit.outputFiles].map(([fileName, output]) => [fileName, output.text]))
+ runButton.disabled = ![...emittedFiles.keys()].some(fileName => fileName.endsWith(".js"))
+ void renderEmittedFiles()
+
+ typeQueries.clear()
+ collectProgramTypeQueries(program, parsed.fileNames)
+
+ const configuredFiles = new Set(parsed.fileNames)
+ const orphanSourceFiles = [...projectModels]
+ .filter(
+ ([fileName, model]) =>
+ !configuredFiles.has(fileName) &&
+ (model.getLanguageId() === "javascript" || model.getLanguageId() === "typescript")
+ )
+ .map(([fileName]) => fileName)
+ if (orphanSourceFiles.length > 0) {
+ const inferredProgram = api.createProgram(orphanSourceFiles, {
+ ...parsed.options,
+ allowJs: true,
+ })
+ try {
+ collectProgramTypeQueries(inferredProgram, orphanSourceFiles)
+ } finally {
+ inferredProgram.dispose()
+ }
+ }
+ inlayEmitter.fire()
+ projectFailure = undefined
+ compilerFailure = undefined
+ renderStatus()
+ } finally {
+ program.dispose()
+ }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error)
+ diagnosticCount = 0
+ emittedFiles = new Map()
+ runButton.disabled = true
+ setDiagnostics([])
+ typeQueries.clear()
+ inlayEmitter.fire()
+ renderEmitError(message)
+ projectFailure = message
+ renderStatus()
+ console.error(error)
+ }
+}
+
+let stradaCompileVersion = 0
+
+async function compileStradaProject() {
+ const backend = stradaBackend
+ if (!backend) return
+ const compileVersion = ++stradaCompileVersion
+ setStatus("Checking project...", "loading")
+ runButton.disabled = true
+ try {
+ const result = await backend.compile()
+ if (compileVersion !== stradaCompileVersion) return
+ const diagnostics = remapCompilerOverrideDiagnostics([
+ ...(result.diagnostics as Diagnostic[]),
+ ...compilerOverrideDiagnostics(),
+ ])
+ diagnosticCount = diagnostics.length
+ setDiagnostics(diagnostics)
+ emittedFiles = new Map(Object.entries(result.outputFiles))
+ runButton.disabled = ![...emittedFiles.keys()].some(fileName => fileName.endsWith(".js"))
+ void renderEmittedFiles()
+
+ typeQueries.clear()
+ await Promise.all(
+ [...projectModels.values()]
+ .filter(model => model.getLanguageId() === "javascript" || model.getLanguageId() === "typescript")
+ .map(model => collectStradaTypeQueries(backend, model))
+ )
+ if (compileVersion !== stradaCompileVersion) return
+ inlayEmitter.fire()
+ projectFailure = undefined
+ compilerFailure = undefined
+ renderStatus()
+ } catch (error) {
+ if (compileVersion !== stradaCompileVersion) return
+ diagnosticCount = 0
+ emittedFiles = new Map()
+ runButton.disabled = true
+ setDiagnostics([])
+ typeQueries.clear()
+ inlayEmitter.fire()
+ const message = error instanceof Error ? error.message : String(error)
+ renderEmitError(message)
+ projectFailure = message
+ renderStatus()
+ console.error(error)
+ }
+}
+
+async function collectStradaTypeQueries(backend: StradaBackend, model: monaco.editor.ITextModel) {
+ const source = model.getValue()
+ const queryPattern = /^\s*\/\/\s*\^\?\s*$/gm
+ const queries: TypeQuery[] = []
+ let match: RegExpExecArray | null
+ while ((match = queryPattern.exec(source))) {
+ const queryEnd = match.index + match[0].lastIndexOf("?")
+ const queryPosition = model.getPositionAt(queryEnd)
+ if (queryPosition.lineNumber === 1) continue
+ const inspectedPosition = model.getOffsetAt({
+ lineNumber: queryPosition.lineNumber - 1,
+ column: queryPosition.column,
+ })
+ const info =
+ (await backend.quickInfo(model.uri.path, inspectedPosition)) ??
+ (await backend.quickInfo(model.uri.path, Math.max(0, inspectedPosition - 1)))
+ if (!info?.displayParts) continue
+ const text = info.displayParts
+ .map((part: { text: string }) => part.text)
+ .join("")
+ .replace(/\r?\n\s*/g, " ")
+ queries.push({
+ lineNumber: queryPosition.lineNumber,
+ column: queryPosition.column + 1,
+ label: truncate(`: ${text}`, 120),
+ })
+ }
+ typeQueries.set(model.uri.toString(), queries)
+}
+
+function collectProgramTypeQueries(program: ReturnType, fileNames: readonly string[]) {
+ const checker = program.getProject().checker
+ for (const fileName of fileNames) {
+ const model = projectModels.get(fileName)
+ const sourceFile = program.getSourceFile(fileName)
+ if (!model || !sourceFile) continue
+ typeQueries.set(model.uri.toString(), collectTypeQueries(model.getValue(), sourceFile, checker, model))
+ }
+}
+
+function deduplicateDiagnostics(diagnostics: readonly Diagnostic[]) {
+ const seen = new Set()
+ return diagnostics.filter(diagnostic => {
+ const key = [diagnostic.fileName, diagnostic.pos, diagnostic.end, diagnostic.code, diagnostic.text].join(":")
+ if (seen.has(key)) return false
+ seen.add(key)
+ return true
+ })
+}
+
+function collectTypeQueries(
+ source: string,
+ sourceFile: CompilerNode,
+ checker: ReturnType["getProject"]>["checker"],
+ model: monaco.editor.ITextModel
+) {
+ const queryPattern = /^\s*\/\/\s*\^\?\s*$/gm
+ const queries: TypeQuery[] = []
+ let match: RegExpExecArray | null
+
+ while ((match = queryPattern.exec(source))) {
+ const queryEnd = match.index + match[0].lastIndexOf("?")
+ const queryPosition = model.getPositionAt(queryEnd)
+ if (queryPosition.lineNumber === 1) continue
+
+ const inspectedPosition = model.getOffsetAt({
+ lineNumber: queryPosition.lineNumber - 1,
+ column: queryPosition.column,
+ })
+ const node =
+ findNodeAtPosition(sourceFile, inspectedPosition) ??
+ findNodeAtPosition(sourceFile, Math.max(0, inspectedPosition - 1))
+ if (!node) continue
+
+ const type = checker.getTypeAtLocation(node as never)
+ const typeText = checker.typeToString(type, node as never).replace(/\r?\n\s*/g, " ")
+ queries.push({
+ lineNumber: queryPosition.lineNumber,
+ column: queryPosition.column + 1,
+ label: truncate(`: ${typeText}`, 120),
+ })
+ }
+
+ return queries
+}
+
+function findNodeAtPosition(node: CompilerNode, position: number): CompilerNode | undefined {
+ if (position < node.getFullStart() || position > node.getEnd()) return undefined
+
+ let match: CompilerNode | undefined
+ node.forEachChild(child => {
+ const descendant = findNodeAtPosition(child, position)
+ if (descendant) {
+ match = descendant
+ return true
+ }
+ return undefined
+ })
+ return match ?? node
+}
+
+function setDiagnostics(diagnostics: readonly Diagnostic[]) {
+ for (const [fileName, model] of projectModels) {
+ const markers = diagnostics
+ .filter(diagnostic => diagnostic.fileName === fileName)
+ .map(diagnostic => {
+ const start = model.getPositionAt(Math.max(0, diagnostic.pos))
+ const end = model.getPositionAt(Math.max(diagnostic.pos + 1, diagnostic.end))
+ return {
+ code: `TS${diagnostic.code}`,
+ endColumn: end.column,
+ endLineNumber: end.lineNumber,
+ message: diagnostic.text,
+ severity: diagnosticSeverity(diagnostic.category),
+ source: diagnostic.source || "TS",
+ startColumn: start.column,
+ startLineNumber: start.lineNumber,
+ }
+ })
+ monaco.editor.setModelMarkers(model, "typescript-7", markers)
+ }
+ renderDiagnostics(diagnostics)
+}
+
+function renderDiagnostics(diagnostics: readonly Diagnostic[]) {
+ const sorted = [...diagnostics].sort(
+ (left, right) =>
+ diagnosticSortOrder(left.category) - diagnosticSortOrder(right.category) ||
+ (left.fileName ?? "").localeCompare(right.fileName ?? "") ||
+ left.pos - right.pos ||
+ left.code - right.code
+ )
+ diagnosticsSummary.textContent = String(sorted.length)
+ diagnosticsList.replaceChildren()
+ diagnosticsPanel.dataset.empty = String(sorted.length === 0)
+ if (sorted.length === 0) {
+ diagnosticsList.appendChild(createText("p", "No problems found.", "empty-message"))
+ return
+ }
+ if (!hasShownDiagnostics) {
+ diagnosticsPanel.open = true
+ hasShownDiagnostics = true
+ }
+
+ for (const diagnostic of sorted) {
+ const fileName =
+ diagnostic.fileName && projectModels.has(diagnostic.fileName) ? diagnostic.fileName : configFileName
+ const model = projectModels.get(fileName)
+ const start = model?.getPositionAt(Math.max(0, diagnostic.pos)) ?? new monaco.Position(1, 1)
+ const end =
+ model?.getPositionAt(Math.max(diagnostic.pos + 1, diagnostic.end)) ??
+ new monaco.Position(start.lineNumber, start.column + 1)
+ const button = document.createElement("button")
+ button.type = "button"
+ button.className = `diagnostic diagnostic-${diagnosticCategoryName(diagnostic.category)}`
+ button.appendChild(createText("strong", `TS${diagnostic.code}`))
+ button.appendChild(createText("span", diagnostic.text, "diagnostic-message"))
+ button.appendChild(
+ createText("small", `${relativeProjectPath(fileName)}:${start.lineNumber}:${start.column}`, "diagnostic-location")
+ )
+ button.addEventListener("click", () => {
+ navigateToModel(fileName, new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column))
+ })
+ diagnosticsList.appendChild(button)
+ }
+}
+
+function diagnosticSortOrder(category: number) {
+ switch (category) {
+ case DiagnosticCategory.Error:
+ return 0
+ case DiagnosticCategory.Warning:
+ return 1
+ case DiagnosticCategory.Suggestion:
+ return 2
+ default:
+ return 3
+ }
+}
+
+function diagnosticCategoryName(category: number) {
+ switch (category) {
+ case DiagnosticCategory.Error:
+ return "error"
+ case DiagnosticCategory.Warning:
+ return "warning"
+ case DiagnosticCategory.Suggestion:
+ return "suggestion"
+ default:
+ return "message"
+ }
+}
+
+function diagnosticSeverity(category: number) {
+ switch (category) {
+ case DiagnosticCategory.Error:
+ return monaco.MarkerSeverity.Error
+ case DiagnosticCategory.Warning:
+ return monaco.MarkerSeverity.Warning
+ case DiagnosticCategory.Suggestion:
+ return monaco.MarkerSeverity.Hint
+ default:
+ return monaco.MarkerSeverity.Info
+ }
+}
+
+async function renderEmittedFiles() {
+ const renderVersion = ++emitRenderVersion
+ emitOutput.replaceChildren()
+ const files = [...emittedFiles].sort(([left], [right]) => left.localeCompare(right))
+ emitSummary.textContent =
+ files.length === 0 ? "No files emitted" : `${files.length} emitted file${files.length === 1 ? "" : "s"}`
+
+ if (files.length === 0) {
+ emitOutput.appendChild(createText("p", "No files emitted.", "empty-message"))
+ return
+ }
+
+ for (const [fileName, text] of files) {
+ const displayText = text.replace(/(?:\r?\n)+$/, "")
+ const section = document.createElement("section")
+ section.className = "emit-file"
+ section.appendChild(createText("h3", relativeProjectPath(fileName)))
+ const pre = document.createElement("pre")
+ pre.tabIndex = 0
+ const code = document.createElement("code")
+ code.textContent = displayText
+ pre.appendChild(code)
+ section.appendChild(pre)
+ emitOutput.appendChild(section)
+
+ const language = fileName.endsWith(".js") ? "javascript" : fileName.endsWith(".json") ? "json" : "typescript"
+ const highlighted = await monaco.editor.colorize(displayText, language, { tabSize: 2 })
+ if (renderVersion !== emitRenderVersion) return
+ code.innerHTML = highlighted
+ }
+}
+
+function renderEmitError(message: string) {
+ emitRenderVersion++
+ emitSummary.textContent = "Compile failed"
+ emitOutput.replaceChildren(createText("p", message, "empty-message"))
+}
+
+function runProject() {
+ const logs: RuntimeLog[] = []
+ const javascriptFiles = new Map([...emittedFiles].filter(([fileName]) => fileName.endsWith(".js")))
+ const entryFile =
+ [...javascriptFiles.keys()].find(fileName => fileName.endsWith("/index.js")) ?? javascriptFiles.keys().next().value
+
+ if (!entryFile) {
+ renderRunLogs([{ level: "error", text: "No JavaScript entry file was emitted." }])
+ return
+ }
+ if ([...javascriptFiles.values()].some(code => /^\s*(?:export|import)\b/m.test(code))) {
+ renderRunLogs([
+ {
+ level: "error",
+ text: 'Run supports CommonJS output. Set compilerOptions.module to "CommonJS" in tsconfig.json.',
+ },
+ ])
+ return
+ }
+
+ try {
+ executeCommonJs(entryFile, javascriptFiles, createRuntimeConsole(logs))
+ } catch (error) {
+ logs.push({
+ level: "error",
+ text: error instanceof Error ? error.stack ?? error.message : String(error),
+ })
+ }
+ renderRunLogs(logs)
+ runLog.scrollIntoView({ block: "nearest" })
+}
+
+function executeCommonJs(entryFile: string, files: ReadonlyMap, runtimeConsole: Console) {
+ const cache = new Map()
+
+ const load = (fileName: string): any => {
+ const cached = cache.get(fileName)
+ if (cached) return cached.exports
+ const code = files.get(fileName)
+ if (code === undefined) throw new Error(`Cannot find emitted module ${fileName}`)
+
+ const module = { exports: {} as any }
+ cache.set(fileName, module)
+ const require = (specifier: string) => {
+ if (specifier === "reflect-metadata") return {}
+ if (!specifier.startsWith(".")) {
+ throw new Error(`Run cannot load package import "${specifier}".`)
+ }
+ const resolved = new URL(specifier, `file://${fileName}`).pathname
+ const candidates = [resolved, `${resolved}.js`, `${resolved}/index.js`]
+ const target = candidates.find(candidate => files.has(candidate))
+ if (!target) throw new Error(`Cannot resolve "${specifier}" from ${fileName}`)
+ return load(target)
+ }
+ const directory = fileName.slice(0, fileName.lastIndexOf("/")) || "/"
+ const evaluate = new Function("exports", "require", "module", "__filename", "__dirname", "console", code)
+ evaluate(module.exports, require, module, fileName, directory, runtimeConsole)
+ return module.exports
+ }
+
+ load(entryFile)
+}
+
+function createRuntimeConsole(logs: RuntimeLog[]) {
+ const runtimeConsole = Object.create(console) as Console
+ for (const level of ["debug", "error", "info", "log", "warn"] as const) {
+ runtimeConsole[level] = (...values: any[]) => {
+ logs.push({ level, text: values.map(formatRuntimeValue).join(" ") })
+ renderRunLogs(logs)
+ console[level](...values)
+ }
+ }
+ runtimeConsole.clear = () => {
+ logs.splice(0)
+ renderRunLogs(logs)
+ }
+ return runtimeConsole
+}
+
+function formatRuntimeValue(value: unknown): string {
+ if (typeof value === "string") return value
+ if (typeof value === "bigint") return `${value}n`
+ if (typeof value === "symbol") return String(value)
+ if (value instanceof Error) return value.stack ?? value.message
+ try {
+ const json = JSON.stringify(value, undefined, 2)
+ return json ?? String(value)
+ } catch {
+ return String(value)
+ }
+}
+
+function renderRunLogs(logs: readonly RuntimeLog[]) {
+ runLog.replaceChildren()
+ if (logs.length === 0) {
+ runLog.appendChild(createText("p", "Run the project to see console output.", "empty-message"))
+ return
+ }
+ for (const log of logs) {
+ const row = document.createElement("div")
+ row.className = `run-log-entry ${log.level}`
+ row.appendChild(createText("strong", log.level.slice(0, 3).toUpperCase()))
+ row.appendChild(document.createTextNode(log.text))
+ runLog.appendChild(row)
+ }
+}
+
+function renderFileList() {
+ type Tree = {
+ directories: Map
+ files: string[]
+ }
+
+ const root: Tree = { directories: new Map(), files: [] }
+ for (const fileName of [...projectModels.keys()].sort()) {
+ const parts = relativeProjectPath(fileName).split("/")
+ const basename = parts.pop()!
+ let tree = root
+ for (const part of parts) {
+ let child = tree.directories.get(part)
+ if (!child) {
+ child = { directories: new Map(), files: [] }
+ tree.directories.set(part, child)
+ }
+ tree = child
+ }
+ tree.files.push(basename)
+ }
+
+ fileButtons.clear()
+ fileList.replaceChildren(renderTree(root, ""))
+ updateActiveFile()
+
+ function renderTree(tree: Tree, parentPath: string): HTMLUListElement {
+ const list = document.createElement("ul")
+ list.className = "file-tree"
+ for (const [directory, child] of [...tree.directories].sort(([left], [right]) => left.localeCompare(right))) {
+ const item = document.createElement("li")
+ const details = document.createElement("details")
+ details.open = true
+ details.appendChild(createText("summary", directory, "file-tree-folder"))
+ details.appendChild(renderTree(child, `${parentPath}${directory}/`))
+ item.appendChild(details)
+ list.appendChild(item)
+ }
+ for (const basename of tree.files.sort()) {
+ const relativePath = `${parentPath}${basename}`
+ const fileName = `${projectRoot}/${relativePath}`
+ const button = document.createElement("button")
+ button.type = "button"
+ button.className = "file-tree-file-button"
+ button.dataset.kind = fileKind(fileName)
+ button.textContent = basename
+ button.addEventListener("click", () => {
+ navigateToModel(fileName)
+ if (mobileLayout.matches) {
+ layoutState.filesVisible = false
+ applyLayoutState()
+ persistLayoutState()
+ }
+ })
+ fileButtons.set(fileName, button)
+ const deleteButton = document.createElement("button")
+ deleteButton.type = "button"
+ deleteButton.className = "file-tree-delete"
+ deleteButton.textContent = "×"
+ deleteButton.title = `Delete ${relativePath}`
+ deleteButton.setAttribute("aria-label", `Delete ${relativePath}`)
+ deleteButton.addEventListener("click", () => void deleteProjectFile(fileName))
+ const item = document.createElement("li")
+ item.className = "file-tree-file"
+ item.appendChild(button)
+ item.appendChild(deleteButton)
+ list.appendChild(item)
+ }
+ return list
+ }
+}
+
+function updateActiveFile() {
+ const model = inputEditor.getModel()
+ if (!model) return
+ const projectModel = projectModels.has(model.uri.path)
+ currentFile.textContent = projectModel
+ ? relativeProjectPath(model.uri.path)
+ : model.uri.path.startsWith("/typescript/lib/")
+ ? `${model.uri.path.slice(model.uri.path.lastIndexOf("/") + 1)} (bundled)`
+ : model.uri.path
+ inputEditor.updateOptions({ readOnly: !projectModel })
+ editorHint.textContent =
+ model.getLanguageId() === "typescript"
+ ? "Type query: align ^? below an expression"
+ : model.getLanguageId() === "json"
+ ? "Edit compiler options directly"
+ : "Read-only library file"
+ for (const [fileName, button] of fileButtons) {
+ if (fileName === model.uri.path) button.setAttribute("aria-current", "page")
+ else button.removeAttribute("aria-current")
+ }
+ if (projectModel) persistProjectState()
+}
+
+function restoreLegacySelection() {
+ const params = new URLSearchParams(location.search)
+ const values = ["ssl", "ssc", "pln", "pc"].map(key => Number(params.get(key)))
+ if (values.some(value => !Number.isInteger(value) || value <= 0)) return
+ inputEditor.setSelection(new monaco.Selection(values[0], values[1], values[2], values[3]))
+}
+
+function restoreProjectSelection(state: ProjectState) {
+ const selection = state.selection
+ if (!selection) return
+ inputEditor.setSelection(
+ new monaco.Selection(
+ selection.selectionStartLineNumber,
+ selection.selectionStartColumn,
+ selection.positionLineNumber,
+ selection.positionColumn
+ )
+ )
+ inputEditor.revealRangeInCenter(inputEditor.getSelection()!, monaco.editor.ScrollType.Immediate)
+}
+
+function renderStatus() {
+ const failure = compilerFailure ?? lspFailure
+ if (failure) {
+ loader.hidden = false
+ loader.dataset.state = "error"
+ loadingMessage.textContent = failure
+ setStatus(failure, "error")
+ return
+ }
+ if (!compilerReady) {
+ loadingMessage.textContent = "Downloading TypeScript..."
+ setStatus("Loading compiler API...", "loading")
+ return
+ }
+ if (!lspReady) {
+ loadingMessage.textContent = `Starting language server: ${lspStatus}`
+ setStatus(`LSP: ${lspStatus}`, "loading")
+ return
+ }
+
+ loader.hidden = true
+ loader.dataset.state = "ready"
+ if (projectFailure) {
+ setStatus(projectFailure, "error")
+ inputEditor.layout()
+ return
+ }
+ const compiler = lspServerInfo ?? __TS_VERSION__
+ const diagnostics = `${diagnosticCount} diagnostic${diagnosticCount === 1 ? "" : "s"}`
+ setStatus(
+ `${compiler} ready · ${diagnostics}${typeAcquisitionFailure ? ` · package types: ${typeAcquisitionFailure}` : ""}`,
+ typeAcquisitionFailure ? "error" : "ready"
+ )
+ inputEditor.layout()
+}
+
+function setStatus(message: string, state: "loading" | "ready" | "error") {
+ status.textContent = message
+ status.dataset.state = state
+}
+
+function registerProjectModel(model: monaco.editor.ITextModel) {
+ model.onDidChangeContent(() => {
+ persistProjectState()
+ refreshCompilerOverrides()
+ scheduleTypeAcquisition()
+ window.clearTimeout(updateTimer)
+ updateTimer = window.setTimeout(() => {
+ void compileActiveProject?.()
+ }, 220)
+ })
+}
+
+function createNewFile() {
+ newFilePath.value = "src/new-file.ts"
+ newFileError.hidden = true
+ newFileError.textContent = ""
+ newFileDialog.showModal()
+ newFilePath.focus()
+ newFilePath.select()
+}
+
+function finishCreatingFile() {
+ const relativePath = newFilePath.value.trim().replaceAll("\\", "/").replace(/^\/+/, "")
+ const parts = relativePath.split("/")
+ if (relativePath === "" || parts.some(part => part === "" || part === "." || part === "..")) {
+ showNewFileError("Enter a file path inside /workspace.")
+ return
+ }
+
+ const fileName = `${projectRoot}/${relativePath}`
+ if (projectModels.has(fileName)) {
+ showNewFileError(`${relativePath} already exists.`)
+ return
+ }
+
+ newFileDialog.close()
+ const model = monaco.editor.createModel("", languageForFile(fileName), monaco.Uri.parse(`file://${fileName}`))
+ projectModels.set(fileName, model)
+ registerProjectModel(model)
+ refreshCompilerOverrides()
+ renderFileList()
+ inputEditor.setModel(model)
+ inputEditor.focus()
+ persistProjectState()
+ void compileActiveProject?.()
+}
+
+function showNewFileError(message: string) {
+ newFileError.textContent = message
+ newFileError.hidden = false
+ newFilePath.focus()
+}
+
+async function deleteProjectFile(fileName: string) {
+ const model = projectModels.get(fileName)
+ if (!model) return
+ if (projectModels.size === 1) {
+ await requestConfirmation({
+ cancelLabel: null,
+ confirmLabel: "OK",
+ message: "The project must contain at least one file.",
+ title: "Cannot delete file",
+ })
+ return
+ }
+
+ const relativePath = relativeProjectPath(fileName)
+ const confirmed = await requestConfirmation({
+ confirmLabel: "Delete file",
+ danger: true,
+ message: `Delete ${relativePath}? This cannot be undone.`,
+ title: "Delete file",
+ })
+ if (!confirmed) return
+
+ const deletedUri = model.uri.toString()
+ projectModels.delete(fileName)
+ refreshCompilerOverrides()
+ if (inputEditor.getModel() === model) {
+ const remainingFiles = [...projectModels.keys()]
+ const fallbackFile =
+ remainingFiles.find(candidate => candidate === entryFileName) ??
+ remainingFiles.find(candidate => /\.[cm]?[jt]sx?$/i.test(candidate)) ??
+ remainingFiles[0]
+ applyingEditorNavigation = true
+ try {
+ inputEditor.setModel(projectModels.get(fallbackFile)!)
+ } finally {
+ applyingEditorNavigation = false
+ }
+ }
+ model.dispose()
+ removeLocationsForUri(backLocations, deletedUri)
+ removeLocationsForUri(forwardLocations, deletedUri)
+ trackedEditorLocation = getEditorLocation()
+ renderFileList()
+ persistProjectState()
+ location.reload()
+}
+
+function removeLocationsForUri(locations: EditorLocation[], uri: string) {
+ for (let index = locations.length - 1; index >= 0; index--) {
+ if (locations[index].uri === uri) locations.splice(index, 1)
+ }
+}
+
+async function resetProject() {
+ const confirmed = await requestConfirmation({
+ confirmLabel: "Reset project",
+ danger: true,
+ message: "Reset the project to the TypeScript Playground v2 defaults? All current project files will be replaced.",
+ title: "Reset project",
+ })
+ if (!confirmed) return
+ localStorage.removeItem(storageKey)
+ const url = new URL(location.href)
+ url.hash = ""
+ location.replace(url)
+}
+
+function loadProjectState(): ProjectState {
+ if (initialLegacyExample) {
+ return createExampleProjectState(initialLegacyExample)
+ }
+
+ if (location.hash.startsWith(projectHashPrefix)) {
+ const decoded = decodeCompressedHash(location.hash.slice(projectHashPrefix.length))
+ if (decoded) {
+ try {
+ return normalizeVersionedProjectState(JSON.parse(decoded))
+ } catch (error) {
+ console.warn("Could not restore the versioned playground project", error)
+ return { files: {}, useDefaults: true }
+ }
+ }
+ }
+
+ if (location.hash.startsWith("#src=")) {
+ try {
+ return createLegacyProjectState(decodeURIComponent(location.hash.slice("#src=".length)))
+ } catch (error) {
+ console.warn("Could not decode the legacy playground source", error)
+ }
+ }
+
+ if (location.hash.startsWith(legacyCodeHashPrefix)) {
+ const decoded = decodeCompressedHash(location.hash.slice(legacyCodeHashPrefix.length))
+ if (decoded) {
+ try {
+ return normalizeProjectState(JSON.parse(decoded))
+ } catch {
+ return createLegacyProjectState(decoded)
+ }
+ }
+ }
+
+ const stored = localStorage.getItem(storageKey)
+ if (!stored) return { files: {}, useDefaults: true }
+ try {
+ return normalizeProjectState(JSON.parse(stored))
+ } catch (error) {
+ console.warn("Could not restore the TypeScript Playground v2 project", error)
+ return { files: {}, useDefaults: true }
+ }
+}
+
+function legacyExampleFromHash(hash: string) {
+ if (!hash.startsWith("#example/")) return undefined
+ const id = decodeURIComponent(hash.slice("#example/".length))
+ return bundledExamples.examples.find(example => example.id === id)
+}
+
+function decodeCompressedHash(encoded: string) {
+ const decoded = LZString.decompressFromEncodedURIComponent(encoded)
+ if (decoded) return decoded
+ try {
+ return LZString.decompressFromEncodedURIComponent(decodeURIComponent(encoded))
+ } catch {
+ return null
+ }
+}
+
+function serializeProjectState(state: ProjectState) {
+ const versioned: VersionedProjectState = {
+ activeFile: state.activeFile,
+ files: state.files,
+ selection: state.selection,
+ version: projectStateVersion,
+ }
+ return JSON.stringify(versioned)
+}
+
+function normalizeVersionedProjectState(value: unknown) {
+ if (!value || typeof value !== "object" || (value as { version?: unknown }).version !== projectStateVersion) {
+ throw new Error("Unsupported playground project URL version")
+ }
+ return normalizeProjectState(value)
+}
+
+function normalizeProjectState(value: unknown): ProjectState {
+ if (!value || typeof value !== "object") return { files: {}, useDefaults: true }
+ const candidate = value as { activeFile?: unknown; files?: unknown; selection?: unknown; version?: unknown }
+ if (candidate.version !== undefined && candidate.version !== projectStateVersion) {
+ console.warn(`Ignoring unsupported playground project version: ${String(candidate.version)}`)
+ return { files: {}, useDefaults: true }
+ }
+ const filesValue = candidate.files && typeof candidate.files === "object" ? candidate.files : value
+ const files = Object.fromEntries(
+ Object.entries(filesValue).filter(
+ (entry): entry is [string, string] => entry[0].startsWith(`${projectRoot}/`) && typeof entry[1] === "string"
+ )
+ )
+ const migrations = new Map([
+ [`${projectRoot}/index.ts`, entryFileName],
+ [`${projectRoot}/greet.ts`, `${projectRoot}/src/greet.ts`],
+ ])
+ const hasLegacyRootFiles = [...migrations.keys()].some(fileName => files[fileName] !== undefined)
+ for (const [oldPath, newPath] of migrations) {
+ if (files[oldPath] !== undefined && files[newPath] === undefined) {
+ files[newPath] = files[oldPath]
+ }
+ delete files[oldPath]
+ }
+ if (hasLegacyRootFiles && files[configFileName]) {
+ try {
+ const config = JSON.parse(files[configFileName])
+ if (Array.isArray(config.include) && config.include.length === 1 && config.include[0] === "./*.ts") {
+ config.include = ["./src/**/*"]
+ }
+ config.compilerOptions ??= {}
+ config.compilerOptions.declaration ??= true
+ files[configFileName] = `${JSON.stringify(config, undefined, 2)}\n`
+ } catch {
+ files[configFileName] = files[configFileName].replace(
+ /"include"\s*:\s*\[\s*"\.\/\*\.ts"\s*\]/,
+ '"include": ["./src/**/*"]'
+ )
+ }
+ }
+ const requestedActiveFile =
+ typeof candidate.activeFile === "string" ? migrations.get(candidate.activeFile) ?? candidate.activeFile : undefined
+ const selection = normalizeSelection(candidate.selection)
+ return {
+ activeFile: requestedActiveFile?.startsWith(`${projectRoot}/`) ? requestedActiveFile : undefined,
+ files,
+ selection,
+ useDefaults: false,
+ }
+}
+
+function normalizeSelection(value: unknown): ProjectState["selection"] {
+ if (!value || typeof value !== "object") return undefined
+ const candidate = value as Record
+ const keys = ["positionColumn", "positionLineNumber", "selectionStartColumn", "selectionStartLineNumber"] as const
+ if (keys.some(key => !Number.isInteger(candidate[key]) || Number(candidate[key]) < 1)) return undefined
+ return {
+ positionColumn: Number(candidate.positionColumn),
+ positionLineNumber: Number(candidate.positionLineNumber),
+ selectionStartColumn: Number(candidate.selectionStartColumn),
+ selectionStartLineNumber: Number(candidate.selectionStartLineNumber),
+ }
+}
+
+function createLegacyProjectState(code: string, fileType = getLegacyFileType()): ProjectState {
+ const fileNamePattern = /^\s*\/\/\s*@filename:\s*(.+)$/i
+ if (!code.split(/\r\n?|\n/g).some(line => fileNamePattern.test(line))) {
+ const fileName = `${projectRoot}/src/index.${fileType}`
+ return {
+ activeFile: fileName,
+ files: {
+ [configFileName]: defaultFiles[0].text,
+ [fileName]: code,
+ },
+ useDefaults: false,
+ }
+ }
+
+ const fourSlashStyle = /^\s*\/\/\s*@Filename:/m.test(code)
+ const files: Record = {
+ [configFileName]: defaultFiles[0].text,
+ }
+ let currentFile = `src/index.${fileType}`
+ let currentLines: string[] = []
+ let hasFileDirective = false
+ const flush = (allowEmpty = false) => {
+ if (currentLines.length === 0 && !allowEmpty) return
+ const relativePath = sanitizeLegacyPath(currentFile) || `src/index.${fileType}`
+ const fileName = `${projectRoot}/${relativePath}`
+ files[fileName] = currentLines.join("\n")
+ }
+ for (const line of code.split(/\r\n?|\n/g)) {
+ const match = fileNamePattern.exec(line)
+ if (match) {
+ flush(hasFileDirective)
+ currentFile = match[1].trim()
+ currentLines = []
+ hasFileDirective = true
+ } else {
+ currentLines.push(fourSlashStyle ? line.replace(/^(\s*)\/\/\/\//, "$1") : line)
+ }
+ }
+ flush(hasFileDirective)
+ const sourceFiles = Object.keys(files).filter(fileName => fileName !== configFileName)
+ return {
+ activeFile: sourceFiles[0] ?? entryFileName,
+ files,
+ useDefaults: false,
+ }
+}
+
+function sanitizeLegacyPath(fileName: string) {
+ const parts = fileName.replaceAll("\\", "/").replace(/^\/+/, "").split("/")
+ return parts.filter(part => part !== "" && part !== "." && part !== "..").join("/")
+}
+
+function getLegacyFileType() {
+ const params = new URLSearchParams(location.search)
+ if (params.has("useJavaScript")) return "js"
+ const fileType = params.get("filetype")
+ return fileType && (fileType === "d.ts" || /^[cm]?[jt]sx?$/.test(fileType)) ? fileType : "ts"
+}
+
+function applyLegacyCompilerOptions(files: Record) {
+ const params = new URLSearchParams(location.search)
+ const reserved = new Set(["example", "filetype", "pc", "pln", "ssc", "ssl", "ts", "useJavaScript"])
+ let config: any
+ try {
+ config = JSON.parse(files[configFileName] ?? defaultFiles[0].text)
+ } catch {
+ config = JSON.parse(defaultFiles[0].text)
+ }
+ config.compilerOptions ??= {}
+ for (const [key, rawValue] of params) {
+ if (reserved.has(key)) continue
+ const value = parseLegacyCompilerOption(key, rawValue)
+ if (value !== undefined) config.compilerOptions[key] = value
+ }
+ if (params.has("useJavaScript") || params.get("filetype")?.startsWith("js")) {
+ config.compilerOptions.allowJs = true
+ config.compilerOptions.checkJs = true
+ }
+ files[configFileName] = `${JSON.stringify(config, undefined, 2)}\n`
+}
+
+function parseLegacyCompilerOption(key: string, rawValue: string) {
+ if (rawValue === "true") return true
+ if (rawValue === "false") return false
+ const number = Number(rawValue)
+ if (!Number.isFinite(number)) return rawValue
+ const enumMaps: Record> = {
+ jsx: {
+ 0: "preserve",
+ 1: "react",
+ 2: "react-native",
+ 3: "react-jsx",
+ 4: "react-jsxdev",
+ },
+ module: {
+ 0: "none",
+ 1: "commonjs",
+ 2: "amd",
+ 3: "umd",
+ 4: "system",
+ 5: "es2015",
+ 6: "es2020",
+ 7: "es2022",
+ 99: "esnext",
+ 100: "node16",
+ 199: "nodenext",
+ 200: "preserve",
+ },
+ moduleResolution: {
+ 1: "classic",
+ 2: "node",
+ 3: "node16",
+ 99: "nodenext",
+ 100: "bundler",
+ },
+ newLine: {
+ 0: "crlf",
+ 1: "lf",
+ },
+ target: {
+ 0: "es3",
+ 1: "es5",
+ 2: "es2015",
+ 3: "es2016",
+ 4: "es2017",
+ 5: "es2018",
+ 6: "es2019",
+ 7: "es2020",
+ 8: "es2021",
+ 9: "es2022",
+ 10: "es2023",
+ 11: "es2024",
+ 12: "es2025",
+ 99: "esnext",
+ },
+ }
+ return enumMaps[key]?.[number] ?? number
+}
+
+function persistProjectState() {
+ const activeModel = inputEditor.getModel()
+ const selection = activeModel && projectModels.has(activeModel.uri.path) ? inputEditor.getSelection() : undefined
+ const state: ProjectState = {
+ activeFile: activeModel && projectModels.has(activeModel.uri.path) ? activeModel.uri.path : entryFileName,
+ files: Object.fromEntries([...projectModels].map(([fileName, model]) => [fileName, model.getValue()])),
+ selection: selection
+ ? {
+ positionColumn: selection.positionColumn,
+ positionLineNumber: selection.positionLineNumber,
+ selectionStartColumn: selection.selectionStartColumn,
+ selectionStartLineNumber: selection.selectionStartLineNumber,
+ }
+ : undefined,
+ }
+ try {
+ const serialized = serializeProjectState(state)
+ localStorage.setItem(storageKey, serialized)
+ if (!playgroundSettings.saveToUrl) return
+ const url = new URL(location.href)
+ url.hash = `${projectHashPrefix.slice(1)}${LZString.compressToEncodedURIComponent(serialized)}`
+ history.replaceState({}, "", url)
+ } catch (error) {
+ console.warn("Could not save the TypeScript Playground v2 project", error)
+ }
+}
+
+function languageForFile(fileName: string): ProjectFile["language"] {
+ if (fileName.endsWith(".json")) return "json"
+ if (/\.[cm]?jsx?$/i.test(fileName)) return "javascript"
+ return "typescript"
+}
+
+function fileKind(fileName: string) {
+ const language = languageForFile(fileName)
+ return language === "json" ? "{}" : language === "javascript" ? "JS" : "TS"
+}
+
+function relativeProjectPath(fileName: string) {
+ return fileName.startsWith(`${projectRoot}/`) ? fileName.slice(projectRoot.length + 1) : fileName
+}
+
+function truncate(value: string, maxLength: number) {
+ return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}…`
+}
+
+function createText(tagName: K, text: string, className?: string) {
+ const element = document.createElement(tagName)
+ element.textContent = text
+ if (className) element.className = className
+ return element
+}
+
+function getElement(id: string) {
+ const element = document.getElementById(id)
+ if (!element) throw new Error(`Missing #${id}`)
+ return element as T
+}
diff --git a/packages/playground-v2/src/strada.ts b/packages/playground-v2/src/strada.ts
new file mode 100644
index 000000000000..dc1aa5e76dad
--- /dev/null
+++ b/packages/playground-v2/src/strada.ts
@@ -0,0 +1,626 @@
+import { monaco } from "./tsgo-lsp"
+
+type CompilerDiagnostic = {
+ category: number
+ code: number
+ end: number
+ fileName?: string
+ pos: number
+ text: string
+}
+
+type CompileResult = {
+ diagnostics: CompilerDiagnostic[]
+ emitSkipped: boolean
+ outputFiles: Record
+ rootNames: string[]
+}
+
+type StradaBackendOptions = {
+ compilerSource: string
+ editor: monaco.editor.IStandaloneCodeEditor
+ files(): Record
+ models: Map
+ onNavigate(fileName: string, range: monaco.Range): void
+ version: string
+ baseUrl: string
+}
+
+export class StradaBackend {
+ readonly version: string
+ readonly #worker = new Worker(new URL("./strada.worker.js", import.meta.url), {
+ type: "module",
+ })
+ readonly #pending = new Map<
+ number,
+ {
+ resolve(value: any): void
+ reject(error: Error): void
+ }
+ >()
+ readonly #disposables: monaco.IDisposable[] = []
+ readonly #options: StradaBackendOptions
+ #nextId = 1
+
+ private constructor(options: StradaBackendOptions) {
+ this.#options = options
+ this.version = options.version
+ this.#worker.addEventListener("message", event => {
+ const pending = this.#pending.get(event.data.id)
+ if (!pending) return
+ this.#pending.delete(event.data.id)
+ if (event.data.error) pending.reject(new Error(event.data.error))
+ else pending.resolve(event.data.result)
+ })
+ }
+
+ static async create(options: StradaBackendOptions) {
+ const backend = new StradaBackend(options)
+ await backend.#request("init", {
+ baseUrl: options.baseUrl,
+ compilerSource: options.compilerSource,
+ files: options.files(),
+ version: options.version,
+ })
+ backend.#registerLanguageFeatures()
+ return backend
+ }
+
+ async compile() {
+ await this.updateFiles()
+ return this.#request("compile", {})
+ }
+
+ async updateFiles() {
+ await this.#request("updateFiles", {
+ files: this.#options.files(),
+ })
+ }
+
+ async quickInfo(fileName: string, position: number) {
+ return this.#request("quickInfo", { fileName, position })
+ }
+
+ async readFile(fileName: string) {
+ return this.#request("readFile", { fileName })
+ }
+
+ async goToDefinition(model: monaco.editor.ITextModel, position: monaco.Position) {
+ await this.updateFiles()
+ const result = await this.#request("definitions", {
+ fileName: model.uri.path,
+ position: model.getOffsetAt(position),
+ })
+ const definition = result?.definitions?.[0]
+ if (!definition) return
+ const target = await this.#ensureModel(definition.fileName)
+ if (!target) return
+ this.#options.onNavigate(target.uri.path, spanToRange(target, definition.textSpan))
+ }
+
+ async #formatOptions(model: monaco.editor.ITextModel) {
+ const options = model.getOptions()
+ return {
+ ConvertTabsToSpaces: options.insertSpaces,
+ IndentSize: options.indentSize,
+ IndentStyle: 2,
+ InsertSpaceAfterCommaDelimiter: true,
+ InsertSpaceAfterFunctionKeywordForAnonymousFunctions: true,
+ InsertSpaceAfterKeywordsInControlFlowStatements: true,
+ InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true,
+ InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false,
+ InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,
+ InsertSpaceAfterSemicolonInForStatements: true,
+ InsertSpaceBeforeAndAfterBinaryOperators: true,
+ NewLineCharacter: model.getEOL(),
+ PlaceOpenBraceOnNewLineForControlBlocks: false,
+ PlaceOpenBraceOnNewLineForFunctions: false,
+ TabSize: options.tabSize,
+ baseIndentSize: 0,
+ convertTabsToSpaces: options.insertSpaces,
+ indentSize: options.indentSize,
+ indentStyle: 2,
+ insertSpaceAfterCommaDelimiter: true,
+ insertSpaceAfterConstructor: false,
+ insertSpaceAfterFunctionKeywordForAnonymousFunctions: true,
+ insertSpaceAfterKeywordsInControlFlowStatements: true,
+ insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: false,
+ insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false,
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true,
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false,
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,
+ insertSpaceAfterSemicolonInForStatements: true,
+ insertSpaceAfterTypeAssertion: false,
+ insertSpaceBeforeAndAfterBinaryOperators: true,
+ insertSpaceBeforeFunctionParenthesis: false,
+ newLineCharacter: model.getEOL(),
+ placeOpenBraceOnNewLineForControlBlocks: false,
+ placeOpenBraceOnNewLineForFunctions: false,
+ semicolons: "ignore",
+ tabSize: options.tabSize,
+ trimTrailingWhitespace: true,
+ }
+ }
+
+ dispose() {
+ this.#disposables.forEach(disposable => disposable.dispose())
+ this.#worker.terminate()
+ }
+
+ #registerLanguageFeatures() {
+ this.#disposables.push(
+ this.#options.editor.addAction({
+ id: "strada.goToDefinition",
+ label: "Go to Definition",
+ keybindings: [monaco.KeyCode.F12],
+ run: () => {
+ const model = this.#options.editor.getModel()
+ const position = this.#options.editor.getPosition()
+ if (model && position) void this.goToDefinition(model, position)
+ },
+ })
+ )
+ for (const language of ["javascript", "typescript"]) {
+ this.#disposables.push(
+ monaco.languages.registerCompletionItemProvider(language, {
+ triggerCharacters: [".", '"', "'", "/", "@", "<", "#"],
+ provideCompletionItems: async (model, position, context) => {
+ await this.updateFiles()
+ const result = await this.#request("completions", {
+ fileName: model.uri.path,
+ options: { triggerCharacter: context.triggerCharacter },
+ position: model.getOffsetAt(position),
+ })
+ const word = model.getWordUntilPosition(position)
+ const range = new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn)
+ const suggestions = (result?.entries ?? []).map((entry: any) => {
+ const suggestion: monaco.languages.CompletionItem & {
+ strada?: {
+ data: any
+ fileName: string
+ name: string
+ position: number
+ source: string | undefined
+ }
+ } = {
+ detail: entry.labelDetails?.description,
+ filterText: entry.filterText,
+ insertText: entry.insertText ?? entry.name,
+ kind: completionKind(entry.kind),
+ label: entry.name,
+ range,
+ sortText: entry.sortText,
+ }
+ suggestion.strada = {
+ data: entry.data,
+ fileName: model.uri.path,
+ name: entry.name,
+ position: model.getOffsetAt(position),
+ source: entry.source,
+ }
+ return suggestion
+ })
+ return {
+ incomplete: result?.isIncomplete,
+ suggestions,
+ }
+ },
+ resolveCompletionItem: async item => {
+ const metadata = (
+ item as typeof item & {
+ strada?: {
+ data: any
+ fileName: string
+ name: string
+ position: number
+ source: string | undefined
+ }
+ }
+ ).strada
+ if (!metadata) return item
+ const details = await this.#request("completionDetails", metadata)
+ if (!details) return item
+ item.detail = displayParts(details.displayParts)
+ item.documentation = {
+ value: displayParts(details.documentation),
+ }
+ const changes = (details.codeActions ?? []).flatMap((action: any) => action.changes ?? [])
+ item.additionalTextEdits = changes
+ .filter((change: any) => normalizePath(change.fileName) === normalizePath(metadata.fileName))
+ .flatMap((change: any) =>
+ change.textChanges.map((textChange: any) => ({
+ range: spanToRange(this.#options.models.get(metadata.fileName)!, textChange.span),
+ text: textChange.newText,
+ }))
+ )
+ return item
+ },
+ }),
+ monaco.languages.registerHoverProvider(language, {
+ provideHover: async (model, position) => {
+ await this.updateFiles()
+ const info = await this.quickInfo(model.uri.path, model.getOffsetAt(position))
+ if (!info) return undefined
+ return {
+ contents: [
+ {
+ value: `\`\`\`typescript\n${displayParts(info.displayParts)}\n\`\`\``,
+ },
+ {
+ value: displayParts(info.documentation),
+ },
+ ],
+ range: spanToRange(model, info.textSpan),
+ }
+ },
+ }),
+ monaco.languages.registerDefinitionProvider(language, {
+ provideDefinition: async (model, position) => {
+ await this.updateFiles()
+ const result = await this.#request("definitions", {
+ fileName: model.uri.path,
+ position: model.getOffsetAt(position),
+ })
+ if (!result?.definitions) return undefined
+ const locations = []
+ for (const definition of result.definitions) {
+ const target = await this.#ensureModel(definition.fileName)
+ if (!target) continue
+ locations.push({
+ range: spanToRange(target, definition.textSpan),
+ uri: target.uri,
+ })
+ }
+ return locations
+ },
+ }),
+ monaco.languages.registerSignatureHelpProvider(language, {
+ signatureHelpTriggerCharacters: ["(", ",", "<"],
+ provideSignatureHelp: async (model, position) => {
+ await this.updateFiles()
+ const help = await this.#request("signatureHelp", {
+ fileName: model.uri.path,
+ options: {
+ triggerReason: {
+ kind: "invoked",
+ },
+ },
+ position: model.getOffsetAt(position),
+ })
+ if (!help) return undefined
+ return {
+ dispose() {},
+ value: {
+ activeParameter: help.argumentIndex,
+ activeSignature: help.selectedItemIndex,
+ signatures: help.items.map((item: any) => ({
+ documentation: displayParts(item.documentation),
+ label: [
+ displayParts(item.prefixDisplayParts),
+ item.parameters
+ .map((parameter: any) => displayParts(parameter.displayParts))
+ .join(displayParts(item.separatorDisplayParts)),
+ displayParts(item.suffixDisplayParts),
+ ].join(""),
+ parameters: item.parameters.map((parameter: any) => ({
+ documentation: displayParts(parameter.documentation),
+ label: displayParts(parameter.displayParts),
+ })),
+ })),
+ },
+ }
+ },
+ }),
+ monaco.languages.registerReferenceProvider(language, {
+ provideReferences: async (model, position) => {
+ await this.updateFiles()
+ const references = await this.#request("references", {
+ fileName: model.uri.path,
+ position: model.getOffsetAt(position),
+ })
+ if (!references) return []
+ const locations = []
+ for (const reference of references) {
+ const target = await this.#ensureModel(reference.fileName)
+ if (!target) continue
+ locations.push({
+ range: spanToRange(target, reference.textSpan),
+ uri: target.uri,
+ })
+ }
+ return locations
+ },
+ }),
+ monaco.languages.registerDocumentHighlightProvider(language, {
+ provideDocumentHighlights: async (model, position) => {
+ await this.updateFiles()
+ const results = await this.#request("occurrences", {
+ fileName: model.uri.path,
+ position: model.getOffsetAt(position),
+ })
+ return (results ?? []).map((occurrence: any) => ({
+ kind: occurrence.isWriteAccess
+ ? monaco.languages.DocumentHighlightKind.Write
+ : monaco.languages.DocumentHighlightKind.Text,
+ range: spanToRange(model, occurrence.textSpan),
+ }))
+ },
+ }),
+ monaco.languages.registerDocumentSymbolProvider(language, {
+ provideDocumentSymbols: async model => {
+ await this.updateFiles()
+ const items = await this.#request("navigationBarItems", {
+ fileName: model.uri.path,
+ })
+ return items?.flatMap((item: any) => navigationSymbols(model, item)) ?? []
+ },
+ }),
+ monaco.languages.registerRenameProvider(language, {
+ resolveRenameLocation: async (model, position) => {
+ await this.updateFiles()
+ const info = await this.#request("renameInfo", {
+ fileName: model.uri.path,
+ position: model.getOffsetAt(position),
+ })
+ if (!info?.canRename) {
+ return {
+ range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
+ rejectReason: info?.localizedErrorMessage ?? "This symbol cannot be renamed.",
+ text: "",
+ }
+ }
+ return {
+ range: spanToRange(model, info.triggerSpan),
+ text: model.getValueInRange(spanToRange(model, info.triggerSpan)),
+ }
+ },
+ provideRenameEdits: async (model, position, newName) => {
+ await this.updateFiles()
+ const locations = await this.#request("renameLocations", {
+ fileName: model.uri.path,
+ position: model.getOffsetAt(position),
+ })
+ if (!locations) return { edits: [], rejectReason: "This symbol cannot be renamed." }
+ const edits = []
+ for (const location of locations) {
+ const target = await this.#ensureModel(location.fileName)
+ if (!target) continue
+ edits.push({
+ resource: target.uri,
+ textEdit: {
+ range: spanToRange(target, location.textSpan),
+ text: `${location.prefixText ?? ""}${newName}${location.suffixText ?? ""}`,
+ },
+ versionId: target.getVersionId(),
+ })
+ }
+ return { edits }
+ },
+ }),
+ monaco.languages.registerDocumentFormattingEditProvider(language, {
+ provideDocumentFormattingEdits: async model => {
+ await this.updateFiles()
+ const edits = await this.#request("formatDocument", {
+ fileName: model.uri.path,
+ options: await this.#formatOptions(model),
+ })
+ return edits.map(edit => ({
+ range: spanToRange(model, edit.span),
+ text: edit.newText,
+ }))
+ },
+ }),
+ monaco.languages.registerDocumentRangeFormattingEditProvider(language, {
+ provideDocumentRangeFormattingEdits: async (model, range) => {
+ await this.updateFiles()
+ const edits = await this.#request("formatRange", {
+ end: model.getOffsetAt(range.getEndPosition()),
+ fileName: model.uri.path,
+ options: await this.#formatOptions(model),
+ start: model.getOffsetAt(range.getStartPosition()),
+ })
+ return edits.map(edit => ({
+ range: spanToRange(model, edit.span),
+ text: edit.newText,
+ }))
+ },
+ }),
+ monaco.languages.registerOnTypeFormattingEditProvider(language, {
+ autoFormatTriggerCharacters: [";", "}", "\n"],
+ provideOnTypeFormattingEdits: async (model, position, ch) => {
+ await this.updateFiles()
+ const edits = await this.#request("formatOnType", {
+ fileName: model.uri.path,
+ key: ch,
+ options: await this.#formatOptions(model),
+ position: model.getOffsetAt(position),
+ })
+ return edits.map(edit => ({
+ range: spanToRange(model, edit.span),
+ text: edit.newText,
+ }))
+ },
+ }),
+ monaco.languages.registerCodeActionProvider(language, {
+ provideCodeActions: async (model, range, context) => {
+ const errorCodes = context.markers
+ .map(marker => Number(String(marker.code ?? "").replace(/^TS/, "")))
+ .filter(Number.isFinite)
+ if (errorCodes.length === 0) return { actions: [], dispose() {} }
+ await this.updateFiles()
+ const fixes = await this.#request("codeFixes", {
+ end: model.getOffsetAt(range.getEndPosition()),
+ errorCodes,
+ fileName: model.uri.path,
+ formatOptions: await this.#formatOptions(model),
+ preferences: {},
+ start: model.getOffsetAt(range.getStartPosition()),
+ })
+ const actions = []
+ for (const fix of fixes) {
+ const edits = []
+ for (const change of fix.changes ?? []) {
+ const target = await this.#ensureModel(change.fileName)
+ if (!target) continue
+ for (const textChange of change.textChanges) {
+ edits.push({
+ resource: target.uri,
+ textEdit: {
+ range: spanToRange(target, textChange.span),
+ text: textChange.newText,
+ },
+ versionId: target.getVersionId(),
+ })
+ }
+ }
+ actions.push({
+ diagnostics: context.markers,
+ edit: { edits },
+ isPreferred: fix.fixId !== undefined,
+ kind: "quickfix",
+ title: fix.description,
+ })
+ }
+ return { actions, dispose() {} }
+ },
+ }),
+ monaco.languages.registerInlayHintsProvider(language, {
+ provideInlayHints: async (model, range) => {
+ await this.updateFiles()
+ const start = model.getOffsetAt(range.getStartPosition())
+ const end = model.getOffsetAt(range.getEndPosition())
+ const hints = await this.#request("inlayHints", {
+ fileName: model.uri.path,
+ preferences: {
+ includeInlayEnumMemberValueHints: true,
+ includeInlayFunctionLikeReturnTypeHints: true,
+ includeInlayFunctionParameterTypeHints: true,
+ includeInlayParameterNameHints: "literals",
+ includeInlayParameterNameHintsWhenArgumentMatchesName: false,
+ includeInlayPropertyDeclarationTypeHints: true,
+ includeInlayVariableTypeHints: true,
+ includeInlayVariableTypeHintsWhenTypeMatchesName: false,
+ },
+ span: { length: end - start, start },
+ })
+ return {
+ dispose() {},
+ hints: hints.map(hint => ({
+ kind:
+ hint.kind === "Type" ? monaco.languages.InlayHintKind.Type : monaco.languages.InlayHintKind.Parameter,
+ label: hint.text,
+ paddingLeft: hint.whitespaceBefore,
+ paddingRight: hint.whitespaceAfter,
+ position: model.getPositionAt(hint.position),
+ })),
+ }
+ },
+ })
+ )
+ }
+ }
+
+ async #ensureModel(fileName: string) {
+ const normalized = normalizePath(fileName)
+ const existing = monaco.editor.getModel(monaco.Uri.file(normalized))
+ if (existing) return existing
+ const text = await this.readFile(normalized)
+ if (text === undefined) return undefined
+ return monaco.editor.createModel(text, languageForFile(normalized), monaco.Uri.file(normalized))
+ }
+
+ #request(method: string, args: any): Promise {
+ const id = this.#nextId++
+ return new Promise((resolve, reject) => {
+ this.#pending.set(id, { resolve, reject })
+ this.#worker.postMessage({ id, method, args })
+ })
+ }
+}
+
+function displayParts(parts: readonly { text: string }[] | undefined) {
+ return parts?.map(part => part.text).join("") ?? ""
+}
+
+function spanToRange(model: monaco.editor.ITextModel, span: { start: number; length: number }) {
+ const start = model.getPositionAt(span.start)
+ const end = model.getPositionAt(span.start + span.length)
+ return new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column)
+}
+
+function navigationSymbols(model: monaco.editor.ITextModel, item: any): monaco.languages.DocumentSymbol[] {
+ const range = spanToRange(model, item.spans?.[0] ?? { start: 0, length: 0 })
+ return [
+ {
+ children: item.childItems?.flatMap((child: any) => navigationSymbols(model, child)) ?? [],
+ detail: "",
+ kind: symbolKind(item.kind),
+ name: item.text,
+ range,
+ selectionRange: range,
+ tags: [],
+ },
+ ]
+}
+
+function symbolKind(kind: string | undefined) {
+ switch (kind) {
+ case "class":
+ return monaco.languages.SymbolKind.Class
+ case "const":
+ case "let":
+ case "var":
+ return monaco.languages.SymbolKind.Variable
+ case "enum":
+ return monaco.languages.SymbolKind.Enum
+ case "enum member":
+ return monaco.languages.SymbolKind.EnumMember
+ case "function":
+ return monaco.languages.SymbolKind.Function
+ case "interface":
+ return monaco.languages.SymbolKind.Interface
+ case "method":
+ return monaco.languages.SymbolKind.Method
+ case "module":
+ return monaco.languages.SymbolKind.Module
+ case "property":
+ return monaco.languages.SymbolKind.Property
+ case "type":
+ return monaco.languages.SymbolKind.TypeParameter
+ default:
+ return monaco.languages.SymbolKind.Object
+ }
+}
+
+function completionKind(kind: string | undefined) {
+ switch (kind) {
+ case "class":
+ return monaco.languages.CompletionItemKind.Class
+ case "const":
+ case "let":
+ case "var":
+ return monaco.languages.CompletionItemKind.Variable
+ case "function":
+ return monaco.languages.CompletionItemKind.Function
+ case "interface":
+ return monaco.languages.CompletionItemKind.Interface
+ case "method":
+ return monaco.languages.CompletionItemKind.Method
+ case "property":
+ return monaco.languages.CompletionItemKind.Property
+ case "keyword":
+ return monaco.languages.CompletionItemKind.Keyword
+ default:
+ return monaco.languages.CompletionItemKind.Text
+ }
+}
+
+function languageForFile(fileName: string) {
+ if (/\.json$/i.test(fileName)) return "json"
+ if (/\.[cm]?jsx?$/i.test(fileName)) return "javascript"
+ return "typescript"
+}
+
+function normalizePath(fileName: string) {
+ return fileName.replaceAll("\\", "/").replace(/^file:\/\//, "")
+}
diff --git a/packages/playground-v2/src/strada.worker.ts b/packages/playground-v2/src/strada.worker.ts
new file mode 100644
index 000000000000..c1c14ce8a080
--- /dev/null
+++ b/packages/playground-v2/src/strada.worker.ts
@@ -0,0 +1,369 @@
+type TS = typeof import("typescript")
+
+type InitMessage = {
+ id: number
+ method: "init"
+ args: {
+ compilerSource: string
+ files: Record
+ version: string
+ baseUrl: string
+ }
+}
+
+type RequestMessage = {
+ id: number
+ method: string
+ args: any
+}
+
+type FileEntry = {
+ text: string
+ version: number
+}
+
+let ts: TS
+let version = ""
+let baseUrl = ""
+let compilerOptions: import("typescript").CompilerOptions = {}
+let projectVersion = 0
+const files = new Map()
+const libraries = new Map()
+let languageService: import("typescript").LanguageService
+
+self.addEventListener("message", (event: MessageEvent) => {
+ void handleMessage(event.data)
+})
+
+async function handleMessage(message: InitMessage | RequestMessage) {
+ try {
+ const result =
+ message.method === "init" ? await initialize(message.args) : await dispatch(message.method, message.args)
+ self.postMessage({ id: message.id, result })
+ } catch (error) {
+ self.postMessage({
+ id: message.id,
+ error: error instanceof Error ? error.stack ?? error.message : String(error),
+ })
+ }
+}
+
+async function initialize(args: InitMessage["args"]) {
+ ts = new Function(`${args.compilerSource}\nreturn ts;`)() as TS
+ version = args.version
+ baseUrl = args.baseUrl
+ replaceFiles(args.files)
+ refreshConfig()
+ await loadLibraries(compilerOptions)
+ languageService = ts.createLanguageService(createLanguageServiceHost())
+ return { version: ts.version }
+}
+
+async function dispatch(method: string, args: any) {
+ switch (method) {
+ case "updateFiles":
+ replaceFiles(args.files)
+ refreshConfig()
+ await loadLibraries(compilerOptions)
+ return
+ case "compile":
+ return compile()
+ case "quickInfo":
+ return languageService.getQuickInfoAtPosition(args.fileName, args.position)
+ case "completions":
+ return languageService.getCompletionsAtPosition(args.fileName, args.position, args.options)
+ case "completionDetails":
+ return languageService.getCompletionEntryDetails(
+ args.fileName,
+ args.position,
+ args.name,
+ {},
+ args.source,
+ {},
+ args.data
+ )
+ case "definitions":
+ return languageService.getDefinitionAndBoundSpan
+ ? languageService.getDefinitionAndBoundSpan(args.fileName, args.position)
+ : {
+ definitions: languageService.getDefinitionAtPosition(args.fileName, args.position),
+ }
+ case "references":
+ return languageService.getReferencesAtPosition(args.fileName, args.position)
+ case "occurrences": {
+ const service = languageService as import("typescript").LanguageService & {
+ getOccurrencesAtPosition?(fileName: string, position: number): readonly any[] | undefined
+ }
+ if (service.getOccurrencesAtPosition) {
+ return service.getOccurrencesAtPosition(args.fileName, args.position)
+ }
+ const highlights = service.getDocumentHighlights(args.fileName, args.position, [...files.keys()])
+ return (
+ highlights
+ ?.find(result => normalizePath(result.fileName) === normalizePath(args.fileName))
+ ?.highlightSpans.map(highlight => ({
+ isWriteAccess: highlight.kind === "writtenReference",
+ textSpan: highlight.textSpan,
+ })) ?? []
+ )
+ }
+ case "signatureHelp":
+ return languageService.getSignatureHelpItems(args.fileName, args.position, args.options)
+ case "renameInfo":
+ return languageService.getRenameInfo(args.fileName, args.position, {
+ allowRenameOfImportPath: true,
+ })
+ case "renameLocations":
+ return languageService.findRenameLocations(args.fileName, args.position, false, false, true)
+ case "navigationBarItems":
+ return languageService.getNavigationBarItems(args.fileName)
+ case "formatDocument":
+ return languageService.getFormattingEditsForDocument(args.fileName, args.options)
+ case "formatRange":
+ return languageService.getFormattingEditsForRange(args.fileName, args.start, args.end, args.options)
+ case "formatOnType":
+ return languageService.getFormattingEditsAfterKeystroke(args.fileName, args.position, args.key, args.options)
+ case "codeFixes":
+ return languageService.getCodeFixesAtPosition(
+ args.fileName,
+ args.start,
+ args.end,
+ args.errorCodes,
+ args.formatOptions,
+ args.preferences
+ )
+ case "inlayHints": {
+ const service = languageService as import("typescript").LanguageService & {
+ provideInlayHints?(
+ fileName: string,
+ span: import("typescript").TextSpan,
+ preferences: import("typescript").UserPreferences
+ ): readonly any[]
+ }
+ return service.provideInlayHints?.(args.fileName, args.span, args.preferences) ?? []
+ }
+ case "readFile":
+ return readFile(args.fileName)
+ default:
+ throw new Error(`Unknown Strada request: ${method}`)
+ }
+}
+
+function replaceFiles(nextFiles: Record) {
+ const nextNames = new Set(Object.keys(nextFiles))
+ for (const fileName of files.keys()) {
+ if (!nextNames.has(fileName)) files.delete(fileName)
+ }
+ for (const [fileName, text] of Object.entries(nextFiles)) {
+ const current = files.get(fileName)
+ if (!current || current.text !== text) {
+ files.set(fileName, {
+ text,
+ version: (current?.version ?? 0) + 1,
+ })
+ }
+ }
+ projectVersion++
+}
+
+function refreshConfig() {
+ const parsed = parseConfig()
+ compilerOptions = parsed.options
+ projectVersion++
+}
+
+function parseConfig() {
+ const configPath =
+ [...files.keys()].find(fileName => /\/tsconfig\.json$/i.test(fileName)) ??
+ [...files.keys()].find(fileName => /\/jsconfig\.json$/i.test(fileName))
+ const configText = configPath ? files.get(configPath)?.text ?? "{}" : "{}"
+ const configResult = ts.parseConfigFileTextToJson(configPath ?? "/workspace/tsconfig.json", configText)
+ const config = configResult.config ?? {}
+ const converted = ts.convertCompilerOptionsFromJson(config.compilerOptions ?? {}, "/workspace", configPath)
+ const options: import("typescript").CompilerOptions = {
+ ...converted.options,
+ allowJs: converted.options.allowJs ?? configPath?.endsWith("/jsconfig.json") ?? false,
+ checkJs: converted.options.checkJs ?? configPath?.endsWith("/jsconfig.json") ?? false,
+ }
+ const rootNames = getRootNames(config)
+ return {
+ errors: [
+ ...(configResult.error ? [serializeDiagnostic(configResult.error)] : []),
+ ...converted.errors.map(serializeDiagnostic),
+ ],
+ options,
+ rootNames,
+ }
+}
+
+function getRootNames(config: any) {
+ const sourceFiles = [...files.keys()].filter(
+ fileName => !fileName.includes("/node_modules/") && /\.[cm]?[jt]sx?$/i.test(fileName)
+ )
+ if (Array.isArray(config.files)) {
+ return config.files
+ .filter((fileName: unknown): fileName is string => typeof fileName === "string")
+ .map((fileName: string) => normalizePath(`/workspace/${fileName}`))
+ .filter((fileName: string) => files.has(fileName))
+ }
+ const includes = Array.isArray(config.include) ? config.include : ["**/*"]
+ const excludes = Array.isArray(config.exclude)
+ ? config.exclude
+ : ["node_modules", "bower_components", "jspm_packages"]
+ return sourceFiles.filter(fileName => {
+ const relative = fileName.replace(/^\/workspace\//, "")
+ return (
+ includes.some((pattern: string) => matchesGlob(relative, pattern)) &&
+ !excludes.some((pattern: string) => matchesGlob(relative, pattern))
+ )
+ })
+}
+
+function matchesGlob(fileName: string, pattern: string) {
+ const normalized = pattern.replace(/^\.\//, "")
+ const expression = normalized
+ .replaceAll("**/", "\u0000")
+ .replaceAll("**", "\u0001")
+ .replaceAll("*", "\u0002")
+ .replaceAll("?", "\u0003")
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
+ .replaceAll("\u0000", "(?:.*/)?")
+ .replaceAll("\u0001", ".*")
+ .replaceAll("\u0002", "[^/]*")
+ .replaceAll("\u0003", ".")
+ return new RegExp(`^${expression}$`, "i").test(fileName)
+}
+
+async function loadLibraries(options: import("typescript").CompilerOptions) {
+ if (options.noLib) return
+ const roots = options.lib?.length
+ ? options.lib.map(lib => (lib.toLowerCase().startsWith("lib.") ? lib : `lib.${lib}.d.ts`))
+ : [ts.getDefaultLibFileName(options)]
+ for (const root of roots) await loadLibrary(root)
+}
+
+async function loadLibrary(fileName: string) {
+ const normalized = fileName.startsWith("lib.") ? fileName : `lib.${fileName}.d.ts`
+ const path = `/typescript/lib/${normalized}`
+ if (libraries.has(path)) return
+ const response = await fetch(`${baseUrl}${normalized}`)
+ if (!response.ok) {
+ if (response.status === 404) return
+ throw new Error(`Could not load ${normalized}: ${response.status}`)
+ }
+ const text = await response.text()
+ libraries.set(path, text)
+ const references = [...text.matchAll(/ {
+ const referenced = match[1]
+ return referenced.endsWith(".d.ts") ? referenced.split("/").at(-1)! : `lib.${referenced}.d.ts`
+ })
+ for (const reference of references) await loadLibrary(reference)
+}
+
+function compile() {
+ const parsed = parseConfig()
+ compilerOptions = parsed.options
+ const outputFiles: Record = {}
+ const host = createCompilerHost(outputFiles)
+ const program = ts.createProgram(parsed.rootNames, parsed.options, host)
+ const emit = program.emit()
+ const diagnostics = [
+ ...parsed.errors,
+ ...ts.getPreEmitDiagnostics(program).map(serializeDiagnostic),
+ ...emit.diagnostics.map(serializeDiagnostic),
+ ]
+ return {
+ diagnostics,
+ emitSkipped: emit.emitSkipped,
+ outputFiles,
+ rootNames: parsed.rootNames,
+ }
+}
+
+function createCompilerHost(outputFiles: Record) {
+ const host: import("typescript").CompilerHost = {
+ directoryExists: directoryName =>
+ [...files.keys(), ...libraries.keys()].some(fileName =>
+ fileName.startsWith(`${normalizePath(directoryName).replace(/\/$/, "")}/`)
+ ),
+ fileExists: fileName => readFile(fileName) !== undefined,
+ getCanonicalFileName: fileName => fileName,
+ getCurrentDirectory: () => "/workspace",
+ getDefaultLibFileName: options => `/typescript/lib/${ts.getDefaultLibFileName(options)}`,
+ getDirectories: directoryName => {
+ const prefix = `${normalizePath(directoryName).replace(/\/$/, "")}/`
+ return [
+ ...new Set(
+ [...files.keys(), ...libraries.keys()]
+ .filter(fileName => fileName.startsWith(prefix))
+ .map(fileName => fileName.slice(prefix.length).split("/")[0])
+ .filter(name => name && !name.includes("."))
+ ),
+ ]
+ },
+ getNewLine: () => "\n",
+ getSourceFile: (fileName, languageVersion) => {
+ const text = readFile(fileName)
+ return text === undefined
+ ? undefined
+ : ts.createSourceFile(fileName, text, languageVersion, true, scriptKind(fileName))
+ },
+ readFile,
+ useCaseSensitiveFileNames: () => true,
+ writeFile: (fileName, text) => {
+ outputFiles[normalizePath(fileName)] = text
+ },
+ }
+ return host
+}
+
+function createLanguageServiceHost(): import("typescript").LanguageServiceHost {
+ return {
+ fileExists: fileName => readFile(fileName) !== undefined,
+ getCompilationSettings: () => ({
+ ...compilerOptions,
+ allowJs: true,
+ }),
+ getCurrentDirectory: () => "/workspace",
+ getDefaultLibFileName: options => `/typescript/lib/${ts.getDefaultLibFileName(options)}`,
+ getProjectVersion: () => String(projectVersion),
+ getScriptFileNames: () => [...files.keys(), ...libraries.keys()],
+ getScriptKind: scriptKind,
+ getScriptSnapshot: fileName => {
+ const text = readFile(fileName)
+ return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text)
+ },
+ getScriptVersion: fileName => files.get(normalizePath(fileName))?.version.toString() ?? "1",
+ readFile,
+ readDirectory: () => [...files.keys()],
+ useCaseSensitiveFileNames: () => true,
+ }
+}
+
+function readFile(fileName: string) {
+ const normalized = normalizePath(fileName)
+ return files.get(normalized)?.text ?? libraries.get(normalized)
+}
+
+function scriptKind(fileName: string) {
+ if (/\.tsx$/i.test(fileName)) return ts.ScriptKind.TSX
+ if (/\.jsx$/i.test(fileName)) return ts.ScriptKind.JSX
+ if (/\.[cm]?js$/i.test(fileName)) return ts.ScriptKind.JS
+ if (/\.json$/i.test(fileName)) return ts.ScriptKind.JSON
+ return ts.ScriptKind.TS
+}
+
+function normalizePath(fileName: string) {
+ return fileName.replaceAll("\\", "/").replace(/^file:\/\//, "")
+}
+
+function serializeDiagnostic(diagnostic: import("typescript").Diagnostic) {
+ return {
+ category: diagnostic.category,
+ code: diagnostic.code,
+ end: (diagnostic.start ?? 0) + (diagnostic.length ?? 1),
+ fileName: diagnostic.file?.fileName,
+ pos: diagnostic.start ?? 0,
+ text: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
+ }
+}
diff --git a/packages/playground-v2/src/styles.css b/packages/playground-v2/src/styles.css
new file mode 100644
index 000000000000..49476e50f0af
--- /dev/null
+++ b/packages/playground-v2/src/styles.css
@@ -0,0 +1,1861 @@
+@import "@fontsource/nunito-sans/400.css";
+@import "@fontsource/nunito-sans/600.css";
+@import "hack-font/build/web/hack.css";
+
+:root {
+ color: #1f1f1f;
+ background: #fafafa;
+ font-family: "Nunito Sans", sans-serif;
+ font-synthesis: none;
+ --accent: #1d5f9e;
+ --border: #d6d6d6;
+ --hover: #eeeeee;
+ --menu-text: #1f1f1f;
+ --muted: #5f6368;
+ --output: #f5f5f5;
+ --panel: #ffffff;
+ --selected: #e4e6f1;
+ --sidebar: #f3f3f3;
+ --toolbar: #3178c6;
+ --tree-guide: #b7b7b7;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+}
+
+body {
+ display: grid;
+ grid-template-rows: minmax(0, 1fr) auto;
+ overflow: hidden;
+}
+
+.playground {
+ width: 100%;
+ height: auto;
+ min-height: 0;
+ margin: 0;
+ position: relative;
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr);
+}
+
+.toolbar {
+ min-height: 4.5rem;
+ padding: 0.75rem 1rem;
+ color: white;
+ background: var(--toolbar);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.toolbar h1,
+.toolbar p,
+.panel-heading h2 {
+ margin: 0;
+}
+
+.toolbar h1 {
+ font-size: 1.25rem;
+ font-weight: 600;
+}
+
+.toolbar h1 a {
+ color: inherit;
+ text-decoration: none;
+}
+
+.toolbar-brand {
+ min-width: 0;
+ display: grid;
+ gap: 0.2rem;
+}
+
+.site-links {
+ display: flex;
+ gap: 0.75rem;
+ font-size: 0.72rem;
+}
+
+.site-links a {
+ min-height: 1.5rem;
+ display: inline-flex;
+ align-items: center;
+ color: inherit;
+ text-decoration: none;
+}
+
+.site-links a:hover {
+ text-decoration: underline;
+}
+
+.site-footer {
+ min-height: 2.4rem;
+ padding: 0.45rem 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ border-top: 1px solid var(--border);
+ color: var(--muted);
+ background: var(--sidebar);
+ font-size: 0.72rem;
+}
+
+.site-footer > a {
+ color: inherit;
+ font-weight: 600;
+ text-decoration: none;
+}
+
+.site-footer nav {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 0.75rem;
+}
+
+.site-footer nav a {
+ color: inherit;
+}
+
+.toolbar p {
+ margin-top: 0.125rem;
+ font-size: 0.875rem;
+ opacity: 0.85;
+}
+
+.toolbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.version-control {
+ position: relative;
+ min-width: 7rem;
+ max-width: 12rem;
+}
+
+.version-control summary {
+ min-height: 2.15rem;
+ padding: 0.4rem 0.65rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.65rem;
+ overflow: hidden;
+ border: 1px solid rgb(255 255 255 / 45%);
+ border-radius: 0.25rem;
+ color: white;
+ background: rgb(0 0 0 / 16%);
+ cursor: pointer;
+ font: inherit;
+ font-weight: 600;
+ list-style: none;
+ white-space: nowrap;
+}
+
+.version-control summary::-webkit-details-marker {
+ display: none;
+}
+
+.version-control summary:hover,
+.version-control[open] summary {
+ background: rgb(0 0 0 / 24%);
+}
+
+.version-control summary[aria-disabled="true"] {
+ pointer-events: none;
+ opacity: 0.55;
+}
+
+.version-caret {
+ width: 0.8rem;
+ height: 0.8rem;
+ flex: 0 0 auto;
+ display: grid;
+ place-items: center;
+ transition: transform 120ms ease;
+}
+
+.version-caret::before {
+ content: "▾";
+ line-height: 1;
+}
+
+.version-control[open] .version-caret {
+ transform: rotate(180deg);
+}
+
+.version-menu {
+ position: absolute;
+ top: calc(100% + 0.3rem);
+ left: 0;
+ z-index: 60;
+ width: max(100%, 10rem);
+ max-height: min(30rem, calc(100dvh - 6rem));
+ padding: 0.3rem 0;
+ overflow-y: auto;
+ border: 1px solid var(--border);
+ border-radius: 0.3rem;
+ color: var(--menu-text);
+ background: var(--panel);
+ box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 28%);
+}
+
+.version-menu-item {
+ width: 100%;
+ min-height: 2.4rem;
+ padding: 0.45rem 1.9rem 0.45rem 0.7rem !important;
+ position: relative;
+ display: block;
+ overflow: hidden;
+ border: 0 !important;
+ border-radius: 0 !important;
+ color: inherit !important;
+ background: transparent !important;
+ font-size: 0.78rem !important;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.version-menu-item:hover,
+.version-menu-item:focus {
+ background: var(--hover) !important;
+ outline: 0;
+}
+
+.version-menu-item[aria-checked="true"] {
+ color: var(--accent) !important;
+ background: var(--selected) !important;
+ font-weight: 700;
+}
+
+.version-menu-item[aria-checked="true"]::after {
+ position: absolute;
+ top: 50%;
+ right: 0.65rem;
+ content: "✓";
+ transform: translateY(-50%);
+}
+
+.version-menu-separator {
+ height: 1px;
+ margin: 0.3rem 0;
+ background: var(--border);
+}
+
+.toolbar button,
+.run-output button {
+ border: 1px solid rgb(255 255 255 / 45%);
+ border-radius: 0.25rem;
+ color: inherit;
+ background: rgb(0 0 0 / 12%);
+ cursor: pointer;
+ font: inherit;
+ font-weight: 600;
+}
+
+.toolbar button {
+ padding: 0.4rem 0.8rem;
+}
+
+.toolbar button:hover:not(:disabled) {
+ background: rgb(0 0 0 / 22%);
+}
+
+.toolbar button:disabled {
+ cursor: wait;
+ opacity: 0.5;
+}
+
+.status {
+ padding: 0.35rem 0.65rem;
+ border: 1px solid rgb(255 255 255 / 35%);
+ border-radius: 0.25rem;
+ font-family: Hack, monospace;
+ font-size: 0.75rem;
+ white-space: nowrap;
+}
+
+.status[data-state="ready"] {
+ background: rgb(0 0 0 / 12%);
+}
+
+.status[data-state="error"] {
+ background: #a1260d;
+}
+
+.resources-dialog {
+ width: min(48rem, calc(100vw - 2rem));
+ height: min(42rem, calc(100dvh - 2rem));
+ padding: 0;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ border-radius: 0.4rem;
+ color: inherit;
+ background: var(--panel);
+ box-shadow: 0 1rem 3rem rgb(0 0 0 / 35%);
+}
+
+.form-dialog {
+ width: min(28rem, calc(100vw - 2rem));
+ padding: 0;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ border-radius: 0.4rem;
+ color: inherit;
+ background: var(--panel);
+ box-shadow: 0 1rem 3rem rgb(0 0 0 / 35%);
+}
+
+.form-dialog::backdrop {
+ background: rgb(0 0 0 / 45%);
+}
+
+.form-dialog form {
+ display: grid;
+ gap: 0.8rem;
+}
+
+.form-dialog header {
+ padding: 0.75rem 0.9rem;
+ border-bottom: 1px solid var(--border);
+ background: var(--sidebar);
+}
+
+.form-dialog h2 {
+ margin: 0;
+ font-size: 1rem;
+}
+
+.form-dialog label {
+ padding: 0 0.9rem;
+ display: grid;
+ gap: 0.35rem;
+ color: var(--muted);
+ font-size: 0.78rem;
+ font-weight: 600;
+}
+
+.form-dialog input {
+ width: 100%;
+ min-height: 2.5rem;
+ padding: 0 0.6rem;
+ border: 1px solid var(--border);
+ border-radius: 0.25rem;
+ color: inherit;
+ background: var(--panel);
+ font-family: Hack, monospace;
+}
+
+.form-dialog footer {
+ padding: 0.7rem 0.9rem;
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+ border-top: 1px solid var(--border);
+}
+
+.form-dialog button {
+ min-height: 2.25rem;
+ padding: 0 0.75rem;
+ border: 1px solid var(--border);
+ border-radius: 0.25rem;
+ color: inherit;
+ background: var(--panel);
+ cursor: pointer;
+ font: inherit;
+ font-weight: 600;
+}
+
+.form-dialog button[type="submit"] {
+ color: white;
+ background: var(--toolbar);
+}
+
+.form-dialog button.danger {
+ color: white;
+ background: #a1260d;
+}
+
+.form-error {
+ margin: 0;
+ padding: 0 0.9rem;
+ color: #c63131;
+ font-size: 0.78rem;
+}
+
+.settings-dialog {
+ width: min(34rem, calc(100vw - 2rem));
+ max-height: calc(100dvh - 2rem);
+ overflow: auto;
+}
+
+.settings-dialog fieldset {
+ margin: 0 0.9rem;
+ padding: 0.75rem;
+ display: grid;
+ gap: 0.7rem;
+ border: 1px solid var(--border);
+ border-radius: 0.3rem;
+}
+
+.settings-dialog legend {
+ padding: 0 0.35rem;
+ font-size: 0.8rem;
+ font-weight: 600;
+}
+
+.settings-dialog .checkbox-setting {
+ padding: 0;
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ align-items: start;
+ gap: 0.55rem;
+ color: inherit;
+ font-weight: 400;
+}
+
+.settings-dialog .checkbox-setting input {
+ width: 1rem;
+ height: 1rem;
+ min-height: 0;
+ margin-top: 0.15rem;
+}
+
+.settings-dialog .checkbox-setting span {
+ display: grid;
+ gap: 0.1rem;
+}
+
+.settings-dialog .checkbox-setting small {
+ color: var(--muted);
+ font-size: 0.72rem;
+}
+
+.settings-dialog .select-setting {
+ padding: 0;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 0.75rem;
+ color: inherit;
+ font-weight: 400;
+}
+
+.settings-dialog select {
+ min-height: 2.25rem;
+ padding: 0 0.45rem;
+ border: 1px solid var(--border);
+ border-radius: 0.25rem;
+ color: inherit;
+ background: var(--panel);
+ font: inherit;
+}
+
+.settings-dialog footer span {
+ flex: 1;
+}
+
+.settings-dialog .form-secondary-action {
+ margin-right: auto;
+}
+
+.confirmation-dialog p {
+ margin: 0;
+ padding: 0.2rem 0.9rem 0.4rem;
+ color: var(--muted);
+ line-height: 1.5;
+}
+
+.resources-dialog::backdrop {
+ background: rgb(0 0 0 / 45%);
+}
+
+.resources-dialog > header {
+ min-height: 3.25rem;
+ padding: 0.65rem 0.8rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ border-bottom: 1px solid var(--border);
+ background: var(--sidebar);
+}
+
+.resources-dialog h2 {
+ margin: 0;
+ font-size: 1rem;
+}
+
+.resources-dialog button,
+.resources-dialog a {
+ color: inherit;
+ font: inherit;
+}
+
+.resources-dialog > header button,
+.help-document nav button {
+ min-height: 2rem;
+ padding: 0 0.55rem;
+ border: 1px solid var(--border);
+ border-radius: 0.25rem;
+ background: var(--panel);
+ cursor: pointer;
+}
+
+.resources-view {
+ height: calc(100% - 3.25rem);
+ min-height: 0;
+}
+
+.resource-search {
+ padding: 0.7rem 0.8rem;
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ align-items: center;
+ gap: 0.65rem;
+ border-bottom: 1px solid var(--border);
+ color: var(--muted);
+ font-size: 0.78rem;
+ font-weight: 600;
+}
+
+.resource-search input {
+ min-width: 0;
+ height: 2.25rem;
+ padding: 0 0.6rem;
+ border: 1px solid var(--border);
+ border-radius: 0.25rem;
+ color: inherit;
+ background: var(--panel);
+ font: inherit;
+}
+
+.resource-list {
+ height: 100%;
+ padding: 0.4rem;
+ overflow: auto;
+}
+
+.resource-search + .resource-list {
+ height: calc(100% - 3.7rem);
+}
+
+.resource-list > button,
+.resource-list > a {
+ width: 100%;
+ padding: 0.65rem 0.7rem;
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 1rem;
+ border: 0;
+ border-bottom: 1px solid var(--border);
+ color: inherit;
+ background: transparent;
+ cursor: pointer;
+ text-align: left;
+ text-decoration: none;
+}
+
+.resource-list > button:hover,
+.resource-list > a:hover {
+ background: var(--hover);
+}
+
+.resource-list small {
+ color: var(--muted);
+ font-size: 0.68rem;
+ text-align: right;
+}
+
+.resource-list > .empty-message,
+.resource-error {
+ padding: 0.75rem;
+}
+
+.resource-error {
+ color: #c63131;
+}
+
+.help-document {
+ height: 100%;
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr);
+}
+
+.help-document nav {
+ min-height: 3rem;
+ padding: 0.45rem 0.65rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.help-document nav a {
+ font-size: 0.75rem;
+}
+
+.help-document article {
+ padding: 1rem;
+ overflow: auto;
+ font-size: 0.9rem;
+ line-height: 1.55;
+}
+
+.help-document article p {
+ max-width: 42rem;
+ margin: 0 0 1rem;
+}
+
+.help-document article code,
+.help-document article kbd {
+ padding: 0.1rem 0.25rem;
+ border-radius: 0.2rem;
+ background: var(--sidebar);
+ font-family: Hack, monospace;
+ font-size: 0.82em;
+}
+
+.workspace {
+ --files-handle-width: 0.3rem;
+ --files-width: 13rem;
+ --output-handle-width: 0.3rem;
+ --output-width: 27.5rem;
+ min-height: 0;
+ display: grid;
+ grid-template-columns:
+ var(--files-width)
+ var(--files-handle-width)
+ minmax(24rem, 1fr)
+ var(--output-handle-width)
+ var(--output-width);
+}
+
+.workspace[data-files-collapsed="true"] {
+ --files-handle-width: 0rem;
+ --files-width: 0rem;
+}
+
+.workspace[data-output-collapsed="true"] {
+ --output-handle-width: 0rem;
+ --output-width: 0rem;
+}
+
+.workspace[data-files-collapsed="true"] .file-explorer,
+.workspace[data-files-collapsed="true"] .file-resizer,
+.workspace[data-output-collapsed="true"] .output-panel,
+.workspace[data-output-collapsed="true"] .output-resizer {
+ display: none;
+}
+
+.file-explorer {
+ grid-column: 1;
+ min-width: 0;
+ overflow: auto;
+ border-right: 1px solid var(--border);
+ background: var(--sidebar);
+}
+
+.file-explorer-heading,
+.panel-heading {
+ min-height: 2.75rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.file-explorer-heading {
+ padding: 0.5rem 0.75rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+}
+
+.file-explorer-heading h2,
+.file-explorer-heading span {
+ margin: 0;
+}
+
+.file-explorer-heading h2 {
+ font-size: 0.8rem;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.file-explorer-heading span {
+ display: block;
+ margin-top: 0.1rem;
+ color: var(--muted);
+ font-family: Hack, monospace;
+ font-size: 0.65rem;
+}
+
+.file-explorer-actions {
+ display: flex;
+ gap: 0.25rem;
+}
+
+.file-explorer-actions button {
+ height: 1.7rem;
+ padding: 0 0.4rem;
+ border: 1px solid var(--border);
+ border-radius: 0.2rem;
+ color: inherit;
+ background: var(--panel);
+ cursor: pointer;
+ font: inherit;
+ font-size: 0.68rem;
+ line-height: 1;
+}
+
+.file-explorer-actions button:hover {
+ background: var(--hover);
+}
+
+.file-list {
+ padding: 0.4rem 0;
+}
+
+.file-tree,
+.file-tree ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.file-tree .file-tree {
+ margin-left: 1rem;
+ border-left: 1px solid var(--tree-guide);
+}
+
+.file-tree .file-tree > li {
+ position: relative;
+}
+
+.file-tree .file-tree > li::before {
+ position: absolute;
+ top: 1rem;
+ left: 0;
+ width: 0.65rem;
+ border-top: 1px solid var(--tree-guide);
+ content: "";
+}
+
+.file-tree-folder {
+ padding: 0.35rem 0.65rem 0.35rem 0.85rem;
+ color: inherit;
+ cursor: pointer;
+ font-size: 0.76rem;
+ font-weight: 600;
+ list-style: none;
+ user-select: none;
+}
+
+.file-tree-folder::before {
+ display: inline-block;
+ width: 0.8rem;
+ margin-right: 0.35rem;
+ color: var(--muted);
+ content: "▾";
+}
+
+.file-tree details:not([open]) > .file-tree-folder::before {
+ content: "▸";
+}
+
+.file-tree-file {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+}
+
+.file-tree-file-button {
+ width: 100%;
+ padding: 0.42rem 0.65rem 0.42rem 0.85rem;
+ overflow: hidden;
+ border: 0;
+ color: inherit;
+ background: transparent;
+ cursor: pointer;
+ font: inherit;
+ font-size: 0.8rem;
+ text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.file-tree-file-button::before {
+ display: inline-block;
+ width: 1.65rem;
+ color: var(--muted);
+ font-family: Hack, monospace;
+ font-size: 0.65rem;
+ content: attr(data-kind);
+}
+
+.file-tree-file-button:hover {
+ background: var(--hover);
+}
+
+.file-tree-file-button[aria-current="page"] {
+ background: var(--selected);
+ font-weight: 600;
+}
+
+.file-tree-delete {
+ width: 1.7rem;
+ height: 1.7rem;
+ margin-right: 0.3rem;
+ padding: 0;
+ border: 0;
+ border-radius: 0.2rem;
+ color: var(--muted);
+ background: transparent;
+ cursor: pointer;
+ font: inherit;
+ font-size: 1rem;
+ line-height: 1;
+ opacity: 0;
+}
+
+.file-tree-file:hover .file-tree-delete,
+.file-tree-file:focus-within .file-tree-delete {
+ opacity: 1;
+}
+
+.file-tree-delete:hover {
+ color: #c63131;
+ background: var(--hover);
+}
+
+.editor-panel {
+ grid-column: 3;
+ min-width: 0;
+ min-height: 0;
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr);
+ background: var(--panel);
+}
+
+.output-panel {
+ grid-column: 5;
+ min-width: 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ border-left: 1px solid var(--border);
+ background: var(--panel);
+}
+
+.workspace-resizer {
+ position: relative;
+ z-index: 4;
+ min-width: 0;
+ background: var(--border);
+ cursor: col-resize;
+ touch-action: none;
+}
+
+.workspace-resizer:hover,
+.workspace-resizer:focus,
+body[data-resizing] .workspace-resizer {
+ background: var(--toolbar);
+ outline: 0;
+}
+
+.file-resizer {
+ grid-column: 2;
+}
+
+.output-resizer {
+ grid-column: 4;
+}
+
+body[data-resizing] {
+ cursor: col-resize;
+ user-select: none;
+}
+
+.diagnostics-panel {
+ order: 1;
+ flex: 0 0 auto;
+ min-height: 0;
+ border-bottom: 1px solid var(--border);
+ background: var(--panel);
+}
+
+.diagnostics-panel summary {
+ min-height: 2.4rem;
+ padding: 0.45rem 0.65rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ background: var(--sidebar);
+ cursor: pointer;
+ font-size: 0.78rem;
+ font-weight: 600;
+ list-style: none;
+}
+
+.diagnostics-panel summary::-webkit-details-marker {
+ display: none;
+}
+
+.diagnostics-panel summary::before {
+ width: 0.75rem;
+ color: var(--muted);
+ content: "▸";
+}
+
+.diagnostics-panel[open] summary::before {
+ content: "▾";
+}
+
+.diagnostics-panel summary > :first-child {
+ margin-right: auto;
+}
+
+.diagnostics-panel summary > :last-child {
+ min-width: 1.5rem;
+ padding: 0.1rem 0.35rem;
+ border-radius: 1rem;
+ background: var(--selected);
+ font-family: Hack, monospace;
+ font-size: 0.68rem;
+ text-align: center;
+}
+
+.diagnostics-list {
+ max-height: 13rem;
+ overflow: auto;
+}
+
+.diagnostics-list > .empty-message {
+ padding: 0.65rem;
+}
+
+.diagnostic {
+ width: 100%;
+ padding: 0.55rem 0.65rem;
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ gap: 0.15rem 0.55rem;
+ border: 0;
+ border-bottom: 1px solid var(--border);
+ color: inherit;
+ background: transparent;
+ cursor: pointer;
+ font: inherit;
+ text-align: left;
+}
+
+.diagnostic:last-child {
+ border-bottom: 0;
+}
+
+.diagnostic:hover {
+ background: var(--hover);
+}
+
+.diagnostic strong {
+ color: var(--muted);
+ font-family: Hack, monospace;
+ font-size: 0.68rem;
+}
+
+.diagnostic-error strong {
+ color: #c63131;
+}
+
+.diagnostic-warning strong {
+ color: #b26a00;
+}
+
+.diagnostic-message {
+ min-width: 0;
+ overflow: hidden;
+ font-size: 0.76rem;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.diagnostic-location {
+ grid-column: 2;
+ color: var(--muted);
+ font-family: Hack, monospace;
+ font-size: 0.65rem;
+}
+
+.panel-heading {
+ padding: 0.55rem 0.75rem;
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.output-panel > .panel-heading {
+ order: 0;
+ flex: 0 0 auto;
+}
+
+.panel-heading h2 {
+ font-size: 0.95rem;
+ font-weight: 600;
+}
+
+.editor-heading-title,
+.editor-heading-actions,
+.editor-navigation {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+}
+
+.editor-heading-title {
+ gap: 0.55rem;
+}
+
+.editor-heading-title h2 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.editor-heading-actions {
+ justify-content: flex-end;
+ gap: 0.55rem;
+}
+
+.editor-navigation {
+ flex: 0 0 auto;
+ gap: 0.2rem;
+}
+
+.editor-navigation button,
+.editor-heading-actions button,
+.output-heading-actions button {
+ min-height: 1.8rem;
+ padding: 0 0.45rem;
+ border: 1px solid var(--border);
+ border-radius: 0.2rem;
+ color: inherit;
+ background: var(--panel);
+ cursor: pointer;
+ font: inherit;
+ font-size: 0.72rem;
+ line-height: 1;
+}
+
+.editor-navigation button {
+ width: 1.8rem;
+ padding: 0;
+ font-family: Hack, monospace;
+ font-size: 0.9rem;
+}
+
+.editor-navigation button:hover:not(:disabled),
+.editor-heading-actions button:hover:not(:disabled),
+.output-heading-actions button:hover:not(:disabled) {
+ background: var(--hover);
+}
+
+.editor-navigation button:disabled,
+.editor-heading-actions button:disabled,
+.output-heading-actions button:disabled {
+ cursor: default;
+ opacity: 0.45;
+}
+
+.panel-heading span {
+ color: var(--muted);
+ font-size: 0.75rem;
+ text-align: right;
+}
+
+.output-heading-actions,
+.output-section-title,
+.run-output header > div {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 0.4rem;
+}
+
+.output-section-title {
+ justify-content: flex-start;
+ gap: 0.35rem;
+}
+
+.output-section-toggle {
+ width: 1.4rem;
+ min-height: 1.4rem;
+ padding: 0;
+ border: 0;
+ color: var(--muted);
+ background: transparent;
+ cursor: pointer;
+ font: inherit;
+ font-size: 0.85rem;
+ line-height: 1;
+}
+
+.output-section-toggle:hover {
+ color: inherit;
+ background: var(--hover);
+}
+
+code {
+ font-family: Hack, monospace;
+}
+
+.editor {
+ min-width: 0;
+ min-height: 0;
+}
+
+.emit-output {
+ order: 2;
+ flex: 1 1 auto;
+ min-height: 0;
+ padding: 0.75rem;
+ overflow: auto;
+ background: var(--output);
+}
+
+.emit-file {
+ margin-bottom: 0.75rem;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ border-radius: 0.25rem;
+ background: var(--panel);
+}
+
+.emit-file:last-child {
+ margin-bottom: 0;
+}
+
+.emit-file h3 {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ margin: 0;
+ padding: 0.45rem 0.65rem;
+ border-bottom: 1px solid var(--border);
+ background: var(--sidebar);
+ font-family: Hack, monospace;
+ font-size: 0.72rem;
+ font-weight: 600;
+}
+
+.emit-file pre {
+ margin: 0;
+ padding: 0.75rem;
+ overflow: auto;
+ font-family: Hack, monospace;
+ font-size: 0.75rem;
+ line-height: 1.5;
+ tab-size: 2;
+}
+
+.empty-message {
+ margin: 0;
+ color: var(--muted);
+ font-size: 0.8rem;
+}
+
+.run-output {
+ order: 3;
+ flex: 0 0 auto;
+ max-height: 12rem;
+ overflow: auto;
+ border-top: 1px solid var(--border);
+ background: var(--panel);
+}
+
+.run-output[data-collapsed="true"] .run-log {
+ display: none;
+}
+
+.output-panel:has(.emit-output[hidden]) .run-output:not([data-collapsed="true"]) {
+ max-height: none;
+ flex: 1 1 auto;
+}
+
+.run-output header {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.4rem 0.65rem;
+ border-bottom: 1px solid var(--border);
+ background: var(--sidebar);
+}
+
+.run-output h2 {
+ margin: 0;
+ font-size: 0.78rem;
+}
+
+.run-output button {
+ padding: 0.2rem 0.45rem;
+ border-color: var(--border);
+ color: inherit;
+ background: var(--panel);
+ font-size: 0.7rem;
+}
+
+.run-output .output-section-toggle {
+ width: 1.4rem;
+ min-height: 1.4rem;
+ padding: 0;
+ border: 0;
+ color: var(--muted);
+ background: transparent;
+ font-size: 0.85rem;
+}
+
+.run-log {
+ padding: 0.65rem;
+ font-family: Hack, monospace;
+ font-size: 0.72rem;
+ line-height: 1.45;
+ white-space: pre-wrap;
+}
+
+.run-log-entry + .run-log-entry {
+ margin-top: 0.35rem;
+ padding-top: 0.35rem;
+ border-top: 1px dashed var(--border);
+}
+
+.run-log-entry strong {
+ display: inline-block;
+ width: 2.5rem;
+ color: var(--muted);
+}
+
+.run-log-entry.warn strong {
+ color: #b26a00;
+}
+
+.run-log-entry.error strong {
+ color: #c63131;
+}
+
+.loading-overlay {
+ position: absolute;
+ top: 5.25rem;
+ right: 1rem;
+ z-index: 20;
+ pointer-events: none;
+}
+
+.loading-overlay[data-state="consent"] {
+ position: fixed;
+ inset: 0;
+ padding: 1rem;
+ display: grid;
+ place-items: center;
+ pointer-events: none;
+ transform: none;
+}
+
+.loading-overlay[data-state="consent"] .loading-card {
+ width: min(30rem, calc(100vw - 2rem));
+ pointer-events: auto;
+}
+
+.loading-card {
+ display: flex;
+ align-items: center;
+ gap: 0.85rem;
+ width: min(25rem, calc(100vw - 2rem));
+ padding: 0.8rem 0.9rem;
+ border: 1px solid var(--border);
+ border-radius: 0.4rem;
+ background: color-mix(in srgb, var(--panel) 92%, transparent);
+ box-shadow: 0 0.45rem 1.5rem rgb(0 0 0 / 22%);
+ backdrop-filter: blur(8px);
+}
+
+.loading-overlay[hidden] {
+ display: none;
+}
+
+.loading-overlay p {
+ margin: 0 0 0.45rem;
+ font-size: 0.82rem;
+ font-weight: 600;
+}
+
+.loading-copy {
+ min-width: 0;
+ flex: 1;
+}
+
+.download-consent {
+ flex: 1;
+}
+
+.download-consent h2 {
+ margin: 0 0 0.45rem;
+ font-size: 1rem;
+}
+
+.download-consent p {
+ margin: 0 0 0.75rem;
+ color: var(--muted);
+ font-size: 0.8rem;
+ line-height: 1.4;
+}
+
+.download-consent label {
+ margin-bottom: 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ font-size: 0.75rem;
+}
+
+.download-consent input {
+ width: 1rem;
+ height: 1rem;
+}
+
+.download-consent button {
+ min-height: 2.25rem;
+ padding: 0 0.75rem;
+ border: 0;
+ border-radius: 0.25rem;
+ color: white;
+ background: var(--toolbar);
+ cursor: pointer;
+ font: inherit;
+ font-weight: 600;
+}
+
+.loading-overlay progress {
+ display: block;
+ width: 100%;
+ height: 0.65rem;
+ accent-color: #3178c6;
+}
+
+.loading-overlay[data-state="error"] progress {
+ accent-color: #c63131;
+}
+
+.loading-overlay small {
+ display: block;
+ min-height: 1rem;
+ margin-top: 0.35rem;
+ overflow: hidden;
+ color: var(--muted);
+ font-family: Hack, monospace;
+ font-size: 0.68rem;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.lds-grid {
+ position: relative;
+ width: 48px;
+ height: 48px;
+ flex: 0 0 48px;
+}
+
+.lds-grid div {
+ position: absolute;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: #3178c6;
+ animation: lds-grid 1.2s linear infinite;
+}
+
+.loading-overlay[data-state="error"] .lds-grid div {
+ background: #c63131;
+ animation: none;
+}
+
+.lds-grid div:nth-child(1) {
+ top: 3px;
+ left: 3px;
+ animation-delay: 0s;
+}
+.lds-grid div:nth-child(2) {
+ top: 3px;
+ left: 19px;
+ animation-delay: -0.4s;
+}
+.lds-grid div:nth-child(3) {
+ top: 3px;
+ left: 35px;
+ animation-delay: -0.8s;
+}
+.lds-grid div:nth-child(4) {
+ top: 19px;
+ left: 3px;
+ animation-delay: -0.4s;
+}
+.lds-grid div:nth-child(5) {
+ top: 19px;
+ left: 19px;
+ animation-delay: -0.8s;
+}
+.lds-grid div:nth-child(6) {
+ top: 19px;
+ left: 35px;
+ animation-delay: -1.2s;
+}
+.lds-grid div:nth-child(7) {
+ top: 35px;
+ left: 3px;
+ animation-delay: -0.8s;
+}
+.lds-grid div:nth-child(8) {
+ top: 35px;
+ left: 19px;
+ animation-delay: -1.2s;
+}
+.lds-grid div:nth-child(9) {
+ top: 35px;
+ left: 35px;
+ animation-delay: -1.6s;
+}
+
+@keyframes lds-grid {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+@media (max-width: 1000px) {
+ .toolbar {
+ min-height: 0;
+ flex-wrap: wrap;
+ }
+
+ .toolbar-actions {
+ min-width: 0;
+ flex: 1 1 34rem;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ }
+
+ .workspace {
+ grid-template-columns: 11rem minmax(20rem, 1fr);
+ grid-template-rows: minmax(20rem, 1fr) minmax(16rem, 0.8fr);
+ }
+
+ .file-explorer {
+ grid-column: 1;
+ grid-row: 1;
+ }
+
+ .workspace-resizer {
+ display: none;
+ }
+
+ .editor-panel {
+ grid-column: 2;
+ grid-row: 1;
+ }
+
+ .output-panel {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ border-top: 1px solid var(--border);
+ border-left: 0;
+ }
+
+ .workspace[data-files-collapsed="true"] {
+ grid-template-columns: minmax(20rem, 1fr);
+ }
+
+ .workspace[data-files-collapsed="true"] .editor-panel {
+ grid-column: 1;
+ }
+
+ .workspace[data-output-collapsed="true"] {
+ grid-template-rows: minmax(20rem, 1fr);
+ }
+}
+
+@media (max-width: 700px), (max-width: 900px) and (max-height: 600px) {
+ html,
+ body {
+ height: auto;
+ min-height: 100%;
+ }
+
+ body {
+ overflow: auto;
+ }
+
+ .playground {
+ height: auto;
+ min-height: calc(100dvh - 2.4rem);
+ grid-template-rows: auto auto;
+ }
+
+ .toolbar {
+ min-height: 0;
+ padding: calc(0.65rem + env(safe-area-inset-top)) calc(0.75rem + env(safe-area-inset-right)) 0.65rem
+ calc(0.75rem + env(safe-area-inset-left));
+ align-items: stretch;
+ flex-direction: column;
+ gap: 0.55rem;
+ }
+
+ .toolbar h1 {
+ font-size: 1.05rem;
+ }
+
+ .site-links {
+ display: none;
+ }
+
+ .site-footer {
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 0.35rem;
+ }
+
+ .site-footer nav {
+ justify-content: flex-start;
+ }
+
+ .toolbar-actions {
+ width: 100%;
+ flex: none;
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.5rem;
+ }
+
+ .version-control {
+ min-width: 0;
+ grid-column: 1 / -1;
+ width: 100%;
+ max-width: none;
+ min-height: 0;
+ padding: 0;
+ }
+
+ .version-control summary {
+ min-height: 2.75rem;
+ }
+
+ .version-menu {
+ width: 100%;
+ }
+
+ .toolbar button {
+ min-width: 0;
+ min-height: 2.75rem;
+ }
+
+ .status {
+ min-width: 0;
+ grid-column: 1 / -1;
+ line-height: 1.35;
+ overflow-wrap: anywhere;
+ white-space: normal;
+ }
+
+ .resources-dialog {
+ width: 100vw;
+ max-width: none;
+ height: 100dvh;
+ max-height: none;
+ border: 0;
+ border-radius: 0;
+ }
+
+ .form-dialog {
+ width: calc(100vw - 1.5rem);
+ }
+
+ .settings-dialog {
+ max-height: calc(100dvh - 1.5rem);
+ }
+
+ .form-dialog input,
+ .form-dialog button {
+ min-height: 2.75rem;
+ }
+
+ .download-consent button {
+ min-height: 2.75rem;
+ }
+
+ .resources-dialog > header button,
+ .help-document nav button {
+ min-height: 2.75rem;
+ }
+
+ .resource-search {
+ grid-template-columns: 1fr;
+ gap: 0.35rem;
+ }
+
+ .resource-search input {
+ min-height: 2.75rem;
+ }
+
+ .resource-list > button,
+ .resource-list > a {
+ min-height: 3.25rem;
+ }
+
+ .workspace {
+ min-height: 0;
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-rows: auto minmax(28rem, 70dvh) auto;
+ }
+
+ .file-explorer {
+ max-height: min(16rem, 40dvh);
+ grid-column: 1;
+ grid-row: 1;
+ overflow: auto;
+ border-right: 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .file-explorer-heading {
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ min-height: 2.75rem;
+ background: var(--sidebar);
+ }
+
+ .file-explorer-actions button {
+ min-width: 2.75rem;
+ height: 2.75rem;
+ padding: 0 0.55rem;
+ }
+
+ .file-tree-folder {
+ min-height: 2.75rem;
+ display: flex;
+ align-items: center;
+ }
+
+ .file-tree-file-button {
+ min-height: 2.75rem;
+ padding-top: 0.65rem;
+ padding-bottom: 0.65rem;
+ }
+
+ .file-tree-delete {
+ width: 2.75rem;
+ height: 2.75rem;
+ opacity: 1;
+ }
+
+ .editor-panel {
+ min-height: 28rem;
+ grid-column: 1;
+ grid-row: 2;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .output-panel {
+ min-height: max(32rem, 70dvh);
+ grid-column: 1;
+ grid-row: 3;
+ border-top: 0;
+ border-left: 0;
+ }
+
+ .panel-heading {
+ align-items: center;
+ }
+
+ .panel-heading span {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .editor-heading-title {
+ flex: 1 1 auto;
+ }
+
+ .editor-heading-actions {
+ flex: 0 1 auto;
+ }
+
+ .editor-navigation button,
+ .editor-heading-actions button,
+ .output-heading-actions button,
+ .output-section-toggle,
+ .run-output .output-section-toggle {
+ min-width: 2.75rem;
+ min-height: 2.75rem;
+ }
+
+ .emit-output {
+ min-height: 18rem;
+ padding: 0.5rem;
+ }
+
+ .emit-file pre {
+ overflow-wrap: anywhere;
+ white-space: pre-wrap;
+ }
+
+ .monaco-editor .scrollbar.horizontal {
+ display: none !important;
+ }
+
+ .run-output {
+ max-height: 16rem;
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+
+ .run-log {
+ overflow-wrap: anywhere;
+ }
+
+ .run-output button {
+ min-width: 2.75rem;
+ min-height: 2.75rem;
+ }
+
+ .loading-overlay {
+ position: fixed;
+ top: 50%;
+ right: 0.5rem;
+ left: 0.5rem;
+ transform: translateY(-50%);
+ }
+
+ .loading-card {
+ width: 100%;
+ }
+}
+
+@media (max-width: 420px) {
+ .file-explorer-heading {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+
+ .file-explorer-actions {
+ gap: 0.15rem;
+ }
+
+ .file-explorer-actions button {
+ min-width: 0;
+ padding-right: 0.4rem;
+ padding-left: 0.4rem;
+ }
+
+ .editor-panel .panel-heading span {
+ display: none;
+ }
+
+ .editor-heading-title {
+ gap: 0.35rem;
+ }
+
+ .editor-heading-actions {
+ flex: 0 0 auto;
+ }
+
+ .loading-card {
+ gap: 0.65rem;
+ }
+
+ .lds-grid {
+ width: 40px;
+ height: 40px;
+ flex-basis: 40px;
+ transform: scale(0.8);
+ transform-origin: center;
+ }
+}
+
+@media (pointer: coarse) {
+ button,
+ select,
+ summary {
+ touch-action: manipulation;
+ }
+
+ .file-tree-delete {
+ width: 2.75rem;
+ height: 2.75rem;
+ opacity: 1;
+ }
+}
+
+:root[data-theme="light"] {
+ color-scheme: light;
+}
+
+:root[data-theme="dark"] {
+ color: #f3f3f3;
+ color-scheme: dark;
+ background: #1e1e1e;
+ --accent: #75b7f0;
+ --border: #414141;
+ --hover: #292929;
+ --menu-text: #f3f3f3;
+ --muted: #b7b7b7;
+ --output: #171717;
+ --panel: #1e1e1e;
+ --selected: #37373d;
+ --sidebar: #252526;
+ --toolbar: #235a97;
+ --tree-guide: #5c5c5c;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme]) {
+ color: #f3f3f3;
+ background: #1e1e1e;
+ --accent: #75b7f0;
+ --border: #414141;
+ --hover: #292929;
+ --menu-text: #f3f3f3;
+ --muted: #b7b7b7;
+ --output: #171717;
+ --panel: #1e1e1e;
+ --selected: #37373d;
+ --sidebar: #252526;
+ --toolbar: #235a97;
+ --tree-guide: #5c5c5c;
+ }
+}
diff --git a/packages/playground-v2/src/tsgo-lsp.ts b/packages/playground-v2/src/tsgo-lsp.ts
new file mode 100644
index 000000000000..d758cfbade49
--- /dev/null
+++ b/packages/playground-v2/src/tsgo-lsp.ts
@@ -0,0 +1,418 @@
+import { createTransportToWorker, MonacoLspClient } from "@vscode/monaco-lsp-client"
+import * as monaco from "monaco-editor-core"
+import {
+ conf as javascriptConfiguration,
+ language as javascriptLanguage,
+} from "monaco-editor/languages/definitions/javascript/javascript.js"
+import {
+ conf as typescriptConfiguration,
+ language as typescriptLanguage,
+} from "monaco-editor/languages/definitions/typescript/typescript.js"
+
+const headerWords = 4
+const readPosition = 0
+const writePosition = 1
+const closed = 2
+const signal = 3
+const bufferSize = 4 * 1024 * 1024
+
+export type TsgoStatus = "mounting files" | "starting tsc.wasm" | "initializing LSP" | "ready"
+
+type WorkerMessage = {
+ type: "lsp" | "drain" | "status" | "stderr" | "error"
+ message?: any
+ status?: TsgoStatus
+}
+
+type LspRange = {
+ start: { line: number; character: number }
+ end: { line: number; character: number }
+}
+
+type StartTsgoLspOptions = {
+ configFileName: string
+ editor: monaco.editor.IStandaloneCodeEditor
+ effectiveConfigText: string
+ extraFiles: Record
+ libraries: Record
+ models: readonly monaco.editor.ITextModel[]
+ module: WebAssembly.Module
+ onError(message: string): void
+ onNavigate(fileName: string, range: monaco.Range): void
+ onStatus(status: TsgoStatus, serverInfo?: string): void
+}
+
+export type TsgoLspController = {
+ updateEffectiveConfig(text: string): void
+}
+
+class RingBufferWorker {
+ readonly #worker = new Worker(new URL("./tsgo-lsp.worker.js", import.meta.url), {
+ type: "module",
+ })
+ readonly #state: Int32Array
+ readonly #data: Uint8Array
+ readonly #queue: Uint8Array[] = []
+ readonly #listeners = new Map()
+ readonly #pendingRequests = new Map()
+ readonly #configFileName: string
+ #configVersion = 1
+ #effectiveConfigText: string
+ #queueOffset = 0
+
+ onStatus?: (status: TsgoStatus) => void
+ onError?: (message: string) => void
+ onServerInfo?: (serverInfo: string) => void
+
+ constructor(stdin: SharedArrayBuffer, configFileName: string, effectiveConfigText: string) {
+ this.#state = new Int32Array(stdin, 0, headerWords)
+ this.#data = new Uint8Array(stdin, headerWords * Int32Array.BYTES_PER_ELEMENT)
+ this.#configFileName = configFileName
+ this.#effectiveConfigText = effectiveConfigText
+ this.#worker.addEventListener("message", (event: MessageEvent) => {
+ if (event.data.type === "drain") {
+ this.#flush()
+ } else if (event.data.type === "status" && event.data.status) {
+ this.onStatus?.(event.data.status)
+ } else if (event.data.type === "error") {
+ this.onError?.(event.data.message)
+ } else if (event.data.type === "stderr") {
+ console.warn("[tsgo]", event.data.message)
+ } else if (event.data.type === "lsp") {
+ void this.#handleLspMessage(event.data.message)
+ }
+ })
+ }
+
+ start(
+ stdin: SharedArrayBuffer,
+ module: WebAssembly.Module,
+ libraries: Record,
+ files: Record
+ ) {
+ this.#worker.postMessage({
+ type: "init",
+ stdin,
+ libraries,
+ module,
+ files,
+ })
+ }
+
+ postMessage(message: unknown) {
+ message = this.#rewriteConfigMessage(message)
+ const lspMessage = message as { id?: string | number; method?: string }
+ if (lspMessage.id !== undefined && lspMessage.method) {
+ this.#pendingRequests.set(lspMessage.id, lspMessage.method)
+ }
+
+ const body = new TextEncoder().encode(JSON.stringify(message))
+ const header = new TextEncoder().encode(`Content-Length: ${body.length}\r\n\r\n`)
+ const framed = new Uint8Array(header.length + body.length)
+ framed.set(header)
+ framed.set(body, header.length)
+ if (framed.length > this.#data.length) {
+ throw new Error(`LSP message exceeds the ${this.#data.length}-byte stdin buffer`)
+ }
+ this.#queue.push(framed)
+ this.#flush()
+ }
+
+ updateEffectiveConfig(text: string) {
+ if (text === this.#effectiveConfigText) return
+ this.#effectiveConfigText = text
+ this.postMessage({
+ jsonrpc: "2.0",
+ method: "textDocument/didChange",
+ params: {
+ contentChanges: [{ text }],
+ textDocument: {
+ uri: monaco.Uri.file(this.#configFileName).toString(),
+ version: this.#configVersion + 1,
+ },
+ },
+ })
+ }
+
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject) {
+ if (type !== "message") return
+ const callback: EventListener = typeof listener === "function" ? listener : event => listener.handleEvent(event)
+ this.#listeners.set(listener, callback)
+ }
+
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject) {
+ if (type === "message") this.#listeners.delete(listener)
+ }
+
+ #rewriteConfigMessage(message: unknown) {
+ if (!message || typeof message !== "object") return message
+ const candidate = message as {
+ method?: string
+ params?: {
+ contentChanges?: Array<{ text?: string }>
+ textDocument?: { text?: string; uri?: string; version?: number }
+ }
+ }
+ const document = candidate.params?.textDocument
+ if (!document?.uri || monaco.Uri.parse(document.uri).path !== this.#configFileName) return message
+ if (candidate.method === "textDocument/didOpen") {
+ this.#configVersion = Math.max(this.#configVersion, document.version ?? this.#configVersion)
+ return {
+ ...candidate,
+ params: {
+ ...candidate.params,
+ textDocument: {
+ ...document,
+ text: this.#effectiveConfigText,
+ },
+ },
+ }
+ }
+ if (candidate.method === "textDocument/didChange") {
+ this.#configVersion = Math.max(this.#configVersion + 1, document.version ?? 0)
+ return {
+ ...candidate,
+ params: {
+ ...candidate.params,
+ contentChanges: [{ text: this.#effectiveConfigText }],
+ textDocument: {
+ ...document,
+ version: this.#configVersion,
+ },
+ },
+ }
+ }
+ return message
+ }
+
+ #flush() {
+ while (this.#queue.length > 0) {
+ const readPos = Atomics.load(this.#state, readPosition)
+ const writePos = Atomics.load(this.#state, writePosition)
+ const free = this.#data.length - (writePos - readPos)
+ if (free <= 0) return
+
+ const current = this.#queue[0]
+ const length = Math.min(free, current.length - this.#queueOffset)
+ const start = writePos % this.#data.length
+ const first = Math.min(length, this.#data.length - start)
+ this.#data.set(current.subarray(this.#queueOffset, this.#queueOffset + first), start)
+ if (first < length) {
+ this.#data.set(current.subarray(this.#queueOffset + first, this.#queueOffset + length), 0)
+ }
+ this.#queueOffset += length
+ Atomics.store(this.#state, writePosition, writePos + length)
+ Atomics.add(this.#state, signal, 1)
+ Atomics.notify(this.#state, signal)
+
+ if (this.#queueOffset === current.length) {
+ this.#queue.shift()
+ this.#queueOffset = 0
+ }
+ }
+ }
+
+ async #handleLspMessage(message: any) {
+ const method = message?.method
+ const label = method ?? this.#pendingRequests.get(message?.id)
+ if (method === "textDocument/publishDiagnostics") return
+ if (label === "textDocument/definition") {
+ message.result = normalizeLibraryLocations(message?.result)
+ await ensureDefinitionModels(message.result)
+ navigateToDefinition(message.result)
+ }
+ if (message?.id !== undefined && !method) {
+ this.#pendingRequests.delete(message.id)
+ }
+ if (message?.result?.capabilities) {
+ const info = message.result.serverInfo
+ if (info?.name) {
+ const name = info.name === "typescript-go" ? "TypeScript" : info.name
+ this.onServerInfo?.(`${name}${info.version ? ` ${info.version}` : ""}`)
+ }
+ this.onStatus?.("ready")
+ }
+
+ const forwarded = new MessageEvent("message", { data: message })
+ for (const listener of this.#listeners.values()) listener(forwarded)
+ }
+}
+
+let languageRegistered = false
+let activeEditor: monaco.editor.IStandaloneCodeEditor | undefined
+let navigateToLocation: StartTsgoLspOptions["onNavigate"] | undefined
+let definitionFilesPromise: Promise> | undefined
+
+export function registerPlaygroundLanguages() {
+ if (languageRegistered) return
+ languageRegistered = true
+ monaco.languages.register({ id: "typescript", extensions: [".ts", ".tsx", ".mts", ".cts"] })
+ monaco.languages.setLanguageConfiguration("typescript", typescriptConfiguration)
+ monaco.languages.setMonarchTokensProvider("typescript", typescriptLanguage)
+ monaco.languages.register({ id: "javascript", extensions: [".js", ".jsx", ".mjs", ".cjs"] })
+ monaco.languages.setLanguageConfiguration("javascript", javascriptConfiguration)
+ monaco.languages.setMonarchTokensProvider("javascript", javascriptLanguage)
+ monaco.languages.register({ id: "json", extensions: [".json"] })
+ monaco.languages.setLanguageConfiguration("json", {
+ brackets: [
+ ["{", "}"],
+ ["[", "]"],
+ ],
+ comments: { lineComment: "//", blockComment: ["/*", "*/"] },
+ })
+ monaco.languages.setMonarchTokensProvider("json", {
+ tokenizer: {
+ root: [
+ [/"(?:\\.|[^"\\])*"(?=\s*:)/, "string.key.json"],
+ [/"(?:\\.|[^"\\])*"/, "string.value.json"],
+ [/\b(?:true|false|null)\b/, "keyword.json"],
+ [/-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/, "number"],
+ [/[{}\[\],:]/, "delimiter"],
+ [/\/\/.*$/, "comment"],
+ ],
+ },
+ })
+}
+
+export function startTsgoLsp(options: StartTsgoLspOptions) {
+ if (!crossOriginIsolated) {
+ throw new Error("The TypeScript language server requires cross-origin isolation; reload once to activate it.")
+ }
+
+ activeEditor = options.editor
+ navigateToLocation = options.onNavigate
+ definitionFilesPromise = Promise.resolve({
+ ...Object.fromEntries(
+ Object.entries(options.libraries).map(([fileName, text]) => [
+ `/typescript/lib/${fileName.slice(fileName.lastIndexOf("/") + 1)}`,
+ text,
+ ])
+ ),
+ ...options.extraFiles,
+ })
+ const stdin = new SharedArrayBuffer(headerWords * Int32Array.BYTES_PER_ELEMENT + bufferSize)
+ const worker = new RingBufferWorker(stdin, options.configFileName, options.effectiveConfigText)
+ let serverInfo: string | undefined
+ worker.onStatus = status => options.onStatus(status, serverInfo)
+ worker.onServerInfo = info => {
+ serverInfo = info
+ }
+ worker.onError = options.onError
+ worker.start(stdin, options.module, options.libraries, {
+ ...options.extraFiles,
+ ...Object.fromEntries(options.models.map(model => [model.uri.path, model.getValue()])),
+ [options.configFileName]: options.effectiveConfigText,
+ })
+
+ const transport = createTransportToWorker(worker as unknown as Worker)
+ new MonacoLspClient(transport)
+ return {
+ updateEffectiveConfig(text: string) {
+ worker.updateEffectiveConfig(text)
+ },
+ } satisfies TsgoLspController
+}
+
+function normalizeLibraryLocations(result: unknown): unknown {
+ if (Array.isArray(result)) return result.map(normalizeLibraryLocations)
+ if (!result || typeof result !== "object") return result
+
+ const location = result as {
+ uri?: string
+ targetUri?: string
+ }
+ const uri = location.targetUri ?? location.uri
+ if (!uri) return result
+ const normalized = normalizeLibraryUri(uri)
+ if (normalized === uri) return result
+ return "targetUri" in location ? { ...location, targetUri: normalized } : { ...location, uri: normalized }
+}
+
+function normalizeLibraryUri(uri: string) {
+ const parsed = monaco.Uri.parse(uri)
+ if (parsed.scheme !== "bundled" || !/^\/libs\/lib(?:\..*)?\.d\.ts$/i.test(parsed.path)) {
+ return uri
+ }
+ return monaco.Uri.file(`/typescript/lib/${parsed.path.slice("/libs/".length)}`).toString()
+}
+
+async function ensureDefinitionModels(result: unknown) {
+ const locations = Array.isArray(result) ? result : [result]
+ const uris = new Set()
+ for (const location of locations) {
+ if (!location || typeof location !== "object") continue
+ const candidate = location as { uri?: string; targetUri?: string }
+ const uri = candidate.targetUri ?? candidate.uri
+ if (uri && monaco.Uri.parse(uri).scheme === "file") uris.add(uri)
+ }
+
+ const definitionFiles = uris.size > 0 ? await getDefinitionFiles() : {}
+ for (const uri of uris) {
+ const monacoUri = monaco.Uri.parse(uri)
+ if (monaco.editor.getModel(monacoUri)) continue
+ const contents = definitionFiles[monacoUri.path]
+ if (contents === undefined) continue
+ monaco.editor.createModel(contents, languageForFile(monacoUri.path), monacoUri)
+ }
+}
+
+function getDefinitionFiles() {
+ definitionFilesPromise ??= fetch(new URL("./lib-files.json", import.meta.url)).then(response => {
+ if (!response.ok) {
+ throw new Error(`Could not load TypeScript libraries: ${response.status} ${response.statusText}`)
+ }
+ return response
+ .json()
+ .then((files: Record) =>
+ Object.fromEntries(
+ Object.entries(files).map(([fileName, text]) => [
+ `/typescript/lib/${fileName.slice(fileName.lastIndexOf("/") + 1)}`,
+ text,
+ ])
+ )
+ )
+ })
+ return definitionFilesPromise
+}
+
+function navigateToDefinition(result: unknown) {
+ const location = Array.isArray(result) ? result[0] : result
+ if (!activeEditor || !location || typeof location !== "object") return
+
+ const target = location as {
+ uri?: string
+ targetUri?: string
+ range?: LspRange
+ targetRange?: LspRange
+ targetSelectionRange?: LspRange
+ }
+
+ const uri = target.targetUri ?? target.uri
+ const range = target.targetSelectionRange ?? target.targetRange ?? target.range
+ if (!uri || !range) return
+ const model = monaco.editor.getModel(monaco.Uri.parse(uri))
+ if (!model) return
+
+ const monacoRange = new monaco.Range(
+ range.start.line + 1,
+ range.start.character + 1,
+ range.end.line + 1,
+ range.end.character + 1
+ )
+ if (navigateToLocation) {
+ navigateToLocation(model.uri.path, monacoRange)
+ return
+ }
+ activeEditor.setModel(model)
+ activeEditor.setSelection(monacoRange)
+ activeEditor.revealRangeInCenter(monacoRange, monaco.editor.ScrollType.Immediate)
+ activeEditor.focus()
+}
+
+function languageForFile(fileName: string) {
+ if (/\.json$/i.test(fileName)) return "json"
+ if (/\.[cm]?jsx?$/i.test(fileName)) return "javascript"
+ return "typescript"
+}
+
+export { monaco }
diff --git a/packages/playground-v2/src/tsgo-lsp.worker.ts b/packages/playground-v2/src/tsgo-lsp.worker.ts
new file mode 100644
index 000000000000..572e56eb19fb
--- /dev/null
+++ b/packages/playground-v2/src/tsgo-lsp.worker.ts
@@ -0,0 +1,306 @@
+import { Directory, Fd, File, Inode, PreopenDirectory, WASI, wasi, WASIProcExit } from "@bjorn3/browser_wasi_shim"
+
+const headerWords = 4
+const readPosition = 0
+const writePosition = 1
+const closed = 2
+const signal = 3
+const subscriptionSize = 48
+const eventSize = 32
+
+type InitMessage = {
+ type: "init"
+ stdin: SharedArrayBuffer
+ libraries: Record
+ module: WebAssembly.Module
+ files: Record
+}
+
+class BlockingStdin extends Fd {
+ readonly #state: Int32Array
+ readonly #data: Uint8Array
+
+ constructor(buffer: SharedArrayBuffer) {
+ super()
+ this.#state = new Int32Array(buffer, 0, headerWords)
+ this.#data = new Uint8Array(buffer, headerWords * Int32Array.BYTES_PER_ELEMENT)
+ }
+
+ override fd_fdstat_get() {
+ const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, wasi.FDFLAGS_NONBLOCK)
+ fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_READ | wasi.RIGHTS_POLL_FD_READWRITE)
+ return { ret: wasi.ERRNO_SUCCESS, fdstat }
+ }
+
+ override fd_filestat_get() {
+ return {
+ ret: wasi.ERRNO_SUCCESS,
+ filestat: new wasi.Filestat(Inode.issue_ino(), wasi.FILETYPE_CHARACTER_DEVICE, 0n),
+ }
+ }
+
+ override fd_fdstat_set_flags() {
+ return wasi.ERRNO_SUCCESS
+ }
+
+ override fd_read(size: number) {
+ let readPos = Atomics.load(this.#state, readPosition)
+ let writePos = Atomics.load(this.#state, writePosition)
+ if (readPos === writePos && !Atomics.load(this.#state, closed)) {
+ const currentSignal = Atomics.load(this.#state, signal)
+ Atomics.wait(this.#state, signal, currentSignal, 50)
+ readPos = Atomics.load(this.#state, readPosition)
+ writePos = Atomics.load(this.#state, writePosition)
+ }
+ if (readPos !== writePos) {
+ const available = writePos - readPos
+ const length = Math.min(size, available)
+ const result = new Uint8Array(length)
+ const start = readPos % this.#data.length
+ const first = Math.min(length, this.#data.length - start)
+ result.set(this.#data.subarray(start, start + first))
+ if (first < length) {
+ result.set(this.#data.subarray(0, length - first), first)
+ }
+ Atomics.store(this.#state, readPosition, readPos + length)
+ self.postMessage({ type: "drain" })
+ return { ret: wasi.ERRNO_SUCCESS, data: result }
+ }
+ if (Atomics.load(this.#state, closed)) {
+ return { ret: wasi.ERRNO_SUCCESS, data: new Uint8Array() }
+ }
+ return { ret: wasi.ERRNO_AGAIN, data: new Uint8Array() }
+ }
+}
+
+class LspStdout extends Fd {
+ #buffer = new Uint8Array()
+
+ override fd_fdstat_get() {
+ const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, 0)
+ fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_WRITE)
+ return { ret: wasi.ERRNO_SUCCESS, fdstat }
+ }
+
+ override fd_filestat_get() {
+ return {
+ ret: wasi.ERRNO_SUCCESS,
+ filestat: new wasi.Filestat(Inode.issue_ino(), wasi.FILETYPE_CHARACTER_DEVICE, 0n),
+ }
+ }
+
+ override fd_write(data: Uint8Array) {
+ const combined = new Uint8Array(this.#buffer.length + data.length)
+ combined.set(this.#buffer)
+ combined.set(data, this.#buffer.length)
+ this.#buffer = combined
+ this.#flushMessages()
+ return { ret: wasi.ERRNO_SUCCESS, nwritten: data.length }
+ }
+
+ #flushMessages() {
+ for (;;) {
+ const headerEnd = findHeaderEnd(this.#buffer)
+ if (headerEnd < 0) return
+ const header = new TextDecoder().decode(this.#buffer.subarray(0, headerEnd))
+ const match = /(?:^|\r\n)Content-Length:\s*(\d+)/i.exec(header)
+ if (!match) {
+ self.postMessage({ type: "error", message: `Invalid LSP header: ${header}` })
+ this.#buffer = new Uint8Array()
+ return
+ }
+ const contentLength = Number(match[1])
+ const bodyStart = headerEnd + 4
+ const bodyEnd = bodyStart + contentLength
+ if (this.#buffer.length < bodyEnd) return
+ const body = new TextDecoder().decode(this.#buffer.subarray(bodyStart, bodyEnd))
+ try {
+ self.postMessage({ type: "lsp", message: JSON.parse(body) })
+ } catch (error) {
+ self.postMessage({
+ type: "error",
+ message: `Invalid LSP JSON: ${String(error)}`,
+ })
+ }
+ this.#buffer = this.#buffer.slice(bodyEnd)
+ }
+ }
+}
+
+class Stderr extends Fd {
+ readonly #decoder = new TextDecoder()
+
+ override fd_write(data: Uint8Array) {
+ const message = this.#decoder.decode(data, { stream: true }).trim()
+ if (message) self.postMessage({ type: "stderr", message })
+ return { ret: wasi.ERRNO_SUCCESS, nwritten: data.length }
+ }
+}
+
+function findHeaderEnd(data: Uint8Array) {
+ for (let i = 0; i <= data.length - 4; i++) {
+ if (data[i] === 13 && data[i + 1] === 10 && data[i + 2] === 13 && data[i + 3] === 10) {
+ return i
+ }
+ }
+ return -1
+}
+
+function installPollOneoff(wasiRuntime: WASI, state: Int32Array) {
+ wasiRuntime.wasiImport.poll_oneoff = (
+ inputPointer: number,
+ outputPointer: number,
+ subscriptionCount: number,
+ eventCountPointer: number
+ ) => {
+ const memory = new DataView(wasiRuntime.inst.exports.memory.buffer)
+ const subscriptions = Array.from({ length: subscriptionCount }, (_, index) =>
+ wasi.Subscription.read_bytes(memory, inputPointer + index * subscriptionSize)
+ )
+ const clockDeadlines = new Map()
+ for (const subscription of subscriptions) {
+ if (subscription.eventtype !== wasi.EVENTTYPE_CLOCK) continue
+ const clockNow =
+ subscription.clockid === wasi.CLOCKID_REALTIME
+ ? BigInt(Date.now()) * 1_000_000n
+ : BigInt(Math.round(performance.now() * 1e6))
+ clockDeadlines.set(
+ subscription,
+ (subscription.flags & wasi.SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME) !== 0
+ ? subscription.timeout
+ : clockNow + subscription.timeout
+ )
+ }
+
+ for (;;) {
+ const now = {
+ [wasi.CLOCKID_MONOTONIC]: BigInt(Math.round(performance.now() * 1e6)),
+ [wasi.CLOCKID_REALTIME]: BigInt(Date.now()) * 1_000_000n,
+ }
+ const ready = subscriptions.filter(subscription => {
+ if (subscription.eventtype === wasi.EVENTTYPE_FD_READ) {
+ return (
+ Atomics.load(state, readPosition) !== Atomics.load(state, writePosition) ||
+ Atomics.load(state, closed) !== 0
+ )
+ }
+ if (subscription.eventtype === wasi.EVENTTYPE_FD_WRITE) return true
+ if (subscription.eventtype !== wasi.EVENTTYPE_CLOCK) return false
+ const clockNow = now[subscription.clockid as keyof typeof now] ?? 0n
+ return (clockDeadlines.get(subscription) ?? 0n) <= clockNow
+ })
+
+ if (ready.length > 0) {
+ ready.forEach((subscription, index) => {
+ const eventPointer = outputPointer + index * eventSize
+ new wasi.Event(subscription.userdata, wasi.ERRNO_SUCCESS, subscription.eventtype).write_bytes(
+ memory,
+ eventPointer
+ )
+ memory.setBigUint64(eventPointer + 16, 0n, true)
+ memory.setUint16(eventPointer + 24, 0, true)
+ })
+ memory.setUint32(eventCountPointer, ready.length, true)
+ return wasi.ERRNO_SUCCESS
+ }
+
+ const currentSignal = Atomics.load(state, signal)
+ const nextDeadline = Math.min(
+ 50,
+ ...subscriptions
+ .filter(subscription => subscription.eventtype === wasi.EVENTTYPE_CLOCK)
+ .map(subscription => {
+ const clockNow = now[subscription.clockid as keyof typeof now] ?? 0n
+ const remaining = (clockDeadlines.get(subscription) ?? clockNow) - clockNow
+ return Math.max(1, Math.ceil(Number(remaining) / 1e6))
+ })
+ )
+ Atomics.wait(state, signal, currentSignal, nextDeadline)
+ }
+ }
+}
+
+type Tree = Map
+
+function createFileSystem(files: Record) {
+ const root: Tree = new Map([
+ ["workspace", new Map()],
+ ["tmp", new Map()],
+ ["typescript", new Map([["lib", new Map()]])],
+ ])
+ for (const [filename, contents] of Object.entries(files)) {
+ const parts = filename.replace(/^\/+/, "").split("/")
+ const basename = parts.pop()!
+ let current = root
+ for (const part of parts) {
+ let child = current.get(part)
+ if (!(child instanceof Map)) {
+ child = new Map()
+ current.set(part, child)
+ }
+ current = child
+ }
+ current.set(basename, contents)
+ }
+
+ function build(tree: Tree): Directory {
+ const contents = new Map()
+ for (const [name, value] of tree) {
+ contents.set(name, typeof value === "string" ? new File(new TextEncoder().encode(value)) : build(value))
+ }
+ return new Directory(contents)
+ }
+
+ return new PreopenDirectory("/", build(root).contents)
+}
+
+async function start(message: InitMessage) {
+ self.postMessage({ type: "status", status: "mounting files" })
+ const files = { ...message.files }
+ for (const [name, contents] of Object.entries(message.libraries)) {
+ const filename = name.slice(name.lastIndexOf("/") + 1)
+ files[`/typescript/lib/${filename}`] = contents
+ }
+
+ const fds = [new BlockingStdin(message.stdin), new LspStdout(), new Stderr(), createFileSystem(files)]
+ const wasiRuntime = new WASI(["tsc", "--lsp", "--stdio"], ["HOME=/workspace", "TMPDIR=/tmp"], fds, { debug: false })
+ installPollOneoff(wasiRuntime, new Int32Array(message.stdin, 0, headerWords))
+ const instance = await WebAssembly.instantiate(message.module, {
+ wasi_snapshot_preview1: wasiRuntime.wasiImport,
+ })
+ self.postMessage({ type: "status", status: "starting tsc.wasm" })
+ self.postMessage({ type: "status", status: "initializing LSP" })
+ try {
+ wasiRuntime.start(
+ instance as unknown as {
+ exports: {
+ memory: WebAssembly.Memory
+ _start(): unknown
+ }
+ }
+ )
+ } catch (error) {
+ if (error instanceof WASIProcExit) {
+ self.postMessage({
+ type: "error",
+ message: `tsc exited with status ${error.code}`,
+ })
+ } else {
+ throw error
+ }
+ }
+}
+
+self.addEventListener(
+ "message",
+ (event: MessageEvent) => {
+ if (event.data.type !== "init") return
+ start(event.data).catch(error => {
+ self.postMessage({
+ type: "error",
+ message: error instanceof Error ? error.stack ?? error.message : String(error),
+ })
+ })
+ },
+ { once: true }
+)
diff --git a/packages/playground-v2/src/type-acquisition.ts b/packages/playground-v2/src/type-acquisition.ts
new file mode 100644
index 000000000000..4b7ad4149e08
--- /dev/null
+++ b/packages/playground-v2/src/type-acquisition.ts
@@ -0,0 +1,77 @@
+import { setupTypeAcquisition } from "@typescript/ata"
+
+type TypeAcquisitionOptions = {
+ onFile(fileName: string, text: string): void
+ onProgress(downloaded: number, total: number): void
+ onStart(): void
+ typescript: typeof import("typescript")
+}
+
+const cacheName = "ts7-playground-package-types-v1"
+
+export function createTypeAcquisition(options: TypeAcquisitionOptions) {
+ let receivedFiles = 0
+ const acquire = setupTypeAcquisition({
+ delegate: {
+ progress: options.onProgress,
+ receivedFile(text, fileName) {
+ receivedFiles++
+ options.onFile(`/workspace${fileName}`, text)
+ },
+ started: options.onStart,
+ },
+ fetcher: cachedFetch,
+ logger: console,
+ projectName: "TypeScript Playground",
+ typescript: options.typescript,
+ })
+
+ return async (source: string) => {
+ const before = receivedFiles
+ await acquire(source)
+ return receivedFiles - before
+ }
+}
+
+export function hasPackageImports(source: string) {
+ const patterns = [
+ /\b(?:import|export)\b[^"'`]*\bfrom\s*["']([^"']+)["']/g,
+ /\b(?:import|require)\s*\(\s*["']([^"']+)["']/g,
+ /\bimport\s*["']([^"']+)["']/g,
+ / void
+ /** A way to display progress */
+ progress?: (downloaded: number, estimatedTotal: number) => void
+ /** Note: An error message does not mean ATA has stopped! */
+ errorMessage?: (userFacingMessage: string, error: Error) => void
+ /** A callback indicating that ATA actually has work to do */
+ started?: () => void
+ /** The callback when all ATA has finished */
+ finished?: (files: Map) => void
+ }
+ /** Passed to fetch as the user-agent */
+ projectName: string
+ /** Your local copy of typescript */
+ typescript: typeof import("typescript")
+ /** If you need a custom version of fetch */
+ fetcher?: typeof fetch
+ /** If you need a custom logger instead of the console global */
+ logger?: Logger
+}
+
+type ModuleMeta = { state: "loading" }
+
+/**
+ * The function which starts up type acquisition,
+ * returns a function which you then pass the initial
+ * source code for the app with.
+ *
+ * This is effectively the main export, everything else is
+ * basically exported for tests and should be considered
+ * implementation details by consumers.
+ */
+export const setupTypeAcquisition: (config: ATABootstrapConfig) => (initialSourceFile: string) => Promise
+
+interface Logger {
+ log: (...args: any[]) => void
+ error: (...args: any[]) => void
+ groupCollapsed: (...args: any[]) => void
+ groupEnd: (...args: any[]) => void
+}
diff --git a/packages/playground-v2/vendor/ata/index.js b/packages/playground-v2/vendor/ata/index.js
new file mode 100644
index 000000000000..ba407ee8da05
--- /dev/null
+++ b/packages/playground-v2/vendor/ata/index.js
@@ -0,0 +1,305 @@
+var __defProp = Object.defineProperty;
+var __defProps = Object.defineProperties;
+var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
+var __getOwnPropSymbols = Object.getOwnPropertySymbols;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __propIsEnum = Object.prototype.propertyIsEnumerable;
+var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
+var __spreadValues = (a, b) => {
+ for (var prop in b || (b = {}))
+ if (__hasOwnProp.call(b, prop))
+ __defNormalProp(a, prop, b[prop]);
+ if (__getOwnPropSymbols)
+ for (var prop of __getOwnPropSymbols(b)) {
+ if (__propIsEnum.call(b, prop))
+ __defNormalProp(a, prop, b[prop]);
+ }
+ return a;
+};
+var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
+var __async = (__this, __arguments, generator) => {
+ return new Promise((resolve, reject) => {
+ var fulfilled = (value) => {
+ try {
+ step(generator.next(value));
+ } catch (e) {
+ reject(e);
+ }
+ };
+ var rejected = (value) => {
+ try {
+ step(generator.throw(value));
+ } catch (e) {
+ reject(e);
+ }
+ };
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
+ step((generator = generator.apply(__this, __arguments)).next());
+ });
+};
+
+// src/apis.ts
+var getNPMVersionsForModule = (config, moduleName) => {
+ const url = `https://data.jsdelivr.com/v1/package/npm/${moduleName}`;
+ return api(config, url, { cache: "no-store" });
+};
+var getNPMVersionForModuleReference = (config, moduleName, reference) => {
+ const url = `https://data.jsdelivr.com/v1/package/resolve/npm/${moduleName}@${reference}`;
+ return api(config, url);
+};
+var getFiletreeForModuleWithVersion = (config, moduleName, version) => __async(null, null, function* () {
+ const url = `https://data.jsdelivr.com/v1/package/npm/${moduleName}@${version}/flat`;
+ const res = yield api(config, url);
+ if (res instanceof Error) {
+ return res;
+ } else {
+ return __spreadProps(__spreadValues({}, res), {
+ moduleName,
+ version
+ });
+ }
+});
+var getDTSFileForModuleWithVersion = (config, moduleName, version, file) => __async(null, null, function* () {
+ const url = `https://cdn.jsdelivr.net/npm/${moduleName}@${version}${file}`;
+ const f = config.fetcher || fetch;
+ const res = yield f(url);
+ if (res.ok) {
+ return res.text();
+ } else {
+ return new Error("OK");
+ }
+});
+function api(config, url, init) {
+ const f = config.fetcher || fetch;
+ return f(url, init).then((res) => {
+ if (res.ok) {
+ return res.json().then((f2) => f2);
+ } else {
+ return new Error("OK");
+ }
+ });
+}
+
+// src/edgeCases.ts
+var mapModuleNameToModule = (moduleSpecifier) => {
+ const builtInNodeMods = [
+ "assert",
+ "assert/strict",
+ "async_hooks",
+ "buffer",
+ "child_process",
+ "cluster",
+ "console",
+ "constants",
+ "crypto",
+ "dgram",
+ "diagnostics_channel",
+ "dns",
+ "dns/promises",
+ "domain",
+ "events",
+ "fs",
+ "fs/promises",
+ "http",
+ "http2",
+ "https",
+ "inspector",
+ "inspector/promises",
+ "module",
+ "net",
+ "os",
+ "path",
+ "path/posix",
+ "path/win32",
+ "perf_hooks",
+ "process",
+ "punycode",
+ "querystring",
+ "readline",
+ "repl",
+ "stream",
+ "stream/promises",
+ "stream/consumers",
+ "stream/web",
+ "string_decoder",
+ "sys",
+ "timers",
+ "timers/promises",
+ "tls",
+ "trace_events",
+ "tty",
+ "url",
+ "util",
+ "util/types",
+ "v8",
+ "vm",
+ "wasi",
+ "worker_threads",
+ "zlib"
+ ];
+ if (moduleSpecifier.indexOf("node:") === 0 || builtInNodeMods.includes(moduleSpecifier)) {
+ return "node";
+ }
+ const [a = "", b = ""] = moduleSpecifier.split("/");
+ const moduleName = a.startsWith("@") ? `${a}/${b}` : a;
+ return moduleName;
+};
+
+// src/index.ts
+var setupTypeAcquisition = (config) => {
+ const moduleMap = /* @__PURE__ */ new Map();
+ const fsMap = /* @__PURE__ */ new Map();
+ let estimatedToDownload = 0;
+ let estimatedDownloaded = 0;
+ return (initialSourceFile) => {
+ estimatedToDownload = 0;
+ estimatedDownloaded = 0;
+ return resolveDeps(initialSourceFile, 0).then((t) => {
+ var _a, _b;
+ if (estimatedDownloaded > 0) {
+ (_b = (_a = config.delegate).finished) == null ? void 0 : _b.call(_a, fsMap);
+ }
+ });
+ };
+ function resolveDeps(initialSourceFile, depth) {
+ return __async(this, null, function* () {
+ var _a, _b, _c, _d, _e;
+ const depsToGet = getNewDependencies(config, moduleMap, initialSourceFile);
+ depsToGet.forEach((dep) => moduleMap.set(dep.module, { state: "loading" }));
+ const trees = yield Promise.all(depsToGet.map((f) => getFileTreeForModuleWithTag(config, f.module, f.version)));
+ const treesOnly = trees.filter((t) => !("error" in t));
+ const hasDTS = treesOnly.filter((t) => t.files.find((f) => isDtsFile(f.name)));
+ const dtsFilesFromNPM = hasDTS.map((t) => treeToDTSFiles(t, `/node_modules/${t.moduleName}`));
+ const mightBeOnDT = treesOnly.filter((t) => !hasDTS.includes(t));
+ const dtTrees = yield Promise.all(
+ // TODO: Switch from 'latest' to the version from the original tree which is user-controlled
+ mightBeOnDT.map((f) => getFileTreeForModuleWithTag(config, `@types/${getDTName(f.moduleName)}`, "latest"))
+ );
+ const dtTreesOnly = dtTrees.filter((t) => !("error" in t));
+ const dtsFilesFromDT = dtTreesOnly.map((t) => treeToDTSFiles(t, `/node_modules/@types/${getDTName(t.moduleName).replace("types__", "")}`));
+ const allDTSFiles = dtsFilesFromNPM.concat(dtsFilesFromDT).reduce((p, c) => p.concat(c), []);
+ estimatedToDownload += allDTSFiles.length;
+ if (allDTSFiles.length && depth === 0) {
+ (_b = (_a = config.delegate).started) == null ? void 0 : _b.call(_a);
+ }
+ for (const tree of treesOnly) {
+ let prefix = `/node_modules/${tree.moduleName}`;
+ if (dtTreesOnly.includes(tree)) prefix = `/node_modules/@types/${getDTName(tree.moduleName).replace("types__", "")}`;
+ const path = prefix + "/package.json";
+ const pkgJSON = yield getDTSFileForModuleWithVersion(config, tree.moduleName, tree.version, "/package.json");
+ if (typeof pkgJSON == "string") {
+ fsMap.set(path, pkgJSON);
+ (_d = (_c = config.delegate).receivedFile) == null ? void 0 : _d.call(_c, pkgJSON, path);
+ } else {
+ (_e = config.logger) == null ? void 0 : _e.error(`Could not download package.json for ${tree.moduleName}`);
+ }
+ }
+ yield Promise.all(
+ allDTSFiles.map((dts) => __async(null, null, function* () {
+ var _a2, _b2, _c2;
+ const dtsCode = yield getDTSFileForModuleWithVersion(config, dts.moduleName, dts.moduleVersion, dts.path);
+ estimatedDownloaded++;
+ if (dtsCode instanceof Error) {
+ (_a2 = config.logger) == null ? void 0 : _a2.error(`Had an issue getting ${dts.path} for ${dts.moduleName}`);
+ } else {
+ fsMap.set(dts.vfsPath, dtsCode);
+ (_c2 = (_b2 = config.delegate).receivedFile) == null ? void 0 : _c2.call(_b2, dtsCode, dts.vfsPath);
+ if (config.delegate.progress && estimatedDownloaded % 5 === 0) {
+ config.delegate.progress(estimatedDownloaded, estimatedToDownload);
+ }
+ yield resolveDeps(dtsCode, depth + 1);
+ }
+ }))
+ );
+ });
+ }
+};
+function treeToDTSFiles(tree, vfsPrefix) {
+ const dtsRefs = [];
+ for (const file of tree.files) {
+ if (isDtsFile(file.name)) {
+ dtsRefs.push({
+ moduleName: tree.moduleName,
+ moduleVersion: tree.version,
+ vfsPath: `${vfsPrefix}${file.name}`,
+ path: file.name
+ });
+ }
+ }
+ return dtsRefs;
+}
+var getReferencesForModule = (ts, code) => {
+ const meta = ts.preProcessFile(code);
+ const libMap = ts.libMap || /* @__PURE__ */ new Map();
+ const references = meta.referencedFiles.concat(meta.importedFiles).concat(meta.libReferenceDirectives).filter((f) => !isDtsFile(f.fileName)).filter((d) => !libMap.has(d.fileName));
+ return references.map((r) => {
+ let version = void 0;
+ if (!r.fileName.startsWith(".")) {
+ version = "latest";
+ const line = code.slice(r.end).split("\n")[0];
+ if (line.includes("// types:")) version = line.split("// types: ")[1].trim();
+ }
+ return {
+ module: r.fileName,
+ version
+ };
+ }).filter((r, index, self) => self.findIndex((m) => m.module === r.module && m.version === r.version) === index);
+};
+function getNewDependencies(config, moduleMap, code) {
+ const refs = getReferencesForModule(config.typescript, code).map((ref) => __spreadProps(__spreadValues({}, ref), {
+ module: mapModuleNameToModule(ref.module)
+ }));
+ const modules = refs.filter((f) => !f.module.startsWith(".")).filter((m) => !moduleMap.has(m.module));
+ return modules;
+}
+var getFileTreeForModuleWithTag = (config, moduleName, tag) => __async(null, null, function* () {
+ let toDownload = tag || "latest";
+ if (toDownload.split(".").length < 2) {
+ const response = yield getNPMVersionForModuleReference(config, moduleName, toDownload);
+ if (response instanceof Error) {
+ return {
+ error: response,
+ userFacingMessage: `Could not go from a tag to version on npm for ${moduleName} - possible typo?`
+ };
+ }
+ const neededVersion = response.version;
+ if (!neededVersion) {
+ const versions = yield getNPMVersionsForModule(config, moduleName);
+ if (versions instanceof Error) {
+ return {
+ error: response,
+ userFacingMessage: `Could not get versions on npm for ${moduleName} - possible typo?`
+ };
+ }
+ const tags = Object.entries(versions.tags).join(", ");
+ return {
+ error: new Error("Could not find tag for module"),
+ userFacingMessage: `Could not find a tag for ${moduleName} called ${tag}. Did find ${tags}`
+ };
+ }
+ toDownload = neededVersion;
+ }
+ const res = yield getFiletreeForModuleWithVersion(config, moduleName, toDownload);
+ if (res instanceof Error) {
+ return {
+ error: res,
+ userFacingMessage: `Could not get the files for ${moduleName}@${toDownload}. Is it possibly a typo?`
+ };
+ }
+ return res;
+});
+function getDTName(s) {
+ if (s.indexOf("@") === 0 && s.indexOf("/") !== -1) {
+ s = s.substr(1).replace("/", "__");
+ }
+ return s;
+}
+function isDtsFile(file) {
+ return /\.d\.([^\.]+\.)?[cm]?ts$/i.test(file);
+}
+export {
+ getFileTreeForModuleWithTag,
+ getNewDependencies,
+ getReferencesForModule,
+ setupTypeAcquisition
+};
diff --git a/packages/playground-v2/vendor/ata/package.json b/packages/playground-v2/vendor/ata/package.json
new file mode 100644
index 000000000000..33ff8313d847
--- /dev/null
+++ b/packages/playground-v2/vendor/ata/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@typescript/ata",
+ "version": "0.9.8",
+ "type": "module",
+ "main": "./index.js",
+ "types": "./index.d.ts"
+}
diff --git a/packages/playground-v2/vendor/examples.json b/packages/playground-v2/vendor/examples.json
new file mode 100644
index 000000000000..a9f9cf07d5b7
--- /dev/null
+++ b/packages/playground-v2/vendor/examples.json
@@ -0,0 +1 @@
+{"examples":[{"path":["3-7","Fixits"],"title":"Big number literals","name":"Big number literals.ts","lang":"en","id":"big-number-literals","sortIndex":1,"hash":"657d5a5ef695d2f2a2977aa7aff4afa0dd6f9c30a29fe38a91cfc16f03ea8ce40c5dc7f2d293f32c488b9988ad65fb0dff9bfc3367c2057bd7c92b959e44e2c2","compilerSettings":{"target":99},"code":"// Did you know there is a limit to how big of a number you\n// can represent in JavaScript when writing ?\n\nconst maxHighValue = 9007199254740991;\nconst maxLowValue = -9007199254740991;\n\n// If you go one over/below these numbers\n// then you start to get into dangerous territory.\n\nconst oneOverMax = 9007199254740992;\nconst oneBelowMin = -9007199254740992;\n\n// The solution for handling numbers of this size\n// is to convert these numbers to BigInts instead\n// of a number:\n//\n// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/BigInt\n\n// TypeScript will now offer a fixit for number\n// literals which are above 2^52 (positive / negative)\n// which adds the suffix \"n\" which informs JavaScript\n// that the type should be BigInt.\n\n// Number literals\n9007199254740993;\n-9007199254740993;\n9007199254740994;\n-9007199254740994;\n\n// Hex numbers\n0x19999999999999;\n-0x19999999999999;\n0x20000000000000;\n-0x20000000000000;\n0x20000000000001;\n-0x20000000000001;"},{"path":["3-7","Fixits"],"title":"Const to let","name":"Const to let.ts","lang":"en","id":"const-to-let","sortIndex":1,"hash":"6ad1f3516a970584b4eb4b549d4f071e22e120148df44b57195fe09323240bbde32d196f3f183b62899bcb44c23d48371b3bf33f01630f5033fc51ecaed2e876","compilerSettings":{},"code":"// New to 3.7 is the ability to quickly convert\n// a const variable to a let when the value\n// has been re-assigned.\n\n// You can try this by highlighting the below error\n// and choosing to run the quick-fix.\n\nconst displayName = \"Andrew\";\n\ndisplayName = \"Andrea\";"},{"path":["3-7","Fixits"],"title":"Infer From Usage Changes","name":"Infer From Usage Changes.ts","lang":"en","id":"infer-from-usage-changes","sortIndex":2,"hash":"f7a4ed733a3746ae16ffbe6533aab704b9533bd3329c6db27b059e313475a272992c16e2131c2cc3718336eaf885554ad1ddb8b35bf95c79e7d447c9bee03693","compilerSettings":{"noImplicitAny":false},"code":"// With 3.7 TypeScript's existing 'infer from usage'\n// code fix became smarter. It will now use a list of\n// known important types (string, number, array, Promise)\n// and infer whether the usage of a type matches the API\n// of these objects.\n\n// For the next few examples, select the parameters of\n// the functions, click the light bulb and choose\n// \"Infer Parameter types...\"\n\n// Infer a number array:\n\nfunction pushNumber(arr) {\n arr.push(12);\n}\n\n// Infer a promise:\n\nfunction awaitPromise(promise) {\n promise.then((value) => console.log(value));\n}\n\n// Infer the function, and its return type:\n\nfunction inferAny(app) {\n const result = app.use(\"hi\");\n return result;\n}\n\n// Infer a string array because a string\n// was added to it:\n\nfunction insertString(names) {\n names[1] = \"hello\";\n}"},{"path":["3-7","Syntax and Messaging"],"title":"Flattened Error Reporting","name":"Flattened Error Reporting.ts","lang":"en","id":"flattened-error-reporting","sortIndex":3,"hash":"f62f4ff309c8a67013ae831386ae42694de363abd841e06f928af91e51a6be28480688b03f3062a94fe6b8575ab76a0b3f4e73f9d122d54b1ac70c44790ef2ca","compilerSettings":{},"code":"// TypeScript's error messages can sometimes be a tad verbose...\n// With 3.7, we've taken a few cases which could be particularly\n// egregious.\n\n// Nested Properties\n\nlet a = { b: { c: { d: { e: \"string\" } } } };\nlet b = { b: { c: { d: { e: 12 } } } };\n\na = b;\n\n// Before, it was 2 lines of code per nested property, which\n// quickly meant people learned to read error messages by\n// reading the first and then last line of an error message.\n\n// Now they're inline. :tada:\n\n// Previously in 3.6:\n//\n// Type '{ b: { c: { d: { e: number; }; }; }; }' is not assignable to type '{ b: { c: { d: { e: string; }; }; }; }'.\n// Types of property 'b' are incompatible.\n// Type '{ c: { d: { e: number; }; }; }' is not assignable to type '{ c: { d: { e: string; }; }; }'.\n// Types of property 'c' are incompatible.\n// Type '{ d: { e: number; }; }' is not assignable to type '{ d: { e: string; }; }'.\n// Types of property 'd' are incompatible.\n// Type '{ e: number; }' is not assignable to type '{ e: string; }'.\n// Types of property 'e' are incompatible.\n// Type 'number' is not assignable to type 'string'\n\n// This can handle working through different types of objects,\n// to still give a useful and concise error message.\n\nclass ExampleClass {\n state = \"ok\";\n}\n\nclass OtherClass {\n state = 12;\n}\n\nlet x = { a: { b: { c: { d: { e: { f: ExampleClass } } } } } };\nlet y = { a: { b: { c: { d: { e: { f: OtherClass } } } } } };\nx = y;\n\n// Previously in 3.6:\n//\n// Type '{ a: { b: { c: { d: { e: { f: typeof OtherClass; }; }; }; }; }; }' is not assignable to type '{ a: { b: { c: { d: { e: { f: typeof ExampleClass; }; }; }; }; }; }'.\n// Types of property 'a' are incompatible.\n// Type '{ b: { c: { d: { e: { f: typeof OtherClass; }; }; }; }; }' is not assignable to type '{ b: { c: { d: { e: { f: typeof ExampleClass; }; }; }; }; }'.\n// Types of property 'b' are incompatible.\n// Type '{ c: { d: { e: { f: typeof OtherClass; }; }; }; }' is not assignable to type '{ c: { d: { e: { f: typeof ExampleClass; }; }; }; }'.\n// Types of property 'c' are incompatible.\n// Type '{ d: { e: { f: typeof OtherClass; }; }; }' is not assignable to type '{ d: { e: { f: typeof ExampleClass; }; }; }'.\n// Types of property 'd' are incompatible.\n// Type '{ e: { f: typeof OtherClass; }; }' is not assignable to type '{ e: { f: typeof ExampleClass; }; }'.\n// Types of property 'e' are incompatible.\n// Type '{ f: typeof OtherClass; }' is not assignable to type '{ f: typeof ExampleClass; }'.\n// Types of property 'f' are incompatible.\n// Type 'typeof OtherClass' is not assignable to type 'typeof ExampleClass'.\n// Type 'OtherClass' is not assignable to type 'ExampleClass'.\n// Types of property 'state' are incompatible.\n// Type 'number' is not assignable to type 'string'"},{"path":["3-7","Syntax and Messaging"],"title":"Nullish Coalescing","name":"Nullish Coalescing.ts","lang":"en","id":"nullish-coalescing","sortIndex":2,"hash":"31115c13c13f4664b1193f32d487e93f6ecd30324f7f008173e1bd08fb6164f9ba48d0cf915a5dff5f66144cf067fe4883f5f1b72da9866aca93f99af77e88a5","compilerSettings":{},"code":"// The nullish coalescing operator is an alternative to ||\n// which returns the right-side expression if the left-side\n// is null or undefined.\n\n// In contrast, || uses falsy checks, meaning an empty\n// string or the number 0 would be considered false.\n\n// A good example for this feature is dealing with partial\n// objects which have defaults when a key isn't passed in.\n\ninterface AppConfiguration {\n // Default: \"(no name)\"; empty string IS valid\n name: string;\n\n // Default: -1; 0 is valid\n items: number;\n\n // Default: true\n active: boolean;\n}\n\nfunction updateApp(config: Partial) {\n // With null-coalescing operator\n config.name = config.name ?? \"(no name)\";\n config.items = config.items ?? -1;\n config.active = config.active ?? true;\n\n // Current solution\n config.name = typeof config.name === \"string\" ? config.name : \"(no name)\";\n config.items = typeof config.items === \"number\" ? config.items : -1;\n config.active = typeof config.active === \"boolean\" ? config.active : true;\n\n // Using || operator which could give bad data\n config.name = config.name || \"(no name)\"; // does not allow for \"\" input\n config.items = config.items || -1; // does not allow for 0 input\n config.active = config.active || true; // really bad, always true\n}\n\n// You can read more about nullish coalescing in the 3.7 blog post:\n//\n// https://devblogs.microsoft.com/typescript/announcing-typescript-3-7/"},{"path":["3-7","Syntax and Messaging"],"title":"Optional Chaining","name":"Optional Chaining.ts","lang":"en","id":"optional-chaining","sortIndex":1,"hash":"f220872faae83af0283838d6f91fb7335d29557f0a4461261fb105b3db773d6323d7e28d8dfe253ebc68718954ab6f37794c0095715182c4cdbfa9c13f1c559b","compilerSettings":{},"code":"// Optional chaining reached TC39 Stage 3 consensus during\n// 3.7's development. Optional Chaining allows you to write\n// code which can immediately stop running expressions when\n// it hits a null or undefined.\n\n// Property Access\n\n// Let's imagine we have an album where the artist, and the\n// artists bio might not be present in the data. For example\n// a compilation may not have a single artist.\n\ntype AlbumAPIResponse = {\n title: string;\n artist?: {\n name: string;\n bio?: string;\n previousAlbums?: string[];\n };\n};\n\ndeclare const album: AlbumAPIResponse;\n\n// With optional chaining, you can write\n// code like this:\n\nconst artistBio = album?.artist?.bio;\n\n// Instead of:\n\nconst maybeArtistBio = album.artist && album.artist.bio;\n\n// In this case ?. acts differently than the &&s since &&\n// will act differently on \"falsy\" values (e.g. an empty string,\n// 0, NaN, and, well, false).\n\n// Optional chaining will only take null or undefined as\n// a signal to stop and return an undefined.\n\n// Optional Element Access\n\n// Property access is via the . operator, the optional chaining\n// also works with the [] operators when accessing elements.\n\nconst maybeArtistBioElement = album?.[\"artist\"]?.[\"bio\"];\n\nconst maybeFirstPreviousAlbum = album?.artist?.previousAlbums?.[0];\n\n// Optional Calls\n\n// When dealing with functions which may or may not exist at\n// runtime, optional chaining supports only calling a function\n// if it exists. This can replace code where you would traditionally\n// write something like: if (func) func()\n\n// For example here's an optional call to the callback from\n// an API request:\n\nconst callUpdateMetadata = (metadata: any) => Promise.resolve(metadata); // Fake API call\n\nconst updateAlbumMetadata = async (metadata: any, callback?: () => void) => {\n await callUpdateMetadata(metadata);\n\n callback?.();\n};\n\n// You can read more about optional chaining in the 3.7 blog post:\n//\n// https://devblogs.microsoft.com/typescript/announcing-typescript-3-7/"},{"path":["3-7","Types and Code Flow"],"title":"Assertion Functions","name":"Assertion Functions.ts","lang":"en","id":"assertion-functions","sortIndex":1,"hash":"f76c42c29fedc10c1863d536ff551354b9ba563e4707f569beb122d59324349c17650461e5c9b462a765ca3690df9a238c51d17b400bc659b390c429b16168c9","compilerSettings":{},"code":"// Given JavaScript's flexibility, it can be a good idea to add\n// runtime checks to your code to validate your assumptions.\n\n// These are typically called assertions (or invariants) and\n// they are small functions which raise errors early when\n// your variables don't match up to what you expect.\n\n// Node comes with a function for doing this out of the box,\n// it's called assert and it's available without an import.\n\n// We're going to define our own though. This declares a\n// function which asserts that the expression called\n// value is true:\ndeclare function assert(value: unknown): asserts value;\n\n// Now we're use it to validate the type of an enum\ndeclare const maybeStringOrNumber: string | number;\nassert(typeof maybeStringOrNumber === \"string\");\n\n// With TypeScript 3.7, the code flow analysis can use these\n// types of functions to figure out what the code is. So,\n// when you hover over the variable below - you can see that\n// it has been narrowed from a string or number to\n// just a string.\n\nmaybeStringOrNumber;\n\n// You can use assertion functions to make guarantees of\n// your types throughout your inferred code, for example\n// TypeScript knows that this function will return a\n// number without the need to add types to the parameter\n// via the above assert declaration.\n\nfunction multiply(x: any, y: any) {\n assert(typeof x === \"number\");\n assert(typeof y === \"number\");\n\n return x * y;\n}\n\n// Assertion functions are siblings to Type Guards\n// example:type-guards except they affect the control flow\n// when it continues through the function.\n\n// For example, we can use assertion functions to narrow\n// an enum down over time:\n\ndeclare const oneOfFirstFiveNumbers: 1 | 2 | 3 | 4 | 5;\n\ndeclare function isOdd(param: unknown): asserts param is 1 | 3 | 5;\ndeclare function isBelowFour(param: unknown): asserts param is 1 | 2 | 3 | 4;\n\n// This should cut down the enum to: 1 | 3 | 5\n\nisOdd(oneOfFirstFiveNumbers);\noneOfFirstFiveNumbers;\n\n// This will then cut the enum's possible states to: 1 | 3\n\nisBelowFour(oneOfFirstFiveNumbers);\noneOfFirstFiveNumbers;\n\n// This is a primer on some of the features of assertion functions\n// in TypeScript 3.7 - you can find out more by reading the\n// release notes:\n//\n// https://devblogs.microsoft.com/typescript/announcing-typescript-3-7/"},{"path":["3-7","Types and Code Flow"],"title":"Recursive Type References","name":"Recursive Type References.ts","lang":"en","id":"recursive-type-references","sortIndex":2,"hash":"ec8662cfd23f3b41cb4792f3e19d18e0cc4de289fed6f6abb9eb654840196146f1e47d568278291f147d06ef0f7a6c08905d996bb203d089bf2730c7a54fa1f3","compilerSettings":{},"code":"// Choosing between using type vs interface is about the\n// constraints in the features for each. With 3.7, one of\n// the constrains on type but not in interface was removed.\n\n// You can find out more about this in example:types-vs-interfaces\n\n// It used to be that you could not refer to the type you\n// are defining inside the type itself. This was a limit\n// which didn't exist inside an interface, and could be worked\n// around with a little work.\n\n// For example, this is not feasible in 3.6:\ntype ValueOrArray = T | Array>;\n\n// An implementation would have looked like this, by mixing\n// the type with an interface.\ntype ValueOrArray2 = T | ArrayOfValueOrArray;\ninterface ArrayOfValueOrArray extends Array> { }\n\n// This allows for a comprehensive definition of JSON,\n// which works by referring to itself.\n\ntype Json = string | number | boolean | null | Json[] | { [key: string]: Json };\n\nconst exampleStatusJSON: Json = {\n available: true,\n username: \"Jean-loup\",\n room: {\n name: \"Highcrest\",\n // Cannot add functions into the Json type\n // update: () => {}\n },\n};\n\n// There's more to learn from the 3.7 beta release notes and its PR:\n//\n// https://devblogs.microsoft.com/typescript/announcing-typescript-3-7/\n// https://github.com/microsoft/TypeScript/pull/33050"},{"path":["3-7","Types and Code Flow"],"title":"Uncalled Function Checks","name":"Uncalled Function Checks.ts","lang":"en","id":"uncalled-function-checks","sortIndex":1,"hash":"41988fd3c184cb01ef29522c39cd97ca1c054d3f475a712aae56a572d02eb880681f22e26a8d4092d45bf0da8f859ef9e6cdb4c039a48c23662c09b7365840f0","compilerSettings":{},"code":"// New to 3.7 is a check inside if statements for\n// when you accidentally use a function instead\n// of the return value of a function.\n\n// This only applies when the function is known\n// to exist making the if statement always be true.\n\n// Here is an example plugin interface, where there\n// are optional and non-optional callbacks.\n\ninterface PluginSettings {\n pluginShouldLoad?: () => void;\n pluginIsActivated: () => void;\n}\n\ndeclare const plugin: PluginSettings;\n\n// Because pluginShouldLoad could not exist, then\n// the check is legitimate.\n\nif (plugin.pluginShouldLoad) {\n // Do something when pluginShouldLoad exists.\n}\n\n// In 3.6 and below, this was not an error.\n\nif (plugin.pluginIsActivated) {\n // Want to do something when the plugin is activated,\n // but instead of calling the method we used it as a\n // property.\n}\n\n// pluginIsActivated should always exist, but TypeScript\n// still allows the check, because the method is called\n// inside the if block.\n\nif (plugin.pluginIsActivated) {\n plugin.pluginIsActivated();\n}"},{"path":["3-8","Breaking Changes"],"title":"Checking Unions with Index Signatures","name":"Checking Unions with Index Signatures.ts","lang":"en","id":"checking-unions-with-index-signatures","sortIndex":1,"hash":"16e0b30e338d71415764867850049b940a9d362145ea871e64d738c3231e743a475c9bbf7596787c3a32441c23c0deede011fa763a32cd4fc5afd618ebbb3bfe","compilerSettings":{"ts":"3.8.3"},"code":"// In previous versions of TypeScript, the checker would not\n// verify that undeclared fields in a union conform to any indexed\n// types in the union.\n\n// You can learn about indexed types here: example:indexed-types\n\n// For example, the IdentifierCache below indicates that any\n// key on the object will be a number:\n\ntype IdentifierCache = { [key: string]: number };\n\n// Meaning this will fail, because 'file_a' has a\n// string value\n\nconst cacheWithString: IdentifierCache = { file_a: \"12343\" };\n\n// However, when you put that into a union, then the\n// validation check would not happen:\n\nlet userCache: IdentifierCache | { index: number };\nuserCache = { file_one: 5, file_two: \"abc\" };\n\n// This is fixed, and there would be an error about\n// 'file_two' from the compiler.\n\n// This also takes into account when the key is a different\n// type, for example: ([key: string] and [key: number])\n\ntype IdentifierResponseCache = { [key: number]: number };\n\nlet resultCache: IdentifierCache | IdentifierResponseCache;\nresultCache = { file_one: \"abc\" };"},{"path":["3-8","JSDoc Improvements"],"title":"Accessibility Modifiers","name":"Accessibility Modifiers.js","lang":"en","id":"accessibility-modifiers","sortIndex":1,"hash":"98a7a7972c4ba777335bff6490370a3b23815c8015eeae5dfe1fc09c09abe9e09863fb0b16bcf8dd3b6e87a99387b7dce3e436892ddbface3e8998ed8a2e63b2","compilerSettings":{"ts":"3.8.3"},"code":"// @ts-check\n\n// The JSDoc support for TypeScript was extended to support\n// the accessibility modifiers on class properties. There is:\n//\n// @public - the default, and what happens if you don't set one\n// @private - the field can only be accessed in the same class\n// where the field is defined\n// @protected - the field is accessible to the class where it is\n// defined and subclasses of that class\n//\n\n// This is a base class of Animal, it has both a private and\n// protected field. Subclasses can access \"this.isFast\" but\n// not \"this.type\".\n\n// Outside of these the class, both of these fields are not\n// visible and return a compiler error when // @ts-check is\n// turned on:\n\nclass Animal {\n constructor(type) {\n /** @private */\n this.type = type;\n /** @protected */\n this.isFast = type === \"cheetah\";\n }\n\n makeNoise() {\n // Supposedly these are pretty much silent\n if (this.type === \"bengal\") {\n console.log(\"\");\n } else {\n throw new Error(\"makeNoise was called on a base class\");\n }\n }\n}\n\nclass Cat extends Animal {\n constructor(type) {\n super(type || \"housecat\");\n }\n\n makeNoise() {\n console.log(\"meow\");\n }\n\n runAway() {\n if (this.isFast) {\n console.log(\"Got away\");\n } else {\n console.log(\"Did not make it\");\n }\n }\n}\n\nclass Cheetah extends Cat {\n constructor() {\n super(\"cheetah\");\n }\n}\n\nclass Bengal extends Cat {\n constructor() {\n super(\"bengal\");\n }\n}\n\nconst housecat = new Cat();\nhousecat.makeNoise();\n\n// These are not available\nhousecat.type;\nhousecat.isFast;\n\n// You can read more in the post\n// https://devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/#jsdoc-modifiers"},{"path":["3-8","Syntax and Messaging"],"title":"Export Modules From","name":"Export Modules From.ts","lang":"en","id":"export-modules-from","sortIndex":1,"hash":"fa8164eb6bdbaaae0185953166cf29e7432f4acb2098aeb1e6eb557b874497a10e915135490478f95396e061d1263e7c840630d26dbdd832ed6f6d9be72f7866","compilerSettings":{"ts":"3.8.3"},"code":"// TypeScripts ability to re-export got closer to supporting\n// the additional cases available in ES2018\n//\n// JavaScript exports have the ability to elegantly\n// re-export a part of a dependency:\n\nexport { ScriptTransformer } from \"@jest/transform\";\n\n// When you wanted to export the full object, that\n// becomes a little more verbose in previous versions\n// of TypeScript:\n\nimport * as console from \"@jest/console\";\nimport * as reporters from \"@jest/reporters\";\n\nexport { console, reporters };\n\n// With 3.8, TypeScript supports more of the export\n// statement forms in the JavaScript specs, letting\n// you write a single line to re-export a module\n\nexport * as jestConsole from \"@jest/console\";\nexport * as jestReporters from \"@jest/reporters\";"},{"path":["3-8","Syntax and Messaging"],"title":"Private Class Fields","name":"Private Class Fields.ts","lang":"en","id":"private-class-fields","sortIndex":1,"hash":"0e9828ae12056988a4edca22495672ad69c3ec317273ac20cc7553b5b4c387ccd19be6ce7f16ffc0f34a9937a3ea1aa644002c7b8986835522b599049db4bfc3","compilerSettings":{"ts":"3.8.3"},"code":"// 3.8 adds private fields, which are a way of declaring a class field to\n// be unavailable outside of the containing class, including to subclasses.\n\n// For example, the Person class below does not allow for anyone using an\n// instance of the class to read the firstName, lastName or prefix\n\nclass Person {\n #firstName: string;\n #lastName: string;\n #prefix: string;\n\n constructor(firstName: string, lastName: string, prefix: string) {\n this.#firstName = firstName;\n this.#lastName = lastName;\n this.#prefix = prefix;\n }\n\n greet() {\n // In iceland it is preferable to use a full name instead of [prefix] [lastname]\n // https://www.w3.org/International/questions/qa-personal-names#patronymic\n if (navigator.languages[0] === \"is\") {\n console.log(`Góðan dag, ${this.#firstName} ${this.#lastName}`);\n } else {\n console.log(`Hello, ${this.#prefix} ${this.#lastName}`);\n }\n }\n}\n\nlet jeremy = new Person(\"Jeremy\", \"Bearimy\", \"Mr\");\n\n// You can't get to any of the private fields from outside that class:\n\n// For example, this won't work:\nconsole.log(jeremy.#lastName);\n\n// Nor this:\nconsole.log(\"Person's last name:\", jeremy[\"#lastName\"]);\n\n// A common question we get is \"Why would you use this over the 'private'\n// keyword in a classfield?\" - let's look by making a comparison to\n// how it worked in TypeScript before 3.8:\n\nclass Dog {\n private _name: string;\n constructor(name: string) {\n this._name = name;\n }\n}\n\nlet oby = new Dog(\"Oby\");\n// Won't let you access via dot notation\noby._name = \"Spot\";\n// But TypeScript allows bracket notation as an escape clause\noby[\"_name\"] = \"Cherny\";\n\n// The TypeScript reference of private only exists at type-level\n// which means that you can only trust it so far. With private fields\n// soon to be a part of the JavaScript language, then you can make better\n// guarantees about the visibility of your code.\n\n// We don't plan on deprecating the `private` field keyword\n// in TypeScript, so your existing code will continue to work, but now\n// you can write code which is closer to the JavaScript language instead.\n\n// You can learn more about class fields in the tc39 proposal\n// https://github.com/tc39/proposal-class-fields/\n// and the beta release notes:\n// https://devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/#ecmascript-private-fields"},{"path":["3-8","Syntax and Messaging"],"title":"Type Imports","name":"Type Imports.tsx","lang":"en","id":"type-imports","sortIndex":1,"hash":"e6695e61e3fc69dd376eeb344a40dde3ec2c46479d9df14d0484365c8a6a35e40b66b1f3997d8ba25d84840524edfe87604caae63fb9ffd62c8793a07464c8aa","compilerSettings":{"ts":"3.8.3"},"code":"// In 3.8 we added new syntax for importing types, which\n// would be similar to users who have come from flow.\n\n// 'import type' provides a way to declare a type only import\n// which means you can be sure that the code will erased when\n// converting to JavaScript in a very predictable way because\n// it will always be removed!\n\n// For example, this line will never add an import or require\nimport type { CSSProperties } from \"react\";\n\n// Which is used here as a type\nconst style: CSSProperties = {\n textAlign: \"center\",\n};\n\n// This is in contrast to this import:\nimport * as React from \"react\";\n\n// Which will be included in the JavaScript\nexport class Welcome extends React.Component {\n render() {\n return (\n \n
Hello, world \n \n );\n }\n}\n\n// However, if the 'import' without types, only imports\n// types - it could also be removed. If you look in the\n// compiled JS output, this import is not included\n\nimport { FunctionComponent } from \"react\";\n\nexport const BetaNotice: FunctionComponent = () => {\n return This page is still in beta
;\n};\n\n// This is called import elision, and it can be the source\n// of confusion. The syntax 'import type' allows you to be\n// specific about what you want in JavaScript.\n\n// This is a small overview for one of the main use cases\n// for 'import types' but there are more which you can read\n// in the 3.8 release notes\n\n// https://devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/#type-only-imports-exports"},{"path":["4-0","New Checks"],"title":"Class Constructor Code Flow","name":"Class Constructor Code Flow.ts","lang":"en","id":"class-constructor-code-flow","sortIndex":1,"hash":"54d7a7ead2770e6e554ed562b5a3f5d4fbdcb450f6796dc402f10e69d681661dafcd8c8cdb485643f4dda513a01c3d0ae69f0cb29179c23ca61af65f01e65a97","compilerSettings":{"ts":"4.0.2"},"code":"// In 4.0, we use control flow analysis to\n// infer the potential type of a class property based on\n// what values are set during the constructor.\n\nclass UserAccount {\n id; // Type is inferred as string | number\n constructor(isAdmin: boolean) {\n if (isAdmin) {\n this.id = \"admin\";\n } else {\n this.id = 0;\n }\n }\n}\n\n// In previous versions of TypeScript, `id` would\n// have been classed as an `any`."},{"path":["4-0","New JS Features"],"title":"JSDoc Deprecated","name":"JSDoc Deprecated.ts","lang":"en","id":"jsdoc-deprecated","sortIndex":1,"hash":"a61596eff52d1e181b4857c55298b0276001ff014cfcbe5d24bded709cb573db810760fa7f2a58633de8f1f2b52c6050d17b0a5e484f9eb288d4e3da3b0947eb","compilerSettings":{},"code":"// In 4.0 the JSDoc tag @deprecated is added to the\n// type system. You can use @deprecated anywhere\n// you can use JSDoc currently.\n\ninterface AccountInfo {\n name: string;\n gender: string;\n\n /** @deprecated use gender field instead */\n sex: \"male\" | \"female\";\n}\n\ndeclare const userInfo: AccountInfo;\nuserInfo.sex;\n\n// TypeScript will offer a non-blocking warning when a\n// deprecated property is accessed, and editors like\n// vscode will use show the deprecated info in places\n// like intellisense, outlines and in your code.\n"},{"path":["4-0","New JS Features"],"title":"Logical Operators and Assignment","name":"Logical Operators and Assignment.ts","lang":"en","id":"logical-operators-and-assignment","sortIndex":1,"hash":"5e5169c5f07cc447f16d95a22a415903328bf4e40b33de234ab89a90adf33ad432e56c1e8cafb94c380baae11c6c4aa0e88912367d669ddb68eff257b6bef35a","compilerSettings":{},"code":"// Logical Operators and Assignment are new features in\n// JavaScript for 2020. These are a suite of new operators\n// which edit a JavaScript object.\n\n// Their goal is to re-use the concept of mathematical \n// operators (e.g. += -= *=) but with logic instead.\n\ninterface User {\n id?: number\n name: string\n location: {\n postalCode?: string\n }\n}\n\nfunction updateUser(user: User) {\n // This code can be replaced \n if (!user.id) user.id = 1\n\n // Or this code:\n user.id = user.id || 1\n\n // With this code:\n user.id ||= 1\n}\n\n// The suites of operators can handle deeply nesting, which \n// can save on quite a lot of boilerplate code too.\n\ndeclare const user: User\nuser.location.postalCode ||= \"90210\"\n\n// There are three new operators: \n//\n// ||= shown above\n// &&= which uses 'and' logic instead of 'or'\n// ??= which builds on example:nullish-coalescing to offer a stricter\n// version of || which uses === instead\n\n// For more info on the proposal, see:\n// https://github.com/tc39/proposal-logical-assignment\n"},{"path":["4-0","New JS Features"],"title":"Nullish Coalescing","name":"Nullish Coalescing.ts","lang":"en","id":"nullish-coalescing","sortIndex":1,"hash":"46f56e1c25cff8d8e4b085f8806c3462b0d964d5b587c71ebf9dd6e778ed253e03c7f96f42b79f9669202adc93e7f29afe1c716d7eddb4a32af57d96a5123ae9","compilerSettings":{},"code":"// # Nullish Coalescing\n//\n// This is a new operator `??` which is meant to augment\n// the common usage of `||` in the same way `===` augments `==`\n// as a more strict form of equality.\n//\n// To understand it, let's look a how || works:\n\nconst response = {\n nullValue: null,\n headerText: \"\",\n animationDuration: 0,\n height: 400,\n showSplashScreen: false,\n} as const;\n\nconst undefinedValue = response.undefinedValue || \"some other default\";\n// This would be: 'some other default'\n\nconst nullValue = response.nullValue || \"some other default\";\n\n// These two examples work similar in most languages. As a tool || is\n// pretty good at defaulting things, but JavaScript's falsy checks\n// can surprise you for some common values:\n\n// Potentially unintended. '' is falsy, result: 'Hello, world!'\nconst headerText = response.headerText || \"Hello, world!\";\n\n// Potentially unintended. 0 is falsy, result: 300\nconst animationDuration = response.animationDuration || 300;\n\n// Potentially unintended. false is falsy, result: true\nconst showSplashScreen = response.showSplashScreen || true;\n\n// When switching to use ?? instead, then === equality is used\n// to compare the two sides:\n\nconst emptyHeaderText = response.headerText ?? \"Hello, world!\";\nconst zeroAnimationDuration = response.animationDuration ?? 300;\nconst skipSplashScreen = response.showSplashScreen ?? true;\n"},{"path":["4-0","New TS Features"],"title":"Named Tuples","name":"Named Tuples.ts","lang":"en","id":"named-tuples","sortIndex":1,"hash":"ddaee350b2cb2004c7ade0bb99ecd6464088fbbdf4b5ef5b6e054d94f9ed75a9944d05c9230feb0e5470c9e13b83043007e2284854aebad80af8bbeb9127c27b","compilerSettings":{"ts":"4.0.2"},"code":"// Tuples are arrays where the order is important to the type system,\n// you can learn more about them in example:tuples\n\n// In TypeScript 4.0, the type of a Tuple's gained the ability to give\n// a name to the different parts of the array.\n\n// For example, you used to write a Lat Long location via a tuple:\n\ntype OldLocation = [number, number]\n\nconst locations: OldLocation[] = [\n [40.7144, -74.006],\n [53.6458, -1.785]\n]\n\n// Knowing which is Latitude and Longitude is ambiguous, and so you\n// would more likely have called it a LatLong tuple.\n\n// With 4.0, you can write:\n\ntype NewLocation = [lat: number, long: number]\n\nconst newLocations: NewLocation[] = [\n [52.3702, 4.8952],\n [53.3498, -6.2603]\n]\n\n// The names now show up in the editor when you hover over\n// the 0 and 1 at the end of the next line\nconst firstLat = newLocations[0][0]\nconst firstLong = newLocations[0][1]\n\n// While that might seem a tad underwhelming, the main goal \n// is to ensure that information isn't lost when working\n// with the type system. For example, when extracting\n// parameters from a function using the Parameter \n// utility type:\n\nfunction centerMap(lng: number, lat: number) { }\n\n// In 4.0, this keeps lng and lat\ntype CenterMapParams = Parameters\n\n// In 3.9, this would look like\ntype OldCenterMapParams = [number, number]\n\n// Making some of the more complex type manipulation lossy\n// for the parameter information."},{"path":["4-0","New TS Features"],"title":"Unknown in Catch","name":"Unknown in Catch.ts","lang":"en","id":"unknown-in-catch","sortIndex":1,"hash":"652e7edb648a13b373910604a20f286978f350090de50a8c038bfe833c197a8f59ea8a387be26fb95e15df561399ccba553581999db9702fb405aa28b5a7d071","compilerSettings":{"ts":"4.0.2"},"code":"// Because JavaScript allows throwing any value, TypeScript\n// does not support declaring the type of an error\n\ntry {\n // ..\n} catch (e) { }\n\n// Historically, this has meant that the `e` in the catch\n// would default to any. This allowed for the freedom to\n// arbitrarily access any property. With 4.0, we've loosened\n// the restrictions on type assignment in the catch clause\n// to allow both `any` and `unknown`.\n\n// Same behavior with any:\ntry {\n // ..\n} catch (e) {\n e.stack;\n}\n\n// Explicit behavior with unknown:\n\ntry {\n // ..\n} catch (e: unknown) {\n // You cannot use `e` at all until the type\n // system learns what it is, for more info see:\n // example:unknown-and-never\n e.stack;\n\n if (e instanceof SyntaxError) {\n e.stack;\n }\n}"},{"path":["4-0","New TS Features"],"title":"Variadic Tuples","name":"Variadic Tuples.ts","lang":"en","id":"variadic-tuples","sortIndex":1,"hash":"d88a897abbbac99720fdd9ecc60c650963a0e6bd3b5be2626bf41f0222e91e504b7ba50e4054dada565a093d8d67efd0e8ddc6aa11f12f36572f80144c57db6a","compilerSettings":{"ts":"4.0.2"},"code":"// Variadic Tuples gives tuples the ability to handle the rest operator (...)\n// to pass types through type checker in a way that works like generics.\n\n// This is quite an advanced topic, so if you get lost do not worry too much.\n// It builds on example:generic-functions and example:tuples\n\n// To start off, here is a variadic tuple which will always prefix another\n// tuple with a number:\n\ntype AddMax = [max: number, ...rest: T];\n// ^ Generic used to constrain the T\n// ^ ... used to indicate where to merge\n\n// This can then be used for composition:\ntype MaxMin = AddMax<[min: number]>\ntype MaxMinDiameter = AddMax<[min: number, diameter: number]>\n\n// The same can be used after the tuple:\ntype SuffixDIContext = [...first: T, context: any];\ntype DIContainer = SuffixDIContext<[param: string]>\n\n// This mechanism can be combined with multiple input params. For example, this\n// function merges two arrays but uses '\\0' as a sigil to indicate where the arrays \n// start and stop.\nfunction joinWithNullTerminators(t: [...T], u: [...U]) {\n return ['\\0', ...t, '\\0', ...u, '\\0'] as const;\n}\n\n// TypeScript can infer the return type of a function like this:\nconst result = joinWithNullTerminators(['variadic', 'types'], [\"terminators\", 3]);\n\n// These tools make it possible to correctly type a function like curry which\n// is a well used concept in functional programming:\n\nfunction curry(f: (...args: [...T, ...U]) => R, ...a: T) {\n return (...b: U) => f(...a, ...b);\n}\n\n// There are three generic arguments:\n// - T: The params which are array of inputs to the curry function\n// - U: The parameters which _aren't_ passed into to curry function, and need applying to the return func\n// - R: the return type of the passed in function\n\nconst sum = (left: number, right: number,) => left + right\n\nconst a = curry(sum, 1, 2)\nconst b = curry(sum, 1)(2)\nconst c = curry(sum)(1, 2)\n\n// You can find a more in-depth explanation, with more code samples in\n// https://github.com/microsoft/TypeScript/pull/39094"},{"path":["4-1","New JS Features"],"title":"New JSX Support","name":"New JSX Support.tsx","lang":"en","id":"new-jsx-support","sortIndex":1,"hash":"a3d9a643f7a0616ca4dd26d4366f7082ba3ca8cac66d93a84a8609ecd5a9b53e54625f01c54b578d92d974ee46f4a3bfa05e9b2b1f07dfeadffd8185d0531edd","compilerSettings":{"ts":"4.1.0-dev.20201028","jsx":4},"code":"// In version 17, the React team introduced a new format\n// for the JavaScript emitted by JSX transforms. You\n// can see the JavaScript in the right hand side of\n// the playground in the \".JS\" tab ->\n\nimport { useState } from \"react\";\n\nexport function ExampleApp() {\n const [count, setCount] = useState(0);\n\n return (\n \n
You clicked {count} times
\n
setCount(count + 1)}> Click me \n
\n );\n}\n\n// The some of the main changes:\n//\n// - Use an `import` to provide functions instead of a React identifier\n// - Different functions for a single element (jsx) vs many children (jsxs)\n// - Key is separate from the props\n//\n// You can read the RFC which this change implements here\n// https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md\n\n// These changes are mostly under-the-hood changes\n// which shouldn't affect you you write JSX code as\n// an end user."},{"path":["4-1","New JS Features"],"title":"See in JSDoc","name":"See in JSDoc.ts","lang":"en","id":"see-in-jsdoc","sortIndex":1,"hash":"9b8c2fcc85a5921c6225f60548665fb48baba59b4c3dcd6289d500a630ff56f52388366bbd465f73717c0d8e0b9f823f6f7be0915059c126206a5faf05107d88","compilerSettings":{"ts":"4.1.0-beta"},"code":"// With 4.1 the JSDoc parser in TypeScript\n// used in both JavaScript and TypeScript files\n// supports the @see parameter.\n\n// You can use the @see to help people quickly\n// jump to other related code via clicking\n// (cmd/ctrl + clicking) or getting hover info\n\n/**\n * @see hello\n */\nconst goodbye = \"Good\";\n\n/**\n * You say hi, I say low\n *\n * @see goodbye\n */\nconst hello = \"Hello, hello\";"},{"path":["4-1","Template Literals"],"title":"Intro to Template Literals","name":"Intro to Template Literals.ts","lang":"en","id":"intro-to-template-literals","sortIndex":1,"hash":"e57d1bcc716fe422bf16b64d41b921f0f36222511a287b361166445968405272c5e99d732c07b23613cf67c1bd332cd408e3c3fd296ab5817a522a6bce7cbdb5","compilerSettings":{"ts":"4.1.0-dev.20201028"},"code":"// TypeScript already supports treating an exact string/number \n// as a literal, for example this function only allows two\n// exact strings and no others:\n\ndeclare function enableFeature(command: \"redesign\" | \"newArtistPage\"): void;\nenableFeature(\"redesign\");\nenableFeature(`newArtistPage`);\nenableFeature(\"newPaymentSystem\");\n\n// String literals supports all the way you can write a \n// string in ES2020, with TypeScript 4.1 we've extended \n// support for interpolation inside a template string literal.\n\ntype Features = \"Redesign\" | \"newArtistPage\";\n\n// This takes the Features union above, and transforms\n// each part of the union to add `-branch` after the string\ntype FeatureBranch = `${Features}-branch`;\n\n// 4.1 supports a set of new generic-like keywords which\n// you can use inside a template literal to manipulate strings.\n// These are: Uppercase, Lowercase, Capitalize and Uncapitalize\n\ntype FeatureID = `${Lowercase}-id`;\ntype FeatureEnvVar = `${Uppercase}-ID`;\n\n// Strings in unions are cross multiplied, so if used more\n// than one union type then each union member is evaluated\n// against each member from the other union.\n\ntype EnabledStates = \"enabled\" | \"disabled\";\ntype FeatureUIStrings = `${Features} is ${EnabledStates}`;\n\n// This ensures that every possible combination of each\n// union is accounted for.\n\n// This type can then be used with an indexed signature\n// to quickly make a list of keys:\n\ntype SetFeatures = {\n [K in FeatureID]: boolean\n};\n\n// Continue learning more about template literals in\n// example:mapped-types-with-template-literals\n\n// Or read the announcement blog post:\n// https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#template-literal-types"},{"path":["4-1","Template Literals"],"title":"Mapped Types with Template Literals","name":"Mapped Types with Template Literals.ts","lang":"en","id":"mapped-types-with-template-literals","sortIndex":1,"hash":"0b79bf15704e013436f7aad33900f711bd354dad463ca49341472d175e41c538cb8f8604e8336898d1d2b6742dfd6e8b5bfbbbde0364d2275c9c075150282435","compilerSettings":{"ts":"4.1.0-dev.20201028"},"code":"// TypeScript 4.1 added support for template literals, you can\n// understand some of the basics in example:intro-to-template-literals\n\n// 4.1 introduces new syntax inside a mapped types declaration,\n// you can now use \"as `templated string`\" which can be used to transform\n// strings inside a union.\n\n// For example, this type will transform all of the properties of an existing\n// type into four functions which correspond to traditional REST calls.\n\n// Template strings literals to describe each API endpoint:\ntype GET = `get${Capitalize}`\ntype POST = `post${Capitalize}`\ntype PUT = `put${Capitalize}`\ntype DELETE = `delete${Capitalize}`\n\n// A union of the above literal types\ntype REST = GET | POST | PUT | DELETE\n\n// Takes a type, then for each string property in the type, map\n// that key to REST above, which would create the four functions.\n\ntype RESTify = {\n [Key in keyof Type as REST]: () => Type[Key]\n};\n\n// The `Key extends string ? Key : never` is needed because an object\n// can contain strings, numbers and symbols as keys. We can only handle\n// the string cases here.\n\n// Now we have a list of objects available through the API:\n\ninterface APIs {\n artwork: { id: string, title: string };\n artist: { id: string, name: string };\n location: { id: string, address: string, country: string }\n}\n\n// Then when we have an object which uses these types\ndeclare const api: RESTify\n\n// Then all these functions are automatically created \napi.getArtist()\napi.postArtist()\napi.putLocation()\n\n// Continue learning more about template literals in:\n// example:string-manipulation-with-template-literals\n\n// Or read the announcement blog post:\n// https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#template-literal-types"},{"path":["4-1","Template Literals"],"title":"String Manipulation with Template Literals","name":"String Manipulation with Template Literals.ts","lang":"en","id":"string-manipulation-with-template-literals","sortIndex":1,"hash":"c991fdd3b6bda4e1597c4613374782be2f902a929400447f52aeecdc10b968706a711b6be8dc71d689cf02d0931343ea5abaf4c1ee3f308b529ac174a3b24daf","compilerSettings":{"ts":"4.1.0-dev.20201028"},"code":"// Template literals can be used to extract and manipulate string literal types.\n// These string literal types, in turn, can be used as properties, and can describe\n// possible transformations from a string to an object in an API.\n\n// ## String Splitting To An Object\n\n// Template literals can use patterns as \"split-points\" to infer the\n// substrings in between. For example...\n\n// This type is a string literal which conforms to a SemVer-like string.\ntype TSVersion = \"4.1.2\"\n\n// We can create a type to extract the components of that string.\n// We'll split across two '.' characters.\ntype ExtractSemver =\n SemverString extends `${infer Major}.${infer Minor}.${infer Patch}` ?\n { major: Major, minor: Minor, patch: Patch } : { error: \"Cannot parse semver string\" }\n\n// Line 1 should be familiar if you've looked at the preceding examples:\n// example:intro-to-template-literals / example:mapped-types-with-template-literals\n\n// Line 2 is a conditional type, TypeScript validates that the infer pattern matches\n// against SemverString parameter.\n\n// Line 3 is the result of the conditional, if true then provide an object\n// with the substrings passed into different positions in an object. If the string\n// does not match, then return the type with an error shape.\n\ntype TS = ExtractSemver\n\n// This won't handle SemVer 100%, because it is an example:\ntype BadSemverButOKString = ExtractSemver<\"4.0.Four.4444\">\n\n// However, ExtractSemver will fail on strings which don't fit the format. This case\n// will only match when a string has the format \"X.Y.Z\", which the next line does not:\ntype SemverError = ExtractSemver<\"Four point Zero point Five\">\n\n// ## Recursive String Splitting\n\n// The previous example will only work when you have an exact string to match,\n// for more nuanced cases you want work with the TypeScript 4.0 feature: example:variadic-tuples.\n\n// To split a string into re-usable components, Tuples are a good way to keep\n// track of the results. Here's a split type:\n\ntype Split =\n string extends S ? string[] :\n S extends '' ? [] :\n S extends `${infer T}${D}${infer U}` ? [T, ...Split] : [S];\n\n// Line 1 declares two params, we'll use single characters for brevity.\n// S represents the string to split, and D is the deliminator. This\n// line ensures they are both strings.\n\n// Line 2 checks if string is a literal, by checking if a general string\n// can be extended from the input string. If so, return a string array. We\n// can't work with non-literal string.\n\n// E.g. this case:\ntype S1 = Split\n\n// Line 3 checks if the string is empty, if so return an empty tuple\ntype S2 = Split<\"\", \".\">\n\n// Line 4 has a similar check to our ExtractSemver. If the string matches\n// `[Prefix as T][Deliminator][Suffix as U]` then extract the prefix (T) into the\n// first parameter of a tuple, then re-run Split on the suffix (U) to ensure\n// that more than one match can be found.\n//\n// If the string does not include the deliminator, then return a tuple of 1 \n// length which contains the string passed in as an argument (S).\n\n// Simple case\ntype S3 = Split<\"1.2\", \".\">\n\n// Will recurse once to get all the .'s splitted\ntype S4 = Split<\"1.2.3\", \".\">\n\n// With this knowledge, you should be able to read and understand quite a\n// few of the community examples of template literals, for example:\n//\n// - An express route extractor by Dan Vanderkam\n// https://twitter.com/danvdk/status/1301707026507198464\n//\n// - A definition for document.querySelector by Mike Ryan\n// https://twitter.com/mikeryandev/status/1308472279010025477\n//\n// People have also experimented with quite complicated string parsers \n// using template string literals, which are fun - but not recommended for\n// production codebases.\n//\n// https://github.com/ghoullier/awesome-template-literal-types\n// \n// Or read the announcement blog post:\n// https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#template-literal-types"},{"path":["4-2","Fixits"],"title":"Create Function from Call","name":"Create Function from Call.ts","lang":"en","id":"create-function-from-call","sortIndex":1,"hash":"979b52d6faf64ce91d539afa1e0639248afd0bf687820c527a596151741d15113095921d786ed19586f3976a2c1d9e31c857265c168d346af6d76692c6a2dc57","compilerSettings":{"ts":"4.2.0-beta"},"code":"// In 4.2 a community member (@a-tarasyuk) added the ability to generate functions\n// from calls which aren't defined. For example if you select all the code on line 5,\n// then click \"Quick Fix\", you will see the option to have the missing function generated.\n\nconst id = generateUUID();\n\n// The fixit will take into account contextual information like the potential\n// return type for the function. For example, TypeScript knows the return type\n// because it is annotated at the variable declaration.\n\nconst idStr: string = generateUUID1();\n\n// The fixit will keep the same number of generics arguments when they are used:\n\nconst idObj = generateUUID3<{ id: string }>();\n\n// Parameters also act as you would expect:\n\nconst complexUUID = generateUUID4(\"SHA32\", 5, { namespace: \"typescriptlang.org\" });\n\n// It's not possible to show in the playground, but the codefix can create stubbed\n// functions across different modules too - lots to use."},{"path":["4-2","New TS Features"],"title":"Abstract Class Constructors","name":"Abstract Class Constructors.ts","lang":"en","id":"abstract-class-constructors","sortIndex":1,"hash":"198e0399282e978459d6deea9f2894fafab81433975d34f578edd39a3e8644ad4029837f12fa3fa4b21429af17c8118c2538078858a39f03018e2db89fb65509","compilerSettings":{"ts":"4.2.0-beta"},"code":"// TypeScript has supported abstract classes since 2015, which\n// provides compiler errors if you try to instantiate that class.\n\n// TypeScript 4.2 adds support for declaring that the constructor\n// function is abstract. This is mostly used by people who use\n// the mixin pattern ( example:mixins )\n\n// The mixin pattern involves having classes dynamically wrapping\n// each other to \"mixing in\" certain features to the end result.\n\n// This pattern is represented in TypeScript via a chain of constructor\n// functions of the classes, and by declaring one as abstract you can use\n// abstract classes inside your mixins.\n\n// All mixins start with a generic constructor to pass the T through, now\n// these can be abstract.\ntype AbstractConstructor = abstract new (...args: any[]) => T\n\n// We'll create an abstract class \"Animal\" where\n// the subclasses must override 'walk' \nabstract class Animal {\n abstract walk(): void;\n breath() { }\n}\n\n// A mixin which adds a new function (in this case, animate)\nfunction animatableAnimal>(Ctor: T) {\n abstract class StopWalking extends Ctor {\n animate() { }\n }\n return StopWalking;\n}\n\n// A subclass of the Animal, through the mixins, must still\n// handle the abstract contract for Animal. Which means it\n// needs to implement 'walk' below. Try deleting the function\n// to see what happens.\n\nclass Dog extends animatableAnimal(Animal) {\n walk() { }\n}\n\n\nconst dog = new Dog()\ndog.breath()\ndog.walk()\ndog.animate()"},{"path":["4-2","New TS Features"],"title":"Rest Elements in Tuple Types","name":"Rest Elements in Tuple Types.ts","lang":"en","id":"rest-elements-in-tuple-types","sortIndex":1,"hash":"d1062a8be053ce678028501b23ecd8436ac6e9b07d378847115c67545adbf722ec65a0da7de66a88d1b90038542488d5aba726c96f51dbc4d11fd0e2de699aa3","compilerSettings":{"ts":"4.2.0-beta"},"code":"// Tuple types are a feature where the position of a type in an\n// array is important. For example, in string[] (array) you know that all\n// elements in the array are a string, in [string] (tuple) you know that\n// only the first element is a string. According to the type system, a\n// [string] is an array with only one element, of string.\n\nconst stringArray: string[] = [\"sugar\", \"tea\", \"rum\"];\nconst singleStringTuple: [string] = [\"sugar\", \"tea\", \"rum\"];\n\n// Tuples allow TypeScript to describe arrays like: [string, number] - which\n// means that only a string and a number can be used in the first and second\n// positions respectively.\n\nconst stringNumberTuple: [string, number] = [\"Weeks from shore\", 2];\n\n// Under the hood, TypeScript uses Tuples to describe parameters for functions\n// which you can learn more from in:\n//\n// - example:tuples\n// - example:named-tuples\n\n// What was added in TypeScript 4.2 is the ability to describe functions which\n// take an unknown number of parameters but that have a particular start,\n// middle or end. This is done via the spread operator ... in a tuple to\n// indicate that the number varies (aka: variadic)\n\n// This type represents an unknown number of strings in the array but always\n// finishes with an object.\ntype StringsThenConfig = [...string[], { huh: boolean }];\n\nconst firstChorus: StringsThenConfig = [\"Blow\", \"Me Bully boys\", \"blow\", { huh: true }];\nconst secondChorus: StringsThenConfig = [\"We'll take our leave and go\", { huh: false }];\nconst thirdChorus: StringsThenConfig = [\"When she dived down below\", { huh: true }];\n\n// You can learn more about how the feature has evolved in the beta blog post:\n// https://devblogs.microsoft.com/typescript/announcing-typescript-4-2-beta/"},{"path":["4-2","New TS Features"],"title":"Smarter Type Alias Preservation","name":"Smarter Type Alias Preservation.ts","lang":"en","id":"smarter-type-alias-preservation","sortIndex":1,"hash":"a2b8eb532f8e84e342b215db378f25f363b7a5fef97b9a1aa0fa841237dc98e7dd0f3f15287ba2de70d49499de0e09f4d2eeb80ceab885d05a43e046a652f0ad","compilerSettings":{"ts":"4.2.0-beta"},"code":"// Type aliases differ from interfaces in that they aren't guaranteed to\n// keep their name as they are used throughout the compiler. In part, this\n// is a trade-off on what gives them their flexibility, but the downside\n// is that sometimes TypeScript shows an object instead of the name.\n\n// In 4.2, the compiler keeps track of the original name for a type alias\n// in more places. Reducing the size of error messages and hover hints.\n\ntype Shape =\n | { kind: \"circle\"; radius: number }\n | { kind: \"square\"; size: number }\n | { kind: \"rectangle\"; width: number; height: number };\n\ntype Named = { name: string };\n\n// Previously: Shape\ndeclare let shape: Shape;\n\n// No change there, but if you took the existing shape union and extend it,\n// then TypeScript used to 'lose' the original name:\n\n// Previously: { kind: \"circle\"; radius: number; | { kind: \"square\"; size: number; | { kind: \"rectangle\"; width: number; height: number; | undefined\ndeclare let optionalShape: Shape | undefined;\n\n// Previously: { kind: \"circle\"; radius: number; | { kind: \"square\"; size: number; | { kind: \"rectangle\"; width: number; height: number; | undefined\ndeclare let namedShape: Shape & Named;\n\n// Previously: ({ kind: \"circle\"; radius: number; Named) | ({ kind: \"square\"; size: number; Named) | ({ kind: \"rectangle\"; width: number; height: number; Named) | undefined\ndeclare let optionalNamedShape: (Shape & Named) | undefined; // (Shape & Named) | undefined"},{"path":["4-2","New TS Features"],"title":"Use Index Accessors for Index Signatures","name":"Use Index Accessors for Index Signatures.ts","lang":"en","id":"use-index-accessors-for-index-signatures","sortIndex":1,"hash":"edab3b9ce2eb623bfcdb6ed87e26ab9f9c23e0422c00f88c15bd05508aa93f180215c072c624afe191d6b073f23636a118105d43715d79ce9cf9b40e74bd3468","compilerSettings":{"ts":"4.2.0-beta","noPropertyAccessFromIndexSignature":true},"code":"// JavaScript has two ways to access an object on a property, the first is via\n// the dot operator x.y, the other is via square brackets x[\"y\"] - the second\n// syntax x[\"y\"] is called index accessors.\n\n// This syntax is reflected in the type system, where you can add an index\n// signature to a type, meaning any unknown property will have a particular type.\n\n// This type uses an index signature to indicate that you can ask\n// for any string and you will either get a string of undefined back.\n\ntype ENV = {\n [envVar: string]: string | undefined;\n};\n\n// In 4.2, there is a compiler flag to ensure consistency with how the\n// syntax for accessing the variable is consistent with how the variable\n// was declared.\n\n// For example, we can have a game ratings object, where games are\n// given a rank from 1 to 5. There are known games ahead of time,\n// but you could get back a lot of different objects\n\ntype Rating = 1 | 2 | 3 | 4 | 5;\n\ninterface GameRatingLibrary {\n hades: Rating;\n ringFitAdventures: Rating;\n discoElysium: Rating;\n\n // Unknown properties are covered by this index signature.\n [propName: string]: Rating;\n}\n\ndeclare const getYearRatings: (year: string) => GameRatingLibrary;\nconst ratings = getYearRatings(\"2020\");\n\n// These are known in above, and so you can safely use\n// the dot syntax.\nconst hadesScore = ratings.hades;\nconst ringFitScore = ratings.ringFitAdventures;\n\n// This game is not declared above, and the index signature\n// is used instead, this means you cannot use the dot\n// operator and must access via ratings[\"oriAndTheBlindForest\"]\nconst nodeEnv = ratings.oriAndTheBlindForest;"},{"path":["4-3","New TS Features"],"title":"No Implicit Class Overrides","name":"No Implicit Class Overrides.ts","lang":"en","id":"no-implicit-class-overrides","sortIndex":1,"hash":"2b0ef879edd1dfdd13c6629e6700fcde6169b7d8144916cddf8af45140ad5ad5262d6a5990aa26a73f7a7e7ab46cf136c1caf35ad961275667537059709d7e00","compilerSettings":{"ts":"4.3.4"},"code":"// When working with class hierarchies, it can be possible\n// to get the functions which you override out of sync because\n// prior to 4.3, there is no type-safe way to ensure a function\n// is always overriding an existing function.\n\n// For example, this Account class expects its one function to be\n// overridden when a user is an admin:\n\nclass Account {\n doSomething() {\n console.log(\"Make me a sandwich\");\n }\n}\n\nclass Admin extends Account {\n doSomething() {\n console.log(\"Sudo make me a sandwich\");\n }\n}\n\n// You can change the names of these functions, for example\n// maybe 'doSomething' is a bit vague. If you change the name\n// to 'performAction' in Account, but _not_ in Admin you have\n// decoupled the functions un-expectedly.\n\n// To enforce the consistency, there is a new flag: noImplicitOverride\n// and additional syntax. To see it in action, delete the space after the @\n// in the following comment:\n\n// @ noImplicitOverride\n\n// Then you need to add `override` where the red error markers are."},{"path":["4-3","New TS Features"],"title":"Separate Write vs Read on Properties","name":"Separate Write vs Read on Properties.ts","lang":"en","id":"separate-write-vs-read-on-properties","sortIndex":1,"hash":"4ddf1572ca8100735f6be415dc6465b5d1110b39dad67ccae8c89d47206a3870c57f83a32f26ae77dcf3a08b711ae6f8f18456f920f4258b9893ddf1a661b2e1","compilerSettings":{"ts":"4.3.4"},"code":"// TypeScript's type system aims to provide type tools\n// which match existing JavaScript code, and one of the\n// features which comes natural to JavaScript code is\n// to support many different types inputs for a value,\n// but to only provide one set output.\n\n// With TS 4.3, you can now have different set types\n// vs the get for a particular property with getters\n// and setters.\n\n// For example, this timer accepts many possible types\n// when setting the start property, but will only give\n// a Date object back.\n\nclass Timer {\n #start = new Date();\n\n get start(): Date {\n return this.#start;\n }\n\n set start(value: string | number | Date | undefined) {\n if (!value) this.#start = new Date();\n else if (value instanceof Date) this.#start = value;\n else this.#start = new Date(value);\n }\n}\n\nconst timer = new Timer();\n\ntimer.start = \"2021-06-28T14\";\nconsole.log(timer.start);\n\ntimer.start = 1624890417925;\nconsole.log(timer.start);\n\ntimer.start = new Date();\nconsole.log(timer.start);"},{"path":["4-4","New JS Features"],"title":"Control Flow Improvements","name":"Control Flow Improvements.ts","lang":"en","id":"control-flow-improvements","sortIndex":1,"hash":"b18741c4eb43e3c71036ce6e50a6bb8313f6606fa3b34b42ebe1360fe61bd003ae04110fba38caac591db0fa17e21df91500b155c36cec6710055453758b3b76","compilerSettings":{"ts":"4.4.2"},"code":"// Control Flow Analysis is the name for the system which\n// narrows the potential types an identifier can be based\n// on the code someone has wrote, you can get an overview\n// in example:type-widening-and-narrowing\n\n// Roughly, Control Flow Analysis lets you write code like\n// the following:\n\ndeclare const userInput: string | number;\nif (typeof userInput === \"string\") {\n userInput; // string\n} else {\n userInput; // number\n}\n\n// In this case, prior to TypeScript 4.4, code flow analysis\n// would only be applied to the code inside the if statement.\n// This meant that a very simple refactor like the following:\n\nconst isString = typeof userInput === \"string\";\n\n// Would have _not_ had control flow analysis applied:\n\nif (isString) {\n userInput; // string | number in 4.3\n} else {\n userInput; // string | number in 4.3\n}\n\n// In TypeScript 4.4 - the version you're currently on, the control flow\n// analysis can handle this sort of code. This works when TypeScript\n// can make reasonable assumptions that the variable hasn't changed since\n// it was created. For example, a `let` would not be able to be used\n// in analysis:\n\nlet isString2 = typeof userInput === \"string\";\nif (isString2) {\n userInput; // string | number in 4.4\n} else {\n userInput; // string | number in 4.4\n}\n\n// For full details see:\n// https://github.com/microsoft/TypeScript/pull/44730"},{"path":["4-4","New JS Features"],"title":"Errors for High Confidence Errors","name":"Errors for High Confidence Errors.ts","lang":"en","id":"errors-for-high-confidence-errors","sortIndex":1,"hash":"103f1377c46dcb1bf741372fc55f07cec5c4602b88635b9f0baa153cb14f3575be0e61672960726208b2a9bffb7059082630f6b4be0f4424e3a14e8ac45d8347","compilerSettings":{"ts":"4.4.2","useJavaScript":true,"checkJS":false},"code":"// For JavaScript users, TypeScript powers most of the tooling\n// for auto-complete and other IDE features like refactoring.\n\n// During the process of running the TypeScript compiler over\n// JavaScript files, TypeScript has typically _not_ raised errors\n// in an editor unless the project is opted-in via 'checkJS' or\n// has // @check-ts comment.\n\n// With 4.4, we are letting the TypeScript compiler suggest\n// suggest spelling corrections when it is confident that a\n// name is misspelled.\n\nconst album = {\n name: \"Afraid of Heights\",\n author: {\n name: \"Billy Talent\",\n releaseDate: \"2016\",\n },\n};\n\n// In a previous version of the TypeScript tooling for JavaScript,\n// this would not have suggested anything, even though there's\n// almost no way it's correct.\nalbum.nme;\n\n// For full details see:\n// https://github.com/microsoft/TypeScript/commit/e53f19f8f235ed21f405017a1f8670e9329027ce"},{"path":["4-4","New TS Features"],"title":"Exact Optional Properties","name":"Exact Optional Properties.ts","lang":"en","id":"exact-optional-properties","sortIndex":1,"hash":"ae05ff5ec4444ec17065f851a0f9b41b752e31c333eb9e35f9bbf1fe1c47946e0cac6ddc883ea8e35b45e6c564f904289bc0cde6656f7d57fde01953327dc6dd","compilerSettings":{"ts":"4.4.2","exactOptionalPropertyTypes":true},"code":"// With exactOptionalPropertyTypes enabled, TypeScript\n// has stricter rules around what you can set an optional\n// property to.\n\n// For example, this interface declares that there is a\n// property which can be one of two strings: 'dark' or 'light'\n// or it should not be in the object.\n\ninterface UserDefaults {\n // The absence of a value represents 'system'\n colorThemeOverride?: \"dark\" | \"light\";\n}\n\n// Without this flag enabled, there are three values which you can\n// set `colorThemeOverride` to be: \"dark\", \"light\" and `undefined`.\n\n// Setting the value to `undefined` will allow most JavaScript runtime\n// checks for the existence to fail, which is effectively falsy.\n// However, there's a fuzziness to this behavior. The definition\n// for `colorThemeOverride` _doesn't_ include `undefined`.\n\n// The flag `exactOptionalPropertyTypes` makes TypeScript accurately\n// match the definition provided as an optional property.\n\ndeclare function getUserSettings(): UserDefaults;\n\nconst settings = getUserSettings();\nsettings.colorThemeOverride = \"dark\";\nsettings.colorThemeOverride = \"light\";\n\n// But not:\nsettings.colorThemeOverride = undefined;\n\n// Prior to the flag `exactOptionalPropertyTypes`, this was not possible."},{"path":["4-4","New TS Features"],"title":"Use unknown for Catch Variables","name":"Use unknown for Catch Variables.ts","lang":"en","id":"use-unknown-for-catch-variables","sortIndex":1,"hash":"427958b0f13a35d990c13527355a7fe57980ea28afef00d52dbf13e9bb1d0ba642a37fc6ee083de1b96bb75b5e436f1619ae86f9243587630e734b06a864553c","compilerSettings":{"ts":"4.4.2","useUnknownInCatchVariables":true},"code":"// If `unknown` is new to you, read: example:unknown-and-never\n\n// Since TypeScript 4.0, you have been able to change the type\n// of the variable in a catch statement from the default of\n// `any` to `unknown` by manually assigning the type:\n\ntry {\n // @ts-ignore\n iWillCrash();\n} catch (err) {\n // This implicitly means you _have_ to declare\n // what the type is before you can write code\n // against `err`:\n console.log(err.message);\n\n // For example, we have to verify it is an\n // error before using it as one.\n if (err instanceof Error) {\n console.log(err.message);\n }\n}\n\n// Also: example:unknown-in-catch\n\n// By using the option `useUnknownInCatchVariables`, you\n// can have the compiler default to `unknown` instead of\n// `any`. Effectively forcing all usage of that variable\n// to be confirmed by the type system before usage.\n\n// You can opt-out on one-off catch statements by assigning\n// the variable to `any`.\n\ntry {\n // @ts-ignore\n iWillCrash();\n} catch (err: any) {\n // In which case, you can treat it\n // however you would like.\n console.log(err.message);\n}"},{"path":["4-7","New JS Features"],"title":"Control Flow Improvements","name":"Control Flow Improvements.ts","lang":"en","id":"control-flow-improvements","sortIndex":1,"hash":"4d70b774633e9e757bdb633b7b3ffa4c7abc786df4472ff586fe5381de50c49d6abef9610cd82d5b86dfb4cd2e09e5f5126f6d4cc520ad7ead3b570fb5592c7e","compilerSettings":{"ts":"4.7.3"},"code":"// In almost every release there are control flow improvements,\n// in 4.7 there are improvements in computed properties work when\n// narrowing. To learn more about narrowing see: example:type-widening-and-narrowing\n\nconst dog = \"stringer\";\n\nconst dogsToOwnersOrID = {\n hayes: \"The McShanes\",\n poppy: \"Pat\",\n stringer: \"Jane\",\n otto: 1,\n} as Record;\n\n// A computed property is a property access which is not hardcoded,\n// for example:\n\nconst owner = dogsToOwnersOrID[dog];\n// ^?\n\n// Prior to TypeScript 4.7, using a computed property would not\n// reliably narrow the type of a value.\n\nif (typeof dogsToOwnersOrID[dog] === \"string\") {\n const str = dogsToOwnersOrID[dog].toUpperCase();\n // ^?\n\n // In 4.6, this would still be 'string | number'.\n}"},{"path":["4-7","New JS Features"],"title":"Improved Function Inference","name":"Improved Function Inference.ts","lang":"en","id":"improved-function-inference","sortIndex":1,"hash":"79a09d53c652104dac47e59ce019748e6dba27321bbf04d4d4d8b295b0ff514e9a2153a241c26b612b7e94a7ce6488bafbc77cc9e2c83812f568c15f0564de00","compilerSettings":{"ts":"4.7.3"},"code":"// With TypeScript 4.7, there are improvements in how types\n// are inferred from typed function calls.\n\n// Take this function 'cache', it takes one type parameter, 'Type'\n// and one function argument, which contains two functions.\n\ndeclare function cache(arg: { add: (n: string) => Type; process: (x: Type) => void }): void;\n\n// When TypeScript is trying to infer the type of 'Type', it\n// has two potential places to look:\n//\n// - the return of 'add'\n// - the first parameter of 'remove'\n\n// TypeScript's code inference now takes more these cases into\n// account with more code styles, and also handle cases where the\n// types depend on each other.\n\ncache({\n add: n => n,\n process: x => x.toLowerCase(),\n});\n\n// Here Type = string because `add` returns a string\n// which is the parameter type. However, prior versions of\n// TypeScript would infer Type = any/unknown because both\n// add and process would be evaluated at the same time.\n\ncache({\n add: function (str) {\n return { value: str + \"!\" };\n },\n process: x => x.value.toLowerCase(),\n});\n\n// Here Type = { value: string }\n\ncache({\n add() {\n return 23;\n },\n process: x => x + 1,\n});\n\n// Here Type = number\n\n// This helps JavaScript and TypeScript users experience\n// less accidental anys when working across different\n// code styles."},{"path":["4-7","New TS Features"],"title":"Extends Constraints","name":"Extends Constraints.ts","lang":"en","id":"extends-constraints","sortIndex":1,"hash":"6e20e71ecd760671aedad3fa2f77b6cf53e9dbcc1fb3cca28bb5907e9e39f537cf559686e8ab1ee72b32dc9fd207f0944b70d802386d40841be8695c48b2ce34","compilerSettings":{"ts":"4.7.3"},"code":"// In 4.7 TypeScript added support for inlining an extends clause\n// in a conditional type. This can help reduce the complexity of\n// conditional types.\n\n// If you're new to conditional types see: example:conditional-types\n\n// As an example, here is a 4.6 conditional type which looks\n// at the return value of a function and only returns a type\n// if the return type is a string.\n\n// prettier-ignore\ntype ReturnTypeOnlyStrings47 =\n T extends (...args: any[]) => \n infer R ? (R extends string ? R : never) : never;\n\n// It's essentially two if statements, one for the return type\n// and one then to check if the return type is a string. In\n// 4.7 this can be done inside one statement.\n\n// prettier-ignore\ntype ReturnType2 =\n T extends (...args: any[]) =>\n (infer R extends string) ? R : never;\n\n// It's a little like being able to use an && inside the conditional\n// type check, which makes code a bit simpler and more readable."},{"path":["4-7","New TS Features"],"title":"Instantiation Expressions","name":"Instantiation Expressions.ts","lang":"en","id":"instantiation-expressions","sortIndex":1,"hash":"d37e983075138a9be24f2f3d4fd048ec07d8ace81cbf3dad56e8de79ddcd8485635b7793ee2efe755fe5983daa4d61ee9bf06bb6638863fe3c843113a658fb01","compilerSettings":{"ts":"4.7.3"},"code":"// Prior to TypeScript 4.7, you would have to do call a function\n// in order to narrow a generic type to something specific. For\n// example, let's take a Map object:\n\nconst map = new Map();\n// ^?\n\n// This map uses strings for keys, and numbers for values. Until\n// we created the map, the values for key (string) and value (number)\n// were yet to be defined and could still be anything.\n\n// Instantiation expressions means that we can create a version of the\n// Map function which will always accept only strings for keys and \n// numbers for values:\n\nconst MapStrNum = Map;\n\nconst map2 = new MapStrNum()\n// ^?\n\n// This feature allows us to elegantly create a more specific typed\n// functions without having to wrap the function in another function."},{"path":["JavaScript","External APIs"],"title":"TypeScript with Deno","name":"TypeScript with Deno.ts","lang":"en","id":"typescript-with-deno","sortIndex":3,"hash":"49e2032853406f96c7b3c8a3d43204577f3c41fd45f5ea1a1070f9bfbf03291e703946589e8b61d146a09bd8ad24cd16d35482e213c18e218ef8155255ea81dc","code":"// Deno is a work-in-progress JavaScript and TypeScript\n// runtime based on v8 with a focus on security.\n\n// https://deno.land\n\n// Deno has a sandbox-based permissions system which reduces the\n// access JavaScript has to the file-system or the network and uses\n// http based imports which are downloaded and cached locally.\n\n// Here is an example of using deno for scripting:\n\nimport compose from \"https://deno.land/x/denofun/lib/compose.ts\";\n\nfunction greet(name: string) {\n return `Hello, ${name}!`;\n}\n\nfunction makeLoud(x: string) {\n return x.toUpperCase();\n}\n\nconst greetLoudly = compose(makeLoud, greet);\n\n// Echos \"HELLO, WORLD!.\"\ngreetLoudly(\"world\");\n\nimport concat from \"https://deno.land/x/denofun/lib/concat.ts\";\n\n// Returns \"helloworld\"\nconcat(\"hello\", \"world\");"},{"path":["JavaScript","External APIs"],"title":"TypeScript with Node","name":"TypeScript with Node.js","lang":"en","id":"typescript-with-node","sortIndex":3,"hash":"8493b066343d2fa1c854b3a543f9286f34f85017da281b685b786e2e0f1774bd611b4b653207b998c8f43e14d6b26e482037e6fb87ddb79cd1a0b6200f8d49a5","code":"// Node.js is a very popular JavaScript runtime built on v8,\n// the JavaScript engine which powers Chrome. You can use it\n// to build servers, front-end clients and anything in-between.\n\n// https://nodejs.org/\n\n// Node.js comes with a set of core libraries which extend the\n// JavaScript runtime. They range from path handling:\n\nimport { join } from \"path\";\nconst myPath = join(\"~\", \"downloads\", \"todo_list.json\");\n\n// To file manipulation:\n\nimport { readFileSync } from \"fs\";\nconst todoListText = readFileSync(myPath, \"utf8\");\n\n// You can incrementally add types to your JavaScript projects\n// using JSDoc-style type. We'll make one for our TODO list item\n// based on the JSON structure:\n\n/**\n * @typedef {Object} TODO a TODO item\n * @property {string} title The display name for the TODO item\n * @property {string} body The description of the TODO item\n * @property {boolean} done Whether the TODO item is completed\n */\n\n// Now assign that to the return value of JSON.parse\n// to learn more about this, see: example:jsdoc-support\n\n/** @type {TODO[]} a list of TODOs */\nconst todoList = JSON.parse(todoListText);\n\n// And process handling:\nimport { spawnSync } from \"child_process\";\ntodoList\n .filter(todo => !todo.done)\n .forEach(todo => {\n // Use the ghi client to create an issue for every todo\n // list item which hasn't been completed yet.\n\n // Note that you get correct auto-complete and\n // docs in JS when you highlight 'todo.title' below.\n spawnSync(`ghi open --message \"${todo.title}\\n${todo.body}\"`);\n });\n\n// TypeScript has up-to-date type definitions for all of the\n// built in modules via DefinitelyTyped - which means you\n// can write node programs with strong type coverage."},{"path":["JavaScript","External APIs"],"title":"TypeScript with React","name":"TypeScript with React.tsx","lang":"en","id":"typescript-with-react","sortIndex":2,"hash":"83f31dfe4570b928766bcc80baac0a0ecf42412c5820bd603154a3e276ecfd5c77c3685d1176b69e16a1ca7b90207110eb63b3b68b1994f7e134922af9d66785","compilerSettings":{"jsx":2,"esModuleInterop":true},"code":"// React is a popular library for creating user interfaces.\n// It provides a JavaScript abstraction for creating view\n// components using a JavaScript language extension called\n// JSX.\n\n// TypeScript supports JSX, and provides a rich set of\n// type tools to richly model how components connect.\n\n// To understand how TypeScript works with React components\n// you may want a primer on generics:\n//\n// - example:generic-functions\n// - example:generic-classes\n\n// First we'll look at how generic interfaces are used to map\n// React components. This is a faux-React functional component:\n\ntype FauxactFunctionComponent = (\n props: Props,\n context?: any\n) => FauxactFunctionComponent | null | JSX.Element;\n\n// Roughly:\n//\n// FauxactFunctionComponent is a generic function which relies on\n// another type, Props. Props has to be an object (to make sure\n// you don't pass a primitive) and the Props type will be\n// re-used as the first argument in the function.\n\n// To use it, you need a props type:\n\ninterface DateProps {\n iso8601Date: string;\n message: string;\n}\n\n// We can then create a DateComponent which uses the\n// DateProps interface, and renders the date.\n\nconst DateComponent: FauxactFunctionComponent = props => (\n {props.message} \n);\n\n// This creates a function which is generic with a Props\n// variable which has to be an object. The component function\n// returns either another component function or null.\n\n// The other component API is a class-based one. Here's a\n// simplified version of that API:\n\ninterface FauxactClassComponent {\n props: Props;\n state: State;\n\n setState: (prevState: State, props: Props) => Props;\n callback?: () => void;\n render(): FauxactClassComponent | null;\n}\n\n// Because this class can have both Props and State - it has\n// two generic arguments which are used throughout the class.\n\n// The React library comes with its own type definitions\n// like these but are much more comprehensive. Let's bring\n// those into our playground and explore a few components.\n\nimport * as React from \"react\";\n\n// Your props are your public API, so it's worth taking the\n// time to use JSDoc to explain how it works:\n\nexport interface Props {\n /** The user's name */\n name: string;\n /** Should the name be rendered in bold */\n priority?: boolean;\n}\n\nconst PrintName: React.FC = props => {\n return (\n \n );\n};\n\n// You can play with the new component's usage below:\n\nconst ShowUser: React.FC = props => {\n return ;\n};\n\n// TypeScript supports providing intellisense inside\n// the {} in an attribute\n\nlet username = \"Cersei\";\nconst ShowStoredUser: React.FC = props => {\n return ;\n};\n\n// TypeScript works with modern React code too, here you can\n// see that count and setCount have correctly been inferred\n// to use numbers based on the initial value passed into\n// useState.\n\nimport { useState, useEffect } from \"react\";\n\nconst CounterExample = () => {\n const [count, setCount] = useState(0);\n\n useEffect(() => {\n document.title = `You clicked ${count} times`;\n });\n\n return (\n \n
You clicked {count} times
\n
setCount(count + 1)}>Click me \n
\n );\n};\n\n// React and TypeScript is a really, really big topic\n// but the fundamentals are pretty small: TypeScript\n// supports JSX, and the rest is handled by the React\n// typings from Definitely Typed.\n\n// You can learn more about using React with TypeScript\n// from these sites:\n//\n// https://github.com/typescript-cheatsheets/react-typescript-cheatsheet\n// https://egghead.io/courses/use-typescript-to-develop-react-applications\n// https://levelup.gitconnected.com/ultimate-react-component-patterns-with-typescript-2-8-82990c516935"},{"path":["JavaScript","External APIs"],"title":"TypeScript with Web","name":"TypeScript with Web.js","lang":"en","id":"typescript-with-web","sortIndex":1,"hash":"4f3be315ea8b94f9349315cdc99e954d05491f7c2e0d1881abefaa6433a23cdf6df6b09b397376113d374508ade521ff745d8294ab8b4ee6d609332e959f3761","code":"// The DOM (Document Object Model) is the underlying API for\n// working with a webpage, and TypeScript has great support\n// for that API.\n\n// Let's create a popover to show when you press \"Run\" in\n// the toolbar above.\n\nconst popover = document.createElement(\"div\");\npopover.id = \"example-popover\";\n\n// Note that popover is correctly typed to be a HTMLDivElement\n// specifically because we passed in \"div\".\n\n// To make it possible to re-run this code, we'll first\n// add a function to remove the popover if it was already there.\n\nconst removePopover = () => {\n const existingPopover = document.getElementById(popover.id);\n if (existingPopover && existingPopover.parentElement) {\n existingPopover.parentElement.removeChild(existingPopover);\n }\n};\n\n// Then call it right away.\n\nremovePopover();\n\n// We can set the inline styles on the element via the\n// .style property on a HTMLElement - this is fully typed.\n\npopover.style.backgroundColor = \"#0078D4\";\npopover.style.color = \"white\";\npopover.style.border = \"1px solid black\";\npopover.style.position = \"fixed\";\npopover.style.bottom = \"10px\";\npopover.style.left = \"20px\";\npopover.style.width = \"200px\";\npopover.style.height = \"100px\";\npopover.style.padding = \"10px\";\n\n// Including more obscure, or deprecated CSS attributes.\npopover.style.webkitBorderRadius = \"4px\";\n\n// To add content to the popover, we'll need to add\n// a paragraph element and use it to add some text.\n\nconst message = document.createElement(\"p\");\nmessage.textContent = \"Here is an example popover\";\n\n// And we'll also add a close button.\n\nconst closeButton = document.createElement(\"a\");\ncloseButton.textContent = \"X\";\ncloseButton.style.position = \"absolute\";\ncloseButton.style.top = \"3px\";\ncloseButton.style.right = \"8px\";\ncloseButton.style.color = \"white\";\ncloseButton.style.cursor = \"pointer\";\n\ncloseButton.onclick = () => {\n removePopover();\n};\n\n// Then add all of these elements on to the page.\npopover.appendChild(message);\npopover.appendChild(closeButton);\ndocument.body.appendChild(popover);\n\n// If you hit \"Run\" above, then a popup should appear\n// in the bottom left, which you can close by clicking\n// on the x in the top right of the popup.\n\n// This example shows how you can work with the DOM API\n// in JavaScript - but using TypeScript to provide great\n// tooling support.\n\n// There is an extended example for TypeScript tooling with\n// WebGL available here: example:typescript-with-webgl"},{"path":["JavaScript","External APIs"],"title":"TypeScript with WebGL","name":"TypeScript with WebGL.js","lang":"en","id":"typescript-with-webgl","sortIndex":5,"hash":"a5ef2ec58e8ef7278fd5c3e171f8f170aef73e57fceca6bc1f819666ee34a903c50faa848443b0b9085adc28f48b5d2e48675174ef877bb8e08aaa03ca17fa90","code":"// This example creates an HTML canvas which uses WebGL to\n// render spinning confetti using JavaScript. We're going\n// to walk through the code to understand how it works, and\n// see how TypeScript's tooling provides useful insight.\n\n// This example builds off: example:working-with-the-dom\n\n// First up, we need to create an HTML canvas element, which\n// we do via the DOM API and set some inline style attributes:\n\nconst canvas = document.createElement(\"canvas\");\ncanvas.id = \"spinning-canvas\";\ncanvas.style.backgroundColor = \"#0078D4\";\ncanvas.style.position = \"fixed\";\ncanvas.style.bottom = \"10px\";\ncanvas.style.right = \"20px\";\ncanvas.style.width = \"500px\";\ncanvas.style.height = \"400px\";\ncanvas.style.zIndex = \"100\";\n\n// Next, to make it easy to make changes, we remove any older\n// versions of the canvas when hitting \"Run\" - now you can\n// make changes and see them reflected when you press \"Run\"\n// or (cmd + enter):\n\nconst existingCanvas = document.getElementById(canvas.id);\nif (existingCanvas && existingCanvas.parentElement) {\n existingCanvas.parentElement.removeChild(existingCanvas);\n}\n\n// Tell the canvas element that we will use WebGL to draw\n// inside the element (and not the default raster engine):\n\nconst gl = canvas.getContext(\"webgl\");\n\n// Next we need to create vertex shaders - these roughly are\n// small programs that apply maths to a set of incoming\n// array of vertices (numbers).\n\n// You can see the large set of attributes at the top of the shader,\n// these are passed into the compiled shader further down the example.\n\n// There's a great overview on how they work here:\n// https://webglfundamentals.org/webgl/lessons/webgl-how-it-works.html\n\nconst vertexShader = gl.createShader(gl.VERTEX_SHADER);\ngl.shaderSource(\n vertexShader,\n `\nprecision lowp float;\n\nattribute vec2 a_position; // Flat square on XY plane\nattribute float a_startAngle;\nattribute float a_angularVelocity;\nattribute float a_rotationAxisAngle;\nattribute float a_particleDistance;\nattribute float a_particleAngle;\nattribute float a_particleY;\nuniform float u_time; // Global state\n\nvarying vec2 v_position;\nvarying vec3 v_color;\nvarying float v_overlight;\n\nvoid main() {\n float angle = a_startAngle + a_angularVelocity * u_time;\n float vertPosition = 1.1 - mod(u_time * .25 + a_particleY, 2.2);\n float viewAngle = a_particleAngle + mod(u_time * .25, 6.28);\n\n mat4 vMatrix = mat4(\n 1.3, 0.0, 0.0, 0.0,\n 0.0, 1.3, 0.0, 0.0,\n 0.0, 0.0, 1.0, 1.0,\n 0.0, 0.0, 0.0, 1.0\n );\n\n mat4 shiftMatrix = mat4(\n 1.0, 0.0, 0.0, 0.0,\n 0.0, 1.0, 0.0, 0.0,\n 0.0, 0.0, 1.0, 0.0,\n a_particleDistance * sin(viewAngle), vertPosition, a_particleDistance * cos(viewAngle), 1.0\n );\n\n mat4 pMatrix = mat4(\n cos(a_rotationAxisAngle), sin(a_rotationAxisAngle), 0.0, 0.0,\n -sin(a_rotationAxisAngle), cos(a_rotationAxisAngle), 0.0, 0.0,\n 0.0, 0.0, 1.0, 0.0,\n 0.0, 0.0, 0.0, 1.0\n ) * mat4(\n 1.0, 0.0, 0.0, 0.0,\n 0.0, cos(angle), sin(angle), 0.0,\n 0.0, -sin(angle), cos(angle), 0.0,\n 0.0, 0.0, 0.0, 1.0\n );\n\n gl_Position = vMatrix * shiftMatrix * pMatrix * vec4(a_position * 0.03, 0.0, 1.0);\n vec4 normal = vec4(0.0, 0.0, 1.0, 0.0);\n vec4 transformedNormal = normalize(pMatrix * normal);\n\n float dotNormal = abs(dot(normal.xyz, transformedNormal.xyz));\n float regularLighting = dotNormal / 2.0 + 0.5;\n float glanceLighting = smoothstep(0.92, 0.98, dotNormal);\n v_color = vec3(\n mix((0.5 - transformedNormal.z / 2.0) * regularLighting, 1.0, glanceLighting),\n mix(0.5 * regularLighting, 1.0, glanceLighting),\n mix((0.5 + transformedNormal.z / 2.0) * regularLighting, 1.0, glanceLighting)\n );\n\n v_position = a_position;\n v_overlight = 0.9 + glanceLighting * 0.1;\n}\n`\n);\ngl.compileShader(vertexShader);\n\n// This example also uses fragment shaders - a fragment\n// shader is another small program that runs through every\n// pixel in the canvas and sets its color.\n\n// In this case, if you play around with the numbers you can see how\n// this affects the lighting in the scene, as well as the border\n// radius on the confetti:\n\nconst fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);\ngl.shaderSource(\n fragmentShader,\n `\nprecision lowp float;\nvarying vec2 v_position;\nvarying vec3 v_color;\nvarying float v_overlight;\n\nvoid main() {\n gl_FragColor = vec4(v_color, 1.0 - smoothstep(0.8, v_overlight, length(v_position)));\n}\n`\n);\ngl.compileShader(fragmentShader);\n\n// Takes the compiled shaders and adds them to the canvas'\n// WebGL context so that can be used:\n\nconst shaderProgram = gl.createProgram();\ngl.attachShader(shaderProgram, vertexShader);\ngl.attachShader(shaderProgram, fragmentShader);\ngl.linkProgram(shaderProgram);\ngl.useProgram(shaderProgram);\n\ngl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());\n\n// We need to get/set the input variables into the shader in a\n// memory-safe way, so the order and the length of their\n// values needs to be stored.\n\nconst attrs = [\n { name: \"a_position\", length: 2, offset: 0 }, // e.g. x and y represent 2 spaces in memory\n { name: \"a_startAngle\", length: 1, offset: 2 }, // but angle is just 1 value\n { name: \"a_angularVelocity\", length: 1, offset: 3 },\n { name: \"a_rotationAxisAngle\", length: 1, offset: 4 },\n { name: \"a_particleDistance\", length: 1, offset: 5 },\n { name: \"a_particleAngle\", length: 1, offset: 6 },\n { name: \"a_particleY\", length: 1, offset: 7 },\n];\n\nconst STRIDE = Object.keys(attrs).length + 1;\n\n// Loop through our known attributes and create pointers in memory for the JS side\n// to be able to fill into the shader.\n\n// To understand this API a little bit: WebGL is based on OpenGL\n// which is a state-machine styled API. You pass in commands in a\n// particular order to render things to the screen.\n\n// So, the intended usage is often not passing objects to every WebGL\n// API call, but instead passing one thing to one function, then passing\n// another to the next. So, here we prime WebGL to create an array of\n// vertex pointers:\n\nfor (var i = 0; i < attrs.length; i++) {\n const name = attrs[i].name;\n const length = attrs[i].length;\n const offset = attrs[i].offset;\n const attribLocation = gl.getAttribLocation(shaderProgram, name);\n gl.vertexAttribPointer(attribLocation, length, gl.FLOAT, false, STRIDE * 4, offset * 4);\n gl.enableVertexAttribArray(attribLocation);\n}\n\n// Then on this line they are bound to an array in memory:\n\ngl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());\n\n// Set up some constants for rendering:\n\nconst NUM_PARTICLES = 200;\nconst NUM_VERTICES = 4;\n\n// Try reducing this one and hitting \"Run\" again,\n// it represents how many points should exist on\n// each confetti and having an odd number sends\n// it way out of whack.\n\nconst NUM_INDICES = 6;\n\n// Create the arrays of inputs for the vertex shaders\nconst vertices = new Float32Array(NUM_PARTICLES * STRIDE * NUM_VERTICES);\nconst indices = new Uint16Array(NUM_PARTICLES * NUM_INDICES);\n\nfor (let i = 0; i < NUM_PARTICLES; i++) {\n const axisAngle = Math.random() * Math.PI * 2;\n const startAngle = Math.random() * Math.PI * 2;\n const groupPtr = i * STRIDE * NUM_VERTICES;\n\n const particleDistance = Math.sqrt(Math.random());\n const particleAngle = Math.random() * Math.PI * 2;\n const particleY = Math.random() * 2.2;\n const angularVelocity = Math.random() * 2 + 1;\n\n for (let j = 0; j < 4; j++) {\n const vertexPtr = groupPtr + j * STRIDE;\n vertices[vertexPtr + 2] = startAngle; // Start angle\n vertices[vertexPtr + 3] = angularVelocity; // Angular velocity\n vertices[vertexPtr + 4] = axisAngle; // Angle diff\n vertices[vertexPtr + 5] = particleDistance; // Distance of the particle from the (0,0,0)\n vertices[vertexPtr + 6] = particleAngle; // Angle around Y axis\n vertices[vertexPtr + 7] = particleY; // Angle around Y axis\n }\n\n // Coordinates\n vertices[groupPtr] = vertices[groupPtr + STRIDE * 2] = -1;\n vertices[groupPtr + STRIDE] = vertices[groupPtr + STRIDE * 3] = +1;\n vertices[groupPtr + 1] = vertices[groupPtr + STRIDE + 1] = -1;\n vertices[groupPtr + STRIDE * 2 + 1] = vertices[groupPtr + STRIDE * 3 + 1] = +1;\n\n const indicesPtr = i * NUM_INDICES;\n const vertexPtr = i * NUM_VERTICES;\n indices[indicesPtr] = vertexPtr;\n indices[indicesPtr + 4] = indices[indicesPtr + 1] = vertexPtr + 1;\n indices[indicesPtr + 3] = indices[indicesPtr + 2] = vertexPtr + 2;\n indices[indicesPtr + 5] = vertexPtr + 3;\n}\n\n// Pass in the data to the WebGL context\ngl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);\ngl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);\n\nconst timeUniformLocation = gl.getUniformLocation(shaderProgram, \"u_time\");\nconst startTime = (window.performance || Date).now();\n\n// Start the background colour as black\ngl.clearColor(0, 0, 0, 1);\n\n// Allow alpha channels on in the vertex shader\ngl.enable(gl.BLEND);\ngl.blendFunc(gl.SRC_ALPHA, gl.ONE);\n\n// Set the WebGL context to be the full size of the canvas\ngl.viewport(0, 0, canvas.width, canvas.height);\n\n// Create a run-loop to draw all of the confetti\n(function frame() {\n gl.uniform1f(timeUniformLocation, ((window.performance || Date).now() - startTime) / 1000);\n\n gl.clear(gl.COLOR_BUFFER_BIT);\n gl.drawElements(gl.TRIANGLES, NUM_INDICES * NUM_PARTICLES, gl.UNSIGNED_SHORT, 0);\n requestAnimationFrame(frame);\n})();\n\n// Add the new canvas element into the bottom left\n// of the playground\ndocument.body.appendChild(canvas);\n\n// Credit: based on this JSFiddle by Subzey\n// https://jsfiddle.net/subzey/52sowezj/"},{"path":["JavaScript","Functions with JavaScript"],"title":"Function Chaining","name":"Function Chaining.ts","lang":"en","id":"function-chaining","sortIndex":2,"hash":"2c05aa57e2420890a6a288782aa1bdf0e6b670650b56d5ea19fc9afc57e24ab85ba19d0b23a2ffe9f16fb8725e69ff9e0912d6e3ef71776269b38a8f50c09bf2","compilerSettings":{"esModuleInterop":true},"code":"// Function chaining APIs are a common pattern in\n// JavaScript, which can make your code focused\n// with less intermediary values and easier to read\n// because of their nesting qualities.\n\n// A really common API which works via chaining\n// is jQuery. Here is an example of jQuery\n// being used with the types from DefinitelyTyped:\n\nimport $ from \"jquery\";\n\n// Here's an example use of the jQuery API:\n\n$(\"#navigation\").css(\"background\", \"red\").height(300).fadeIn(200);\n\n// If you add a dot on the line above, you'll see\n// a long list of functions. This pattern is easy to\n// reproduce in JavaScript. The key is to make sure\n// you always return the same object.\n\n// Here is an example API which creates a chaining\n// API. The key is to have an outer function which\n// keeps track of internal state, and an object which\n// exposes the API that is always returned.\n\nconst addTwoNumbers = (start = 1) => {\n let n = start;\n\n const api = {\n // Implement each function in your API\n add(inc: number = 1) {\n n += inc;\n return api;\n },\n\n print() {\n console.log(n);\n return api;\n },\n };\n return api;\n};\n\n// Which allows the same style of API as we\n// saw in jQuery:\n\naddTwoNumbers(1).add(3).add().print().add(1);\n\n// Here's a similar example which uses a class:\n\nclass AddNumbers {\n private n: number;\n\n constructor(start = 0) {\n this.n = start;\n }\n\n public add(inc = 1) {\n this.n = this.n + inc;\n return this;\n }\n\n public print() {\n console.log(this.n);\n return this;\n }\n}\n\n// Here it is in action:\n\nnew AddNumbers(2).add(3).add().print().add(1);\n\n// This example used the TypeScript\n// type inference to provide a way to\n// provide tooling to JavaScript patterns.\n\n// For more examples on this:\n//\n// - example:code-flow"},{"path":["JavaScript","Functions with JavaScript"],"title":"Generic Functions","name":"Generic Functions.ts","lang":"en","id":"generic-functions","sortIndex":1,"hash":"3c8d1eb865e4f035b1acdf529ba94526984f1bbd82650b9175f846529528f9497207fb264d0b5055b83edf62f286dfda505182f22c7ff8b0d9f214b6750f7577","compilerSettings":{},"code":"// Generics provide a way to use Types as variables in other\n// types. Meta.\n\n// We'll be trying to keep this example light, you can do\n// a lot with generics and it's likely you will see some very\n// complicated code using generics at some point - but that\n// does not mean that generics are complicated.\n\n// Let's start with an example where we wrap an input object\n// in an array. We will only care about one variable in this\n// case, the type which was passed in:\n\nfunction wrapInArray(input: Type): Type[] {\n return [input];\n}\n\n// Note: it's common to see Type referred to as T. This is\n// culturally similar to how people use i in a for loop to\n// represent index. T normally represents Type, so we'll\n// be using the full name for clarity.\n\n// Our function will use inference to always keep the type\n// passed in the same as the type passed out (though\n// it will be wrapped in an array).\n\nconst stringArray = wrapInArray(\"hello generics\");\nconst numberArray = wrapInArray(123);\n\n// We can verify this works as expected by checking\n// if we can assign a string array to a function which\n// should be an object array:\nconst notStringArray: string[] = wrapInArray({});\n\n// You can also skip the generic inference by adding the\n// type yourself also:\nconst stringArray2 = wrapInArray(\"\");\n\n// wrapInArray allows any type to be used, however there\n// are cases when you need to only allow a subset of types.\n// In these cases you can say the type has to extend a\n// particular type.\n\ninterface Drawable {\n draw: () => void;\n}\n\n// This function takes a set of objects which have a function\n// for drawing to the screen\nfunction renderToScreen(input: Type[]) {\n input.forEach((i) => i.draw());\n}\n\nconst objectsWithDraw = [{ draw: () => {} }, { draw: () => {} }];\nrenderToScreen(objectsWithDraw);\n\n// It will fail if draw is missing:\n\nrenderToScreen([{}, { draw: () => {} }]);\n\n// Generics can start to look complicated when you have\n// multiple variables. Here is an example of a caching\n// function that lets you have different sets of input types\n// and caches.\n\ninterface CacheHost {\n save: (a: any) => void;\n}\n\nfunction addObjectToCache(obj: Type, cache: Cache): Cache {\n cache.save(obj);\n return cache;\n}\n\n// This is the same as above, but with an extra parameter.\n// Note: to make this work though, we had to use an any. This\n// can be worked out by using a generic interface.\n\ninterface CacheHostGeneric {\n save: (a: ContentType) => void;\n}\n\n// Now when the CacheHostGeneric is used, you need to tell\n// it what ContentType is.\n\nfunction addTypedObjectToCache>(obj: Type, cache: Cache): Cache {\n cache.save(obj);\n return cache;\n}\n\n// That escalated pretty quickly in terms of syntax. However,\n// this provides more safety. These are trade-offs, that you\n// have more knowledge to make now. When providing APIs for\n// others, generics offer a flexible way to let others use\n// their own types with full code inference.\n\n// For more examples of generics with classes and interfaces:\n//\n// example:advanced-classes\n// example:typescript-with-react\n// https://www.typescriptlang.org/docs/handbook/generics.html\n"},{"path":["JavaScript","Functions with JavaScript"],"title":"Typing Functions","name":"Typing Functions.ts","lang":"en","id":"typing-functions","sortIndex":1,"hash":"a17621929f78585592b2f9520889afda1351c555f2c40cdaab6ba0fe579d226a045350f266ea6ef0d3c8b620b767bb0468ebdd9676bd7eaa83524307af9cec0d","compilerSettings":{},"code":"// TypeScript's inference can get you very far, but there\n// are lots of extra ways to provide a richer way to document\n// the shape of your functions.\n\n// A good first place is to look at optional params, which\n// is a way of letting others know you can skip params.\n\nlet i = 0;\nconst incrementIndex = (value?: number) => {\n i += value === undefined ? 1 : value;\n};\n\n// This function can be called like:\n\nincrementIndex();\nincrementIndex(0);\nincrementIndex(3);\n\n// You can type parameters as functions, which provides\n// type inference when you write the functions.\n\nconst callbackWithIndex = (callback: (i: number) => void) => {\n callback(i);\n};\n\n// Embedding function interfaces can get a bit hard to read\n// with all the arrows. Using a type alias will let you name\n// the function param.\n\ntype NumberCallback = (i: number) => void;\nconst callbackWithIndex2 = (callback: NumberCallback) => {\n callback(i);\n};\n\n// These can be called like:\n\ncallbackWithIndex(index => {\n console.log(index);\n});\n\n// By hovering on index above, you can see how TypeScript\n// has inferred the index to be a number correctly.\n\n// TypeScript inference can work when passing a function\n// as an instance reference too. To show this, we'll use\n// a function which changed a number into string:\n\nconst numberToString = (n: number) => {\n return n.toString();\n};\n\n// This can be used in a function like map on an array\n// to convert all numbers into a string, if you hover\n// on stringedNumbers below you can see the expected types.\nconst stringedNumbers = [1, 4, 6, 10].map(i => numberToString(i));\n\n// We can use shorthand to have the function passed directly\n// and get the same results with more focused code:\nconst stringedNumbersTerse = [1, 4, 6, 10].map(numberToString);\n\n// You may have functions which could accept a lot of types\n// but you are only interested in a few properties. This is\n// a useful case for indexed signatures in types. The\n// following type declares that this function is OK to use\n// any object so long as it includes the property name:\n\ninterface AnyObjectButMustHaveName {\n name: string;\n [key: string]: any;\n}\n\nconst printFormattedName = (input: AnyObjectButMustHaveName) => {};\n\nprintFormattedName({ name: \"joey\" });\nprintFormattedName({ name: \"joey\", age: 23 });\n\n// If you'd like to learn more about index-signatures\n// we recommend:\n//\n// https://www.typescriptlang.org/docs/handbook/interfaces.html#excess-property-checks\n// https://basarat.gitbooks.io/typescript/docs/types/index-signatures.html\n\n// You can also allow this kind of behavior everywhere\n// via the tsconfig flag suppressExcessPropertyErrors -\n// however, you can't know if others using your API have\n// this set to off.\n\n// Functions in JavaScript can accept different sets of params.\n// There are two common patterns for describing these: union\n// types for parameters/return, and function overloads.\n\n// Using union types in your parameters makes sense if there\n// are only one or two changes and documentation does not need\n// to change between functions.\n\nconst boolOrNumberFunction = (input: boolean | number) => {};\n\nboolOrNumberFunction(true);\nboolOrNumberFunction(23);\n\n// Function overloads on the other hand offer a much richer\n// syntax for the parameters and return types.\n\ninterface BoolOrNumberOrStringFunction {\n /** Takes a bool, returns a bool */\n (input: boolean): boolean;\n /** Takes a number, returns a number */\n (input: number): number;\n /** Takes a string, returns a bool */\n (input: string): boolean;\n}\n\n// If this is your first time seeing declare, it allows you\n// to tell TypeScript something exists even if it doesn't\n// exist in the runtime in this file. Useful for mapping\n// code with side-effects but extremely useful for demos\n// where making the implementation would be a lot of code.\n\ndeclare const boolOrNumberOrStringFunction: BoolOrNumberOrStringFunction;\n\nconst boolValue = boolOrNumberOrStringFunction(true);\nconst numberValue = boolOrNumberOrStringFunction(12);\nconst boolValue2 = boolOrNumberOrStringFunction(\"string\");\n\n// If you hover over the above values and functions you\n// can see the right documentation and return values.\n\n// Using function overloads can get you very far, however\n// there's another tool for dealing with different types of\n// inputs and return values and that is generics.\n\n// These provide a way for you to have types as placeholder\n// variables in type definitions.\n\n// example:generic-functions\n// example:function-chaining\n"},{"path":["JavaScript","Helping with JavaScript"],"title":"Errors","name":"Errors.ts","lang":"en","id":"errors","sortIndex":3,"hash":"1bbdcd914e8ca43ee6e9136fb5ec1fcda9e46f8653709741cd1cb8f8687455c85f9ec05dd058cc45955097ba5a20adf0fb786cd96e202cc0f407c882e18f69ce","code":"// By default TypeScript doesn't provide error messaging\n// inside JavaScript. Instead the tooling is focused on\n// providing rich support for editors.\n\n// Turning on errors however, is pretty easy. In a\n// typical JS file, all that's required to turn on TypeScript\n// error messages is adding the following comment:\n\n// @ts-check\n\nlet myString = \"123\";\nmyString = {};\n\n// This may start to add a lot of red squiggles inside your\n// JS file. While still working inside JavaScript, you have\n// a few tools to fix these errors.\n\n// For some of the trickier errors, which you don't feel\n// code changes should happen, you can use JSDoc annotations\n// to tell TypeScript what the types should be:\n\n/** @type {string | {}} */\nlet myStringOrObject = \"123\";\nmyStringOrObject = {};\n\n// Which you can read more on here: example:jsdoc-support\n\n// You could declare the failure unimportant, by telling\n// TypeScript to ignore the next error:\n\nlet myIgnoredError = \"123\";\n// @ts-ignore\nmyStringOrObject = {};\n\n// You can use type inference via the flow of code to make\n// changes to your JavaScript: example:code-flow"},{"path":["JavaScript","Helping with JavaScript"],"title":"Quick Fixes","name":"Quick Fixes.ts","lang":"en","id":"quick-fixes","sortIndex":1,"hash":"a7272e57c1030d9514028e76f257c0e8cbd2b35774e944818b53eb674603e229d741b7b086e0cecf17077df2cab22a5e1d692f6c9aaa11e9af2028385d65c38a","compilerSettings":{},"code":"// TypeScript provides quick-fix recommendations for\n// common accidents. Prompts show up in your editor based\n// on these recommendations.\n\n// For example TypeScript can provide quick-fixes\n// for typos in your types:\n\nconst eulersNumber = 2.7182818284;\neulersNumber.toStrang();\n// ^______^ - select this to see the light bulb\n\nclass ExampleClass {\n method() {\n this.notDeclared = 10;\n }\n}\n"},{"path":["JavaScript","JavaScript Essentials"],"title":"Code Flow","name":"Code Flow.ts","lang":"en","id":"code-flow","sortIndex":3,"hash":"0c539c06a03347b4dce88f0ae497d2559117ded9303f28bfe32c077f5e764d09e5ce0b7d37e16a6d2648920f7cc1aef7b2921b68fb329f2f6a7d525dab8f431f","compilerSettings":{"strictNullChecks":true},"code":"// How code flows inside our JavaScript files can affect\n// the types throughout our programs.\n\nconst users = [{ name: \"Ahmed\" }, { name: \"Gemma\" }, { name: \"Jon\" }];\n\n// We're going to look to see if we can find a user named \"jon\".\nconst jon = users.find((u) => u.name === \"jon\");\n\n// In the above case, 'find' could fail. In that case we\n// don't have an object. This creates the type:\n//\n// { name: string } | undefined\n//\n// If you hover your mouse over the three following uses of 'jon' below,\n// you'll see how the types change depending on where the word is located:\n\nif (jon) {\n jon;\n} else {\n jon;\n}\n\n// The type '{ name: string } | undefined' uses a TypeScript\n// feature called union types. A union type is a way to\n// declare that an object could be one of many things.\n//\n// The pipe acts as the separator between different types.\n// JavaScript's dynamic nature means that lots of functions\n// receive and return objects of unrelated types and we need\n// to be able to express which ones we might be dealing with.\n\n// We can use this in a few ways. Let's start by looking at\n// an array where the values have different types.\n\nconst identifiers = [\"Hello\", \"World\", 24, 19];\n\n// We can use the JavaScript 'typeof x === y' syntax to\n// check for the type of the first element. You can hover on\n// 'randomIdentifier' below to see how it changes between\n// different locations\n\nconst randomIdentifier = identifiers[0];\nif (typeof randomIdentifier === \"number\") {\n randomIdentifier;\n} else {\n randomIdentifier;\n}\n\n// This control flow analysis means that we can write vanilla\n// JavaScript and TypeScript will try to understand how the\n// code types will change in different locations.\n\n// To learn more about code flow analysis:\n// - example:type-guards\n\n// To continue reading through examples you could jump to a\n// few different places now:\n//\n// - Modern JavaScript: example:immutability\n// - Type Guards: example:type-guards\n// - Functional Programming with JavaScript example:function-chaining"},{"path":["JavaScript","JavaScript Essentials"],"title":"Functions","name":"Functions.ts","lang":"en","id":"functions","sortIndex":2,"hash":"81852f84c9c4de521e04065fc1cadc3016ddf3132c9b7f444f1ac2e04b91e3718f6c12f160ac3372d010fd1aec2e38729909b474d7fb80ceba84623bfc25f34a","compilerSettings":{"noImplicitAny":false},"code":"// There are quite a few ways to declare a function in\n// JavaScript. Let's look at a function which adds two\n// numbers together:\n\n// Creates a function in global scope called addOldSchool\nfunction addOldSchool(x, y) {\n return x + y;\n}\n\n// You can move the name of the function to a variable\n// name also\nconst anonymousOldSchoolFunction = function (x, y) {\n return x + y;\n};\n\n// You can also use fat-arrow shorthand for a function\nconst addFunction = (x, y) => {\n return x + y;\n};\n\n// We're going to focus on the last one, but everything\n// applies to all three formats.\n\n// TypeScript provides additional syntax which adds to a\n// function definition and offers hints on what types\n// are expected by this function.\n//\n// Up next is the most open version of the add function, it\n// says that add takes two inputs of any type: this could\n// be strings, numbers or objects which you've made.\n\nconst add1 = (x: any, y: any) => {\n return x + y;\n};\nadd1(\"Hello\", 23);\n\n// This is legitimate JavaScript (strings can be added\n// like this for example) but isn't optimal for our function\n// which we know is for numbers, so we'll convert the x and\n// y to only be numbers.\n\nconst add2 = (x: number, y: number) => {\n return x + y;\n};\nadd2(16, 23);\nadd2(\"Hello\", 23);\n\n// Great. We get an error when anything other than a number\n// is passed in. If you hover over the word add2 above,\n// you'll see that TypeScript describes it as:\n//\n// const add2: (x: number, y: number) => number\n//\n// Where it has inferred that when the two inputs are\n// numbers the only possible return type is a number.\n// This is great, you don't have to write extra syntax.\n// Let's look at what it takes to do that:\n\nconst add3 = (x: number, y: number): string => {\n return x + y;\n};\n\n// This function fails because we told TypeScript that it\n// should expect a string to be returned but the function\n// didn't live up to that promise.\n\nconst add4 = (x: number, y: number): number => {\n return x + y;\n};\n\n// This is a very explicit version of add2 - there are\n// cases when you want to use the explicit return type\n// syntax to give yourself a space to work within before\n// you get started. A bit like how test-driven development\n// recommends starting with a failing test, but in this case\n// it's with a failing shape of a function instead.\n\n// This example is only a primer, you can learn a lot more\n// about how functions work in TypeScript in the handbook and\n// inside the Functional JavaScript section of the examples:\n//\n// https://www.typescriptlang.org/docs/handbook/functions.html\n// example:function-chaining\n\n// And to continue our tour of JavaScript essentials,\n// we'll look at how code flow affects the TypeScript types:\n// example:code-flow"},{"path":["JavaScript","JavaScript Essentials"],"title":"Hello World","name":"Hello World.ts","lang":"en","id":"hello-world","sortIndex":1,"hash":"625ff5289426d2e67dce5e5af1fae07440f0f5584c773e8b5829a00fa3bfc519e038a8d8dd95188004b16213f4f62cb3f88a46bbd3abb2b7075d2b119f622706","compilerSettings":{"target":1},"code":"// Welcome to the TypeScript playground. This site is a lot\n// like running a TypeScript project inside a web browser.\n\n// The playground makes it easy for you to safely experiment\n// with ideas in TypeScript by making it trivial to share\n// these projects. The URL for this page is everything\n// required to load the project for someone else.\n\nconst hello = \"Hello\";\n\n// You can see on the right the result of the TypeScript\n// compiler: this is vanilla JavaScript which can run on\n// browsers, servers or anywhere really.\n\nconst world = \"World\";\n\n// You can see how it makes tiny changes to the code, by\n// converting a \"const\" to a \"var\". This is one of the many\n// things TypeScript does to make it possible to run\n// anywhere JavaScript runs.\n\nconsole.log(hello + \" \" + world);\n\n// Now that you have an idea of how the playground works,\n// let's look at how TypeScript makes working with\n// JavaScript more fun. During this section we'll be trying\n// to keep as close to vanilla JavaScript as possible to\n// show how you can re-use existing knowledge.\n//\n// Click below to continue:\n//\n// example:objects-and-arrays"},{"path":["JavaScript","JavaScript Essentials"],"title":"Objects and Arrays","name":"Objects and Arrays.ts","lang":"en","id":"objects-and-arrays","sortIndex":1,"hash":"d7693ab1214a51469e3e85c07116fe9f049661ce9f5f4189411e2bcfc72e452bf7d79750b8766803ed254dd0d902380916a85c954df828df84058f8a457ed297","compilerSettings":{"strict":false},"code":"// JavaScript objects are collections of values wrapped up\n// with named keys.\n\nconst userAccount = {\n name: \"Kieron\",\n id: 0,\n};\n\n// You can combine these to make larger, more complex\n// data-models.\n\nconst pie = {\n type: \"Apple\",\n};\n\nconst purchaseOrder = {\n owner: userAccount,\n item: pie,\n};\n\n// If you use your mouse to hover over some of these words\n// (try purchaseOrder above) you can see how TypeScript is\n// interpreting your JavaScript into labeled types.\n\n// Values can be accessed via the \".\", so to get a\n// username for a purchase order:\nconsole.log(purchaseOrder.item.type);\n\n// If you hover your mouse over each part of the code\n// between the ()s, you can see TypeScript offering more\n// information about each part. Try re-writing this below:\n\n// Copy this in the next line, character by character:\n//\n// purchaseOrder.item.type\n\n// TypeScript provides feedback to the playground\n// about what JavaScript objects are available in this\n// file and lets you avoid typos and see additional\n// information without having to look it up in another place.\n\n// TypeScript also offers these same features to arrays.\n// Here's an array with just our purchase order above in it.\n\nconst allOrders = [purchaseOrder];\n\n// If you hover on allOrders, you can tell it's an array\n// because the hover info ends with []. You can access the\n// first order by using square brackets with an index\n// (starting from zero).\n\nconst firstOrder = allOrders[0];\nconsole.log(firstOrder.item.type);\n\n// An alternative way to get an object is via pop-ing the\n// array to remove objects. Doing this removes the object\n// from the array, and returns the object. This is called\n// mutating the array, because it changes the underlying\n// data inside it.\n\nconst poppedFirstOrder = allOrders.pop();\n\n// Now allOrders is empty. Mutating data can be useful for\n// many things, but one way to reduce the complexity in your\n// codebases is to avoid mutation. TypeScript offers a way\n// to declare an array readonly instead:\n\n// Creates a type based on the shape of a purchase order:\ntype PurchaseOrder = typeof purchaseOrder;\n\n// Creates a readonly array of purchase orders\nconst readonlyOrders: readonly PurchaseOrder[] = [purchaseOrder];\n\n// Yep! That's a bit more code for sure. There's four\n// new things here:\n//\n// type PurchaseOrder - Declares a new type to TypeScript.\n//\n// typeof - Use the type inference system to set the type\n// based on the const which is passed in next.\n//\n// purchaseOrder - Get the variable purchaseOrder and tell\n// TypeScript this is the shape of all\n// objects in the orders array.\n//\n// readonly - This object does not support mutation, once\n// it is created then the contents of the array\n// will always stay the same.\n//\n// Now if you try to pop from the readonlyOrders, TypeScript\n// will raise an error.\n\nreadonlyOrders.pop();\n\n// You can use readonly in all sorts of places, it's a\n// little bit of extra syntax here and there, but it\n// provides a lot of extra safety.\n\n// You can find out more about readonly:\n// - https://www.typescriptlang.org/docs/handbook/interfaces.html#readonly-properties\n// - https://basarat.gitbooks.io/typescript/content/docs/types/readonly.html\n\n// And you can carry on learning about JavaScript and\n// TypeScript in the example on functions:\n// example:functions\n//\n// Or if you want to know more about immutability:\n// example:immutability"},{"path":["JavaScript","Modern JavaScript"],"title":"Async Await","name":"Async Await.ts","lang":"en","id":"async-await","sortIndex":1,"hash":"1a7e41b65df66b6c014135958b1dad6762a2704d3c672ceb5d389a7569c8963ca11f9bf9475e36beef29a847383ebbb65219f0b481b25fbee10382b23be7149a","code":"// Modern JavaScript added a way to handle callbacks in an\n// elegant way by adding a Promise based API which has special\n// syntax that lets you treat asynchronous code as though it\n// acts synchronously.\n\n// Like all language features, this is a trade-off in\n// complexity: making a function async means your return\n// values are wrapped in Promises. What used to return a\n// string, now returns a Promise.\n\nconst func = () => \":wave:\";\nconst asyncFunc = async () => \":wave:\";\n\nconst myString = func();\nconst myPromiseString = asyncFunc();\n\nmyString.length;\n\n// myPromiseString is a Promise, not the string:\n\nmyPromiseString.length;\n\n// You can use the await keyword to convert a promise\n// into its value. Today, these only work inside an async\n// function.\n\nconst myWrapperFunction = async () => {\n const myString = func();\n const myResolvedPromiseString = await asyncFunc();\n\n // Via the await keyword, now myResolvedPromiseString\n // is a string\n myString.length;\n myResolvedPromiseString.length;\n};\n\n// Code which is running via an await can throw errors,\n// and it's important to catch those errors somewhere.\n\nconst myThrowingFunction = async () => {\n throw new Error(\"Do not call this\");\n};\n\n// We can wrap calling an async function in a try catch to\n// handle cases where the function acts unexpectedly.\n\nconst asyncFunctionCatching = async () => {\n const myReturnValue = \"Hello world\";\n try {\n await myThrowingFunction();\n } catch (error) {\n console.error(\"myThrowingFunction failed\", error);\n }\n return myReturnValue;\n};\n\n// Due to the ergonomics of this API being either returning\n// a single value, or throwing, you should consider offering\n// information about the result inside the returned value and\n// use throw only when something truly exceptional has\n// occurred.\n\nconst exampleSquareRootFunction = async (input: any) => {\n if (isNaN(input)) {\n throw new Error(\"Only numbers are accepted\");\n }\n\n if (input < 0) {\n return { success: false, message: \"Cannot square root negative number\" };\n } else {\n return { success: true, value: Math.sqrt(input) };\n }\n};\n\n// Then the function consumers can check in the response and\n// figure out what to do with your return value. While this\n// is a trivial example, once you have started working with\n// networking code these APIs become worth the extra syntax.\n\nconst checkSquareRoot = async (value: number) => {\n const response = await exampleSquareRootFunction(value);\n if (response.success) {\n response.value;\n }\n};\n\n// Async/Await took code which looked like this:\n\n// getResponse(url, (response) => {\n// getResponse(response.url, (secondResponse) => {\n// const responseData = secondResponse.data\n// getResponse(responseData.url, (thirdResponse) => {\n// ...\n// })\n// })\n// })\n\n// And let it become linear like:\n\n// const response = await getResponse(url)\n// const secondResponse = await getResponse(response.url)\n// const responseData = secondResponse.data\n// const thirdResponse = await getResponse(responseData.url)\n// ...\n\n// Which can make the code sit closer to left edge, and\n// be read with a consistent rhythm."},{"path":["JavaScript","Modern JavaScript"],"title":"Immutability","name":"Immutability.ts","lang":"en","id":"immutability","sortIndex":1,"hash":"894db142ca1ea5b7627eb14b70764fd760d99afe5b3fdc3694d82c2de3debd5ec6cf47116d3e58e33cd197cf3455aed59941cf0353ed4432fdea26720273d887","compilerSettings":{},"code":"// JavaScript is a language with a few ways to declare that\n// some of your objects don't change. The most prominent is\n// const - which says that the value won't change.\n\nconst helloWorld = \"Hello World\";\n\n// You cannot change helloWorld now, TypeScript will give\n// you an error about this, because you would get one at\n// runtime instead.\n\nhelloWorld = \"Hi world\";\n\n// Why care about immutability? A lot of this is about\n// reducing complexity in your code. If you can reduce the\n// number of things which can change, then there are less\n// things to keep track of.\n\n// Using const is a great first step, however this fails\n// down a bit when using objects.\n\nconst myConstantObject = {\n msg: \"Hello World\",\n};\n\n// myConstantObject is not quite a constant though, because\n// we can still make changes to parts of the object, for\n// example we can change msg:\n\nmyConstantObject.msg = \"Hi World\";\n\n// const means the value at that point stays the same, but\n// that the object itself may change internally. This can\n// be changed using Object.freeze.\n\nconst myDefinitelyConstantObject = Object.freeze({\n msg: \"Hello World\",\n});\n\n// When an object is frozen, then you cannot change the\n// internals. TypeScript will offer errors in these cases:\n\nmyDefinitelyConstantObject.msg = \"Hi World\";\n\n// This works the same for arrays too:\n\nconst myFrozenArray = Object.freeze([\"Hi\"]);\nmyFrozenArray.push(\"World\");\n\n// Using freeze means you can trust that the object is\n// staying the same under the hood.\n\n// TypeScript has a few extra syntax hooks to improve working\n// with immutable data which you can find in the TypeScript\n// section of the examples:\n//\n// example:literals\n// example:type-widening-and-narrowing\n"},{"path":["JavaScript","Modern JavaScript"],"title":"Import Export","name":"Import Export.ts","lang":"en","id":"import-export","sortIndex":1,"hash":"fbf32f2325278144a4aa5cb7fb0d055fd583368c0dbb73f60adc3fcab8abcf65894947c85b4848ec441a894795bebd770510a2ea64f5f09394b7cb41dc46528a","code":"// JavaScript added import/export to the language back in 2016\n// and TypeScript has complete support for this style of\n// linking between files and to external modules. TypeScript\n// expands on this syntax by also allowing types to be passed\n// with code.\n\n// Let's look at importing code from a module.\n\nimport { danger, message, warn, DangerDSLType } from \"danger\";\n\n// This takes a set of named imports from a node module\n// called danger. While there are more than four imports,\n// these are the only ones that we have chosen to import.\n\n// Specifically naming which imports you are importing\n// gives tools the ability to remove unused code in your\n// apps, and helps you understand what is being used in\n// a particular file.\n\n// In this case: danger, message and warn are JavaScript\n// imports - where as DangerDSLType is an interface type.\n\n// TypeScript lets engineers document their code using\n// JSDoc, and docs are imported also. For example if\n// you hover on the different parts below, you see\n// explanations of what they are.\n\ndanger.git.modified_files;\n\n// If you want to know how to provide these documentation\n// annotations read example:jsdoc-support\n\n// Another way to import code is by using the default export\n// of a module. An example of this is the debug module, which\n// exposes a function that creates a logging function.\n\nimport debug from \"debug\";\nconst log = debug(\"playground\");\nlog(\"Started running code\");\n\n// Because of the nature of default exports having no true\n// name, they can be tricky when applied with static analysis\n// tools like the refactoring support in TypeScript but they\n// have their uses.\n\n// Because there is a long history in importing/exporting code\n// in JavaScript, there is a confusing part of default exports:\n// Some exports have documentation that implies you can write\n// an import like this:\n\nimport req from \"request\";\n\n// However that fails, and then you find a stack overflow\n// which recommends the import as:\n\nimport * as req from \"request\";\n\n// And this works. Why? We'll get back to that at the end of\n// our section on exporting.\n\n// In order to import, you must be able to export. The modern\n// way to write exports is using the export keyword.\n\n/** The current stickers left on the roll */\nexport const numberOfStickers = 11;\n\n// This could be imported into another file by:\n//\n// import { numberOfStickers } from \"./path/to/file\"\n\n// You can have as many of those in a file as you like. Then\n// a default export is close to the same thing.\n\n/** Generates a sticker for you */\nconst stickerGenerator = () => { };\nexport default stickerGenerator;\n\n// This could be imported into another file by:\n//\n// import getStickers from \"./path/to/file\"\n//\n// The naming is up to the module consumer.\n\n// These aren't the only types of imports, just the most common\n// in modern code. Covering all of the ways code can cross\n// module boundaries is a very long topic in the handbook:\n//\n// https://www.typescriptlang.org/docs/handbook/modules.html\n\n// However, to try cover that last question. If you look at\n// the JavaScript code for this example - you'll see this:\n\n// var stickerGenerator = function () { };\n// exports.default = stickerGenerator;\n\n// This sets the default property on the exports object\n// to be stickerGenerator. There is code out there which\n// sets exports to be a function, instead of an object.\n//\n// TypeScript opted to stick with the ECMAScript specification\n// about how to handle those cases, which is to raise an\n// error. However, there is a compiler setting which will\n// automatically handle those cases for you which is\n// esModuleInterop.\n//\n// If you turn that on for this example, you will see that\n// error go away."},{"path":["JavaScript","Modern JavaScript"],"title":"JSDoc Support","name":"JSDoc Support.js","lang":"en","id":"jsdoc-support","sortIndex":3,"hash":"9ce4a87468aa0882d7fa4875ce04d39b10e2b6a68c011e4bd553e1528f422e74faf290dc3d677c2353109a04a3423b7afb0e5d1fcdc86e711605806ea6fdf6dc","code":"// TypeScript has very rich JSDoc support, for a lot of cases\n// you can even skip making your files .ts and just use JSDoc\n// annotations to create a rich development environment.\n//\n// A JSDoc comment is a multi-line comment which starts with\n// two stars instead of one.\n\n/* This is a normal comment */\n/** This is a JSDoc comment */\n\n// JSDoc comments become attached to the closest JavaScript\n// code below it.\n\nconst myVariable = \"Hi\";\n\n// If you hover over myVariable, you can see that it has the\n// text from inside the JSDoc comment attached.\n\n// JSDoc comments are a way to provide type information to\n// TypeScript and your editors. Let's start with an easy one\n// setting a variable's type to a built-in type.\n\n// For all of these examples, you can hover over the name,\n// and on the next line try write [example]. to see the\n// auto-complete options.\n\n/** @type {number} */\nvar myNumber;\n\n// You can see all of the supported tags in the handbook:\n//\n// https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html#supported-jsdoc\n\n// However, we'll try go through some of the more common examples\n// here. You can also copy & paste any examples from the handbook\n// into here.\n\n// Importing the types for JavaScript configuration files:\n\n/** @type { import(\"webpack\").Config } */\nconst config = {};\n\n// Creating a complex type to re-use in many places:\n\n/**\n * @typedef {Object} User - a User account\n * @property {string} displayName - the name used to show the user\n * @property {number} id - a unique id\n */\n\n// Then use it by referencing the typedef's name:\n\n/** @type { User } */\nconst user = {};\n\n// There's the TypeScript compatible inline type shorthand,\n// which you can use for both type and typedef:\n\n/** @type {{ owner: User, name: string }} */\nconst resource;\n\n/** @typedef {{owner: User, name: string}} Resource */\n\n/** @type {Resource} */\nconst otherResource;\n\n// Declaring a typed function:\n\n/**\n * Adds two numbers together\n * @param {number} a The first number\n * @param {number} b The second number\n * @returns {number}\n */\nfunction addTwoNumbers(a, b) {\n return a + b;\n}\n\n// You can use most of TypeScript's type tools, like unions:\n\n/** @type {(string | boolean)} */\nlet stringOrBoolean = \"\";\nstringOrBoolean = false;\n\n// Extending globals in JSDoc is a more involved process\n// which you can see in the VS Code docs:\n//\n// https://code.visualstudio.com/docs/nodejs/working-with-javascript#_global-variables-and-type-checking\n\n// Adding JSDoc comments to your functions is a win-win\n// situation; you get better tooling and so do all your\n// API consumers."},{"path":["JavaScript","Working With Classes"],"title":"Classes 101","name":"Classes 101.ts","lang":"en","id":"classes-101","sortIndex":1,"hash":"96493525eee69fdf172ed4dbfe3850fd6cf821bdef458a36db1887be1547ad95b0360f1c63da3d2e1c78a4d35893af8cdd825c8b457b7321a623b3c58a95625e","code":"// A class is a special type of JavaScript object which\n// is always created via a constructor. These classes\n// act a lot like objects, and have an inheritance structure\n// similar to languages such as Java/C#/Swift.\n\n// Here's an example class:\n\nclass Vendor {\n name: string;\n\n constructor(name: string) {\n this.name = name;\n }\n\n greet() {\n return \"Hello, welcome to \" + this.name;\n }\n}\n\n// An instance can be created via the new keyword, and\n// you can call methods and access properties from the\n// object.\n\nconst shop = new Vendor(\"Ye Olde Shop\");\nconsole.log(shop.greet());\n\n// You can subclass an object. Here's a food cart which\n// has a variety as well as a name:\n\nclass FoodTruck extends Vendor {\n cuisine: string;\n\n constructor(name: string, cuisine: string) {\n super(name);\n this.cuisine = cuisine;\n }\n\n greet() {\n return \"Hi, welcome to food truck \" + this.name + \". We serve \" + this.cuisine + \" food.\";\n }\n}\n\n// Because we indicated that there needs to be two arguments\n// to create a new FoodTruck, TypeScript will provide errors\n// when you only use one:\n\nconst nameOnlyTruck = new FoodTruck(\"Salome's Adobo\");\n\n// Correctly passing in two arguments will let you create a\n// new instance of the FoodTruck:\n\nconst truck = new FoodTruck(\"Dave's Doritos\", \"junk\");\nconsole.log(truck.greet());"},{"path":["JavaScript","Working With Classes"],"title":"Generic Classes","name":"Generic Classes.ts","lang":"en","id":"generic-classes","sortIndex":3,"hash":"41e50710adc3a129317edf916416bffc8ad72a0b35258986c8234e37a66fd3a0862c4ad5cd242b5d45bcc7db54e90212bd33acf0dfd24d8068dc9a2793fd8ebd","code":"// This example is mostly in TypeScript, because it is much\n// easier to understand this way first. At the end we'll\n// cover how to create the same class but using JSDoc instead.\n\n// Generic Classes are a way to say that a particular type\n// depends on another type. For example, here is a drawer\n// which can hold any sort of object, but only one type:\n\nclass Drawer {\n contents: ClothingType[] = [];\n\n add(object: ClothingType) {\n this.contents.push(object);\n }\n\n remove() {\n return this.contents.pop();\n }\n}\n\n// In order to use a Drawer, you will need another\n// type to work with:\n\ninterface Sock {\n color: string;\n}\n\ninterface TShirt {\n size: \"s\" | \"m\" | \"l\";\n}\n\n// We can create a Drawer just for socks by passing in the\n// type Sock when we create a new Drawer:\nconst sockDrawer = new Drawer();\n\n// Now we can add or remove socks to the drawer:\nsockDrawer.add({ color: \"white\" });\nconst mySock = sockDrawer.remove();\n\n// As well as creating a drawer for TShirts:\nconst tshirtDrawer = new Drawer();\ntshirtDrawer.add({ size: \"m\" });\n\n// If you're a bit eccentric, you could even create a drawer\n// which mixes Socks and TShirts by using a union:\n\nconst mixedDrawer = new Drawer();\n\n// Creating a class like Drawer without the extra TypeScript\n// syntax requires using the template tag in JSDoc. In this\n// example we define the template variable, then provide\n// the properties on the class:\n\n// To have this work in the playground, you'll need to change\n// the settings to be a JavaScript file, and delete the\n// TypeScript code above\n\n/**\n * @template {{}} ClothingType\n */\nclass Dresser {\n constructor() {\n /** @type {ClothingType[]} */\n this.contents = [];\n }\n\n /** @param {ClothingType} object */\n add(object) {\n this.contents.push(object);\n }\n\n /** @return {ClothingType} */\n remove() {\n return this.contents.pop();\n }\n}\n\n// Then we create a new type via JSDoc:\n\n/**\n * @typedef {Object} Coat An item of clothing\n * @property {string} color The colour for coat\n */\n\n// Then when we create a new instance of that class\n// we use @type to assign the variable as a Dresser\n// which handles Coats.\n\n/** @type {Dresser} */\nconst coatDresser = new Dresser();\n\ncoatDresser.add({ color: \"green\" });\nconst coat = coatDresser.remove();"},{"path":["JavaScript","Working With Classes"],"title":"Mixins","name":"Mixins.ts","lang":"en","id":"mixins","sortIndex":4,"hash":"4832d926115ead092b286f398581a066706fc44d82ecc368a82d51c73158a6863500a51093e47211530184633f87fff7d54b4f27f66387d37182c4ae3e60d629","code":"// Mixins are a faux-multiple inheritance pattern for classes\n// in JavaScript which TypeScript has support for. The pattern\n// allows you to create a class which is a merge of many\n// classes.\n\n// To get started, we need a type which we'll use to extend\n// other classes from. The main responsibility is to declare\n// that the type being passed in is a class.\n\ntype Constructor = new (...args: any[]) => {};\n\n// Then we can create a series of classes which extend\n// the final class by wrapping it. This pattern works well\n// when similar objects have different capabilities.\n\n// This mixin adds a scale property, with getters and setters\n// for changing it with an encapsulated private property:\n\nfunction Scale(Base: TBase) {\n return class extends Base {\n // Mixins may not declare private/protected properties\n // however, you can use ES2020 private fields\n _scale = 1;\n\n setScale(scale: number) {\n this._scale = scale;\n }\n\n get scale(): number {\n return this._scale;\n }\n };\n}\n\n// This mixin adds extra methods around alpha composition\n// something which modern computers use to create depth:\n\nfunction Alpha(Base: TBase) {\n return class extends Base {\n alpha = 1;\n\n setHidden() {\n this.alpha = 0;\n }\n\n setVisible() {\n this.alpha = 1;\n }\n\n setAlpha(alpha: number) {\n this.alpha = alpha;\n }\n };\n}\n\n// A simple sprite base class which will then be extended:\n\nclass Sprite {\n name = \"\";\n x = 0;\n y = 0;\n\n constructor(name: string) {\n this.name = name;\n }\n}\n\n// Here we create two different types of sprites\n// which have different capabilities:\n\nconst ModernDisplaySprite = Alpha(Scale(Sprite));\nconst EightBitSprite = Scale(Sprite);\n\n// Creating instances of these classes shows that\n// the objects have different sets of properties\n// and methods due to their mixins:\n\nconst flappySprite = new ModernDisplaySprite(\"Bird\");\nflappySprite.x = 10;\nflappySprite.y = 20;\nflappySprite.setVisible();\nflappySprite.setScale(0.8);\nconsole.log(flappySprite.scale);\n\nconst gameBoySprite = new EightBitSprite(\"L block\");\ngameBoySprite.setScale(0.3);\n\n// Fails because an EightBitSprite does not have\n// the mixin for changing alphas:\ngameBoySprite.setAlpha(0.5);\n\n// If you want to make more guarantees over the classes\n// which you wrap, you can use a constructor with generics.\n\ntype GConstructor = new (...args: any[]) => T;\n\n// Now you can declare that this mixin can only be\n// applied when the base class is a certain shape.\n\ntype Moveable = GConstructor<{ setXYAcceleration: (x: number, y: number) => void }>;\n\n// We can then create a mixin which relies on the function\n// present in the parameter to the GConstructor above.\n\nfunction Jumpable(Base: TBase) {\n return class extends Base {\n jump() {\n // This mixin knows about setXYAcceleration now\n this.setXYAcceleration(0, 20);\n }\n };\n}\n\n// We cannot create this sprite until there is a class\n// in the mixin hierarchy which adds setXYAcceleration:\nconst UserSprite = new Jumpable(ModernDisplaySprite);"},{"path":["JavaScript","Working With Classes"],"title":"This","name":"This.ts","lang":"en","id":"this","sortIndex":2,"hash":"3a34b21e77ee07ec74a01fc523efebb3e1a7a25ae334d3db561998ec38bf1b6cc519d04ff01b8a2f1150b0dae086fcd499001929f60092430fedb2ea2f1d2efb","code":"// When calling a method of a class, you generally expect it\n// to refer to the current instance of the class.\n\nclass Safe {\n contents: string;\n\n constructor(contents: string) {\n this.contents = contents;\n }\n\n printContents() {\n console.log(this.contents);\n }\n}\n\nconst safe = new Safe(\"Crown Jewels\");\nsafe.printContents();\n\n// If you come from an objected oriented language where the\n// this/self variable is easily predictable, then you may\n// find you need to read up on how confusing 'this' can be:\n//\n// https://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/\n// https://aka.ms/AA5ugm2\n\n// TLDR: this can change. The reference to which this refers\n// to can be different depending on how you call the function.\n\n// For example, if you use a reference to the func in another\n// object, and then call it through that - the this variable\n// has moved to refer to the hosting object:\n\nconst customObjectCapturingThis = { contents: \"http://gph.is/VxeHsW\", print: safe.printContents };\ncustomObjectCapturingThis.print(); // Prints \"http://gph.is/VxeHsW\" - not \"Crown Jewels\"\n\n// This is tricky, because when dealing with callback APIs -\n// it can be very tempting to pass the function reference\n// directly. This can be worked around by creating a new\n// function at the call site.\n\nconst objectNotCapturingThis = { contents: \"N/A\", print: () => safe.printContents() };\nobjectNotCapturingThis.print();\n\n// There are a few ways to work around this problem. One\n// route is to force the binding of this to be the object\n// you originally intended via bind.\n\nconst customObjectCapturingThisAgain = { contents: \"N/A\", print: safe.printContents.bind(safe) };\ncustomObjectCapturingThisAgain.print();\n\n// To work around an unexpected this context, you can also\n// change how you create functions in your class. By\n// creating a property which uses an arrow function, the\n// binding of this is done at a different time. Which makes\n// it more predictable for those less experienced with the\n// JavaScript runtime.\n\nclass SafelyBoundSafe {\n contents: string;\n\n constructor(contents: string) {\n this.contents = contents;\n }\n\n printContents = () => {\n console.log(this.contents);\n };\n}\n\n// Now passing the function to another object\n// to run does not accidentally change this.\n\nconst saferSafe = new SafelyBoundSafe(\"Golden Skull\");\nsaferSafe.printContents();\n\nconst customObjectTryingToChangeThis = {\n contents: \"http://gph.is/XLof62\",\n print: saferSafe.printContents,\n};\n\ncustomObjectTryingToChangeThis.print();\n\n// If you have a TypeScript project, you can use the compiler\n// flag noImplicitThis to highlight cases where TypeScript\n// cannot determine what type \"this\" is for a function.\n\n// You can learn more about that in the handbook:\n//\n// https://www.typescriptlang.org/docs/handbook/utility-types.html#thistypetype"},{"path":["Playground","Config"],"title":"JavaScript Playgrounds","name":"JavaScript Playgrounds.js","lang":"en","id":"javascript-playgrounds","sortIndex":3,"hash":"c82f9ebf73c6c1ef4769f412420202b89c23254b43c35cd41a4c97e8dcb201d33152dcc6a0f83676d07cb1e0d3fdc21449c25692ecc7d99a277f40ce3db23ea5","code":"// The playground can now handle JavaScript files.\n\n// It's quite reasonable to wonder why we would add support\n// for JavaScript in the playground, but it's likely that the\n// majority of TypeScript's users are using JavaScript.\n\n// TypeScript can use inferred types, type acquisition and\n// JSDoc support in a JavaScript file to provide a great\n// tooling environment:\n//\n// example:objects-and-arrays\n// example:automatic-type-acquisition\n// example:jsdoc-support\n\n// The playground supporting JavaScript means that you\n// can learn and walk people through complicated JSDoc\n// examples, or debug issues when there are expectation\n// mis-matches.\n\n// For example, how come this JSDoc comment isn't typed\n// correctly?\n\n/**\n * Adds two numbers together\n * @param {number} The first number\n * @param {number} The second number\n * @returns {number}\n */\nfunction addTwoNumbers(a, b) {\n return a + b;\n}\n\n// It's much easier to figure that out in an environment\n// where you can instantly see what's going on by hovering."},{"path":["Playground","Config"],"title":"New Compiler Defaults","name":"New Compiler Defaults.ts","lang":"en","id":"new-compiler-defaults","sortIndex":1,"hash":"047cc94eec810017d3b49cea66ea46b451070246df6e873f172b5999fa63c880246dc603a66e3205c25bb35957ebfb8ea91cad6e53199470b9c7941d95491eb0","code":"// Yes, we know, the defaults for TypeScript's tsconfig do\n// not have strict turned on. However, at every chance we\n// can the team recommends that users try migrate towards\n// having strict mode enabled in their configs.\n\n// For the playground however, we can happily set the defaults\n// to be strict. The playground will also keep track of the\n// compiler flags which have changed from the playground's\n// defaults and make them sharable in the URL.\n\n// You can read more about the URLs in\n// example:sharable-urls\n\n// Wonder what the new defaults are?\n\ndeclare const trueInTS: boolean;\ndeclare const trueInJS: boolean;\ndeclare const monaco: any;\n\nconst defaultCompilerOptions = {\n noImplicitAny: true,\n strictNullChecks: trueInTS,\n strictFunctionTypes: true,\n strictPropertyInitialization: true,\n strictBindCallApply: true,\n noImplicitThis: true,\n noImplicitReturns: true,\n\n alwaysStrict: true,\n allowUnreachableCode: false,\n allowUnusedLabels: false,\n\n downlevelIteration: false,\n noEmitHelpers: false,\n noLib: false,\n noStrictGenericChecks: false,\n noUnusedLocals: false,\n noUnusedParameters: false,\n\n esModuleInterop: true,\n preserveConstEnums: false,\n removeComments: false,\n skipLibCheck: false,\n\n checkJs: trueInJS,\n allowJs: trueInJS,\n\n experimentalDecorators: false,\n emitDecoratorMetadata: false,\n\n target: monaco.languages.typescript.ScriptTarget.ES2017,\n jsx: monaco.languages.typescript.JsxEmit.None,\n};"},{"path":["Playground","Language"],"title":"Automatic Type Acquisition","name":"Automatic Type Acquisition.ts","lang":"en","id":"automatic-type-acquisition","sortIndex":1,"hash":"2e4f12490785675d6045842e2d78ea411043f0cf19d03cd04c9efdbd9b9b6617d95be0676bcc8c56bf876f3dce44a136dd3bd62d0a082889e191df9d442c5815","compilerSettings":{},"code":"// Automatic Type Acquisition is the term for how TypeScript\n// grabs type definitions from @types on npm behind the scenes\n// to provide a better user experience for JavaScript users.\n\n// The playground now has a similar (but a bit more limited)\n// version of the type acquisition process built into\n// TypeScript.\n\n// You can use it by creating imports in your code. It works\n// either through @types from DefinitelyTyped or via d.ts\n// files inside the dependency itself.\n\nimport { danger } from \"danger\";\n\n// Highlight these identifiers below to see the associated\n// JSDocs from the built-in types:\n\ndanger.github;\n\n// This handles transitive dependencies also, so in this case,\n// danger depends on @octokit/rest also.\n\ndanger.github.api.pulls.createComment();\n\n// Type acquisition will also take Node's built-in modules\n// into account and pull in Node's type declarations\n// when you use any of those dependencies. Note, these\n// tend to take a bit longer than the others since there's\n// quite a lot of types to download!\n\nimport { readFileSync } from \"fs\";\n\nconst inputPath = \"my/path/file.ts\";\nreadFileSync(inputPath, \"utf8\");\n"},{"path":["Playground","Language"],"title":"Fixits","name":"Fixits.ts","lang":"en","id":"fixits","sortIndex":1,"hash":"9879bc5eb6783ba15d54a5a0de39bfd379bc7e2d0f8b2291e60a53779aa851df9b8c061fed7e38308484fb2c9f2fa780c442515af9c041618aff699c77071cd8","compilerSettings":{},"code":"// TypeScript supports a lot of fixits, which are automated\n// helpers to refactor your code. For example if you select\n// the text in line 7, and click on the light bulb which\n// pops up, you'll get offered some fixits.\n\nfunction addOne(x: number) {\n return x + 1;\n}\n\n// This feature is available as of TypeScript version 3.7,\n// which will also include nightly builds.\n\n// They might not be something you need to use inside the\n// playground when you're making code samples, or learning.\n\n// However, having fixits available means we can document\n// them in the playground and that's really valuable:\n\n// example:big-number-literals\n// example:const-to-let\n// example:infer-from-usage-changes"},{"path":["Playground","Tooling"],"title":"Mobile Support","name":"Mobile Support.ts","lang":"en","id":"mobile-support","sortIndex":99,"hash":"3d5a3a04bdb4b926710f90fa98b78ec67d053136d032a2874a173a16e78148c2b3f95d755259778eb3077b98469dcf960e54faf9ccb62dcfe45e4b7fb6352bda","code":"// Tiny devices may not be very good for writing code, but\n// they are good for reading our examples, or giving\n// someone's link a quick review and making some minor\n// edits.\n\n// With the new Playground, we've improved the small screen\n// experience by:\n\n// - Removing menu items which aren't related to code.\n// - Some minor style improvements.\n// - Switch to a single pane editor. This means you\n// can't see the transpiled JS.\n\n// This should make the experience less painful."},{"path":["Playground","Tooling"],"title":"Sharable URLs","name":"Sharable URLs.ts","lang":"en","id":"sharable-urls","sortIndex":1,"hash":"91dfecf1bee3dbca2c0c9aa5c9b1719351cfd3f5d458cd6eecb99280ac04254b7ad12cce7d73fd7a675038d72c93ec9d5acdb8a7b33203cc67d75156c3ac4f75","code":"// With the new Playground, URLs contain a lot more\n// information about your settings. This makes reproductions\n// accurate, and you don't have to tell someone to hit a\n// particular compiler flag to replicate your setup.\n\n// The URL contains:\n//\n// - Whether you are on an example\n// - The gzipped source code for your TypeScript/JavaScript\n// - Backwards compatibility for older non-gzipped URLs\n// - Which language you are working in\n// - The diff of your compiler settings from the defaults\n// example:new-compiler-defaults\n// - Your text selection if you have some code highlighted\n//\n// The URL is updated live when any of the above changes\n// using HTML5's replaceState, so your back button will\n// still work as expected.\n\n// When loading an example, you'll see an occasional e=[number]\n// which is used to ensure the browser correctly reloads when\n// you jump between examples. It's harmless.\n\n// You can see the URL change on this page by editing some\n// of the comments, or start selecting some text. Pressing\n// reload on your browser will take you back to the same\n// state."},{"path":["Playground","Tooling"],"title":"TypeScript Versions","name":"TypeScript Versions.ts","lang":"en","id":"typescript-versions","sortIndex":1,"hash":"8d727776b91ddbf0b6512040b2b1b166d1898277c0c570e306ade9a25221f80ec2bb2af4b92f0296aa0ab769a1da65b54d063980bde73816562d5f41174df1f8","compilerSettings":{},"code":"// With the new Playground, we have a lot more control over\n// the environment in which your code runs. The new Playground\n// is now loosely coupled to both monaco-editor and\n// monaco-typescript which provide the editing experience.\n\n// https://github.com/microsoft/monaco-editor/\n// https://github.com/microsoft/monaco-typescript\n\n// Loosely coupling means the playground supports letting\n// users choose between many different versions of the\n// TypeScript build which monaco-typescript has integrated.\n\n// We have infrastructure to build a copy of both monaco-editor\n// and monaco-typescript for any version of TypeScript. This\n// means we can now support:\n\n// - Beta builds of TypeScript\n// - Nightly builds of TypeScript\n// - Pull Request builds of TypeScript\n// - Older builds of TypeScript\n\n// via https://github.com/orta/make-monaco-builds\n\n// The foundational architecture for how the new playground\n// supports different versions of TypeScript came from the\n// project which this site is a fork of:\n\n// https://github.com/agentcooper/typescript-play\n"},{"path":["TypeScript","Language"],"title":"Soundness","name":"Soundness.ts","lang":"en","id":"soundness","sortIndex":1,"hash":"fee907769244cc301e44755d59b57ae74b05c2201e655ea6272608a9e1a8c2fd08d3699a553d26d2c08442ae82ef71f2d181c7b4279bbb4e84330fbc2cc5f7c5","compilerSettings":{"strictFunctionTypes":false},"code":"// Without a background in type theory, you're unlikely\n// to be familiar with the idea of a type system being \"sound\".\n\n// Soundness is the idea that the compiler can make guarantees\n// about the type a value has at runtime, and not just\n// during compilation. This is normal for most programming\n// languages that are built with types from day one.\n\n// Building a type system which models a language which has\n// existed for a few decades however becomes about making\n// decisions with trade-offs on three qualities: Simplicity,\n// Usability and Soundness.\n\n// With TypeScript's goal of being able to support all JavaScript\n// code, the language tends towards simplicity and usability\n// when presented with ways to add types to JavaScript.\n\n// Let's look at a few cases where TypeScript is provably\n// not sound, to understand what those trade-offs would look\n// like otherwise.\n\n// Type Assertions\n\nconst usersAge = (\"23\" as any) as number;\n\n// TypeScript will let you use type assertions to override\n// the inference to something which is quite wrong. Using\n// type assertions is a way of telling TypeScript you know\n// best, and TypeScript will try to let you get on with it.\n\n// Languages which are sound would occasionally use runtime checks\n// to ensure that the data matches what your types say - but\n// TypeScript aims to have no type-aware runtime impact on\n// your transpiled code.\n\n// Function Parameter Bi-variance\n\n// Params for a function support redefining the parameter\n// to be a subtype of the original declaration.\n\ninterface InputEvent {\n timestamp: number;\n}\ninterface MouseInputEvent extends InputEvent {\n x: number;\n y: number;\n}\ninterface KeyboardInputEvent extends InputEvent {\n keyCode: number;\n}\n\nfunction listenForEvent(eventType: \"keyboard\" | \"mouse\", handler: (event: InputEvent) => void) { }\n\n// You can re-declare the parameter type to be a subtype of\n// the declaration. Above, handler expected a type InputEvent\n// but in the below usage examples - TypeScript accepts\n// a type which has additional properties.\n\nlistenForEvent(\"keyboard\", (event: KeyboardInputEvent) => { });\nlistenForEvent(\"mouse\", (event: MouseInputEvent) => { });\n\n// This can go all the way back to the smallest common type:\n\nlistenForEvent(\"mouse\", (event: {}) => { });\n\n// But no further:\n\nlistenForEvent(\"mouse\", (event: string) => { });\n\n// This covers the real-world pattern of event listener\n// in JavaScript, at the expense of having being sound.\n\n// TypeScript can raise an error when this happens via\n// `strictFunctionTypes`. Or, you could work around this\n// particular case with function overloads,\n// see: example:typing-functions\n\n// Void special casing\n\n// Parameter Discarding\n\n// To learn about special cases with function parameters\n// see example:structural-typing\n\n// Rest Parameters\n\n// Rest parameters are assumed to all be optional, this means\n// TypeScript will not have a way to enforce the number of\n// parameters available to a callback.\n\nfunction getRandomNumbers(count: number, callback: (...args: number[]) => void) { }\n\ngetRandomNumbers(2, (first, second) => console.log([first, second]));\ngetRandomNumbers(400, (first) => console.log(first));\n\n// Void Functions Can Match to a Function With a Return Value\n\n// A function which returns a void function, can accept a\n// function which takes any other type.\n\nconst getPI = () => 3.14;\n\nfunction runFunction(func: () => void) {\n func();\n}\n\nrunFunction(getPI);\n\n// For more information on the places where soundness of the\n// type system is compromised, see:\n\n// https://github.com/Microsoft/TypeScript/wiki/FAQ#type-system-behavior\n// https://github.com/Microsoft/TypeScript/issues/9825\n// https://www.typescriptlang.org/docs/handbook/type-compatibility.html"},{"path":["TypeScript","Language"],"title":"Structural Typing","name":"Structural Typing.ts","lang":"en","id":"structural-typing","sortIndex":1,"hash":"af844504fc9e339cb2013df2932ea3a4d8666242fbffdd947dfef5bab7abb1e904197c09db2906aa257e29a4bc93115c78f90c41099c2e433e1377e8de433cd4","compilerSettings":{},"code":"// TypeScript is a Structural Type System. A structural type\n// system means that when comparing types, TypeScript only\n// takes into account the members on the type.\n\n// This is in contrast to nominal type systems, where you\n// could create two types but could not assign them to each\n// other. See example:nominal-typing\n\n// For example, these two interfaces are completely\n// transferrable in a structural type system:\n\ninterface Ball {\n diameter: number;\n}\ninterface Sphere {\n diameter: number;\n}\n\nlet ball: Ball = { diameter: 10 };\nlet sphere: Sphere = { diameter: 20 };\n\nsphere = ball;\nball = sphere;\n\n// If we add in a type which structurally contains all of\n// the members of Ball and Sphere, then it also can be\n// set to be a ball or sphere.\n\ninterface Tube {\n diameter: number;\n length: number;\n}\n\nlet tube: Tube = { diameter: 12, length: 3 };\n\ntube = ball;\nball = tube;\n\n// Because a ball does not have a length, then it cannot be\n// assigned to the tube variable. However, all of the members\n// of Ball are inside tube, and so it can be assigned.\n\n// TypeScript is comparing each member in the type against\n// each other to verify their equality.\n\n// A function is an object in JavaScript and it is compared\n// in a similar fashion. With one useful extra trick around\n// the params:\n\nlet createBall = (diameter: number) => ({ diameter });\nlet createSphere = (diameter: number, useInches: boolean) => {\n return { diameter: useInches ? diameter * 0.39 : diameter };\n};\n\ncreateSphere = createBall;\ncreateBall = createSphere;\n\n// TypeScript will allow (number) to equal (number, boolean)\n// in the parameters, but not (number, boolean) -> (number)\n\n// TypeScript will discard the boolean in the first assignment\n// because it's very common for JavaScript code to skip passing\n// params when they're not needed.\n\n// For example the array's forEach's callback has three params,\n// value, index and the full array - if TypeScript didn't\n// support discarding parameters, then you would have to\n// include every option to make the functions match up:\n\n[createBall(1), createBall(2)].forEach((ball, _index, _balls) => {\n console.log(ball);\n});\n\n// No one needs that.\n\n// Return types are treated like objects, and any differences\n// are compared with the same object equality rules above.\n\nlet createRedBall = (diameter: number) => ({ diameter, color: \"red\" });\n\ncreateBall = createRedBall;\ncreateRedBall = createBall;\n\n// Where the first assignment works (they both have diameter)\n// but the second doesn't (the ball doesn't have a color).\n"},{"path":["TypeScript","Language"],"title":"Type Guards","name":"Type Guards.ts","lang":"en","id":"type-guards","sortIndex":1,"hash":"f44e3a6c27465307f37fb5a6bfb72bf68df85f7719e2946e044cdaecb19a901abb64fd3b3393fcb94e77547934957f884ca65fd8c8255275b3ad932be87c53ff","compilerSettings":{},"code":"// Type Guarding is the term where you influence the code\n// flow analysis via code. TypeScript uses existing JavaScript\n// behavior which validates your objects at runtime to influence\n// the code flow. This example assumes you've read example:code-flow\n\n// To run through these examples, we'll create some classes,\n// here's a system for handling internet or telephone orders.\n\ninterface Order {\n address: string;\n}\ninterface TelephoneOrder extends Order {\n callerNumber: string;\n}\ninterface InternetOrder extends Order {\n email: string;\n}\n\n// Then a type which could be one of the two Order subtypes or undefined\ntype PossibleOrders = TelephoneOrder | InternetOrder | undefined;\n\n// And a function which returns a PossibleOrder\ndeclare function getOrder(): PossibleOrders;\nconst possibleOrder = getOrder();\n\n// We can use the \"in\" operator to check whether a particular\n// key is on the object to narrow the union. (\"in\" is a JavaScript\n// operator for testing object keys.)\n\nif (\"email\" in possibleOrder) {\n const mustBeInternetOrder = possibleOrder;\n}\n\n// You can use the JavaScript \"instanceof\" operator if you\n// have a class which conforms to the interface:\n\nclass TelephoneOrderClass {\n address: string;\n callerNumber: string;\n}\n\nif (possibleOrder instanceof TelephoneOrderClass) {\n const mustBeTelephoneOrder = possibleOrder;\n}\n\n// You can use the JavaScript \"typeof\" operator to\n// narrow your union. This only works with primitives\n// inside JavaScript (like strings, objects, numbers).\n\nif (typeof possibleOrder === \"undefined\") {\n const definitelyNotAnOder = possibleOrder;\n}\n\n// You can see a full list of possible typeof values\n// here: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/typeof\n\n// Using JavaScript operators can only get you so far. When\n// you want to check your own object types you can use\n// type predicate functions.\n\n// A type predicate function is a function where the return\n// type offers information to the code flow analysis when\n// the function returns true.\n\n// Using the possible order, we can use two type guards\n// to declare which type the possibleOrder is:\n\nfunction isAnInternetOrder(order: PossibleOrders): order is InternetOrder {\n return order && \"email\" in order;\n}\n\nfunction isATelephoneOrder(order: PossibleOrders): order is TelephoneOrder {\n return order && \"callerNumber\" in order;\n}\n\n// Now we can use these functions in if statements to narrow\n// down the type which possibleOrder is inside the if:\n\nif (isAnInternetOrder(possibleOrder)) {\n console.log(\"Order received via email:\", possibleOrder.email);\n}\n\nif (isATelephoneOrder(possibleOrder)) {\n console.log(\"Order received via phone:\", possibleOrder.callerNumber);\n}\n\n// You can read more on code flow analysis here:\n//\n// - example:code-flow\n// - example:type-guards\n// - example:discriminate-types\n"},{"path":["TypeScript","Language"],"title":"Type Widening and Narrowing","name":"Type Widening and Narrowing.ts","lang":"en","id":"type-widening-and-narrowing","sortIndex":1,"hash":"c35100b02c36ce67640c9003d01c986641da69ad7ea5624a2f7fa80c74198df3a3b2baeee58043fba131c016a0ff1cd6b986fb272262040892bb5c6ab879ecb7","compilerSettings":{},"code":"// It might be easiest to start of the discussion of\n// widening and narrowing with an example:\n\nconst welcomeString = \"Hello There\";\nlet replyString = \"Hey\";\n\n// Aside from the text differences of the strings, welcomeString\n// is a const (which means the value will never change)\n// and replyString is a let (which means it can change).\n\n// If you hover over both variables, you get very different\n// type information from TypeScript:\n//\n// const welcomeString: \"Hello There\"\n//\n// let replyString: string\n\n// TypeScript has inferred the type of welcomeString to be\n// the literal string \"Hello There\", whereas replyString\n// is general string.\n\n// This is because a let needs to have a wider type, you\n// could set replyString to be any other string - which means\n// it has a wider set of possibilities.\n\nreplyString = \"Hi :wave:\";\n\n// If replyString had the string literal type \"Hey\" - then\n// you could never change the value because it could only\n// change to \"Hey\" again.\n\n// Widening and Narrowing types is about expanding and reducing\n// the possibilities which a type could represent.\n\n// An example of type narrowing is working with unions, the\n// example on code flow analysis is almost entirely based on\n// narrowing: example:code-flow\n\n// Type narrowing is what powers the strict mode of TypeScript\n// via the nullability checks. With strict mode turned off,\n// markers for nullability like undefined and null are ignored\n// in a union.\n\ndeclare const quantumString: string | undefined;\n// This will fail in strict mode only\nquantumString.length;\n\n// In strict mode the onus is on the code author to ensure\n// that the type has been narrowed to the non-null type.\n// Usually this is as simple as an if check:\n\nif (quantumString) {\n quantumString.length;\n}\n\n// In strict mode the type quantumString has two representations.\n// Inside the if, the type was narrowed to just string.\n\n// You can see more examples of narrowing in:\n//\n// example:union-and-intersection-types\n// example:discriminate-types\n\n// And even more resources on the web:\n//\n// https://mariusschulz.com/blog/literal-type-widening-in-typescript\n// https://sandersn.github.io/manual/Widening-and-Narrowing-in-Typescript.html\n"},{"path":["TypeScript","Language Extensions"],"title":"Enums","name":"Enums.ts","lang":"en","id":"enums","sortIndex":1,"hash":"198c95f3b132ab1e43f1fcb31b8557713dd1b368295db00adcf06fd21bf7efb660fcd6484dd4b0756a8c531f031dec38aea22d38bf5a36d4ba3be1b712c0e045","compilerSettings":{},"code":"// Enums are a feature added to JavaScript in TypeScript\n// which makes it easier to handle named sets of constants.\n\n// By default an enum is number based, starting at zero,\n// and each option is assigned an increment by one. This is\n// useful when the value is not important.\n\nenum CompassDirection {\n North,\n East,\n South,\n West,\n}\n\n// By annotating an enum option, you set the value;\n// increments continue from that value:\n\nenum StatusCodes {\n OK = 200,\n BadRequest = 400,\n Unauthorized,\n PaymentRequired,\n Forbidden,\n NotFound,\n}\n\n// You reference an enum by using EnumName.Value\n\nconst startingDirection = CompassDirection.East;\nconst currentStatus = StatusCodes.OK;\n\n// Enums support accessing data in both directions from key\n// to value, and value to key.\n\nconst okNumber = StatusCodes.OK;\nconst okNumberIndex = StatusCodes[\"OK\"];\nconst stringBadRequest = StatusCodes[400];\n\n// Enums can be different types, a string type is common.\n// Using a string can make it easier to debug, because the\n// value at runtime does not require you to look up the number.\n\nenum GamePadInput {\n Up = \"UP\",\n Down = \"DOWN\",\n Left = \"LEFT\",\n Right = \"RIGHT\",\n}\n\n// If you want to reduce the number of objects in your\n// JavaScript runtime, you can create a const enum.\n\n// A const enum's value is replaced by TypeScript during\n// transpilation of your code, instead of being looked up\n// via an object at runtime.\n\nconst enum MouseAction {\n MouseDown,\n MouseUpOutside,\n MouseUpInside,\n}\n\nconst handleMouseAction = (action: MouseAction) => {\n switch (action) {\n case MouseAction.MouseDown:\n console.log(\"Mouse Down\");\n break;\n }\n};\n\n// If you look at the transpiled JavaScript, you can see\n// how the other enums exist as objects and functions,\n// however MouseAction is not there.\n\n// This is also true for the check against MouseAction.MouseDown\n// inside the switch statement inside handleMouseAction.\n\n// Enums can do more than this, you can read more in the\n// TypeScript handbook:\n//\n// https://www.typescriptlang.org/docs/handbook/enums.html\n"},{"path":["TypeScript","Language Extensions"],"title":"Nominal Typing","name":"Nominal Typing.ts","lang":"en","id":"nominal-typing","sortIndex":1,"hash":"06b50356b6940efd83a2e7678591f78f96c4ca2425c42ab70d879bc4b481842f55e122a0616a6f09cebd20a4a36714a1927f05645b72d07aca6f601e57f2bf32","compilerSettings":{},"code":"// A nominal type system means that each type is unique\n// and even if types have the same data you cannot assign\n// across types.\n\n// TypeScript's type system is structural, which means\n// if the type is shaped like a duck, it's a duck. If a\n// goose has all the same attributes as a duck, then it also\n// is a duck. You can learn more here: example:structural-typing\n\n// This can have drawbacks, for example there are cases\n// where a string or number can have special context and you\n// don't want to ever make the values transferrable. For\n// example:\n//\n// - User Input Strings (unsafe)\n// - Translation Strings\n// - User Identification Numbers\n// - Access Tokens\n\n// We can get most of the value from a nominal type\n// system with a little bit of extra code.\n\n// We're going to use an intersectional type, with a unique\n// constraint in the form of a property called __brand (this\n// is convention) which makes it impossible to assign a\n// normal string to a ValidatedInputString.\n\ntype ValidatedInputString = string & { __brand: \"User Input Post Validation\" };\n\n// We will use a function to transform a string to\n// a ValidatedInputString - but the point worth noting\n// is that we're just _telling_ TypeScript that it's true.\n\nconst validateUserInput = (input: string) => {\n const simpleValidatedInput = input.replace(/\\ {\n console.log(name);\n};\n\n// For example, here's some unsafe input from a user, going\n// through the validator and then being allowed to be printed:\n\nconst input = \"alert('bobby tables')\";\nconst validatedInput = validateUserInput(input);\nprintName(validatedInput);\n\n// On the other hand, passing the un-validated string to\n// printName will raise a compiler error:\n\nprintName(input);\n\n// You can read a comprehensive overview of the\n// different ways to create nominal types, and their\n// trade-offs in this 400 comment long GitHub issue:\n//\n// https://github.com/Microsoft/TypeScript/issues/202\n//\n// and this post is a great summary:\n//\n// https://michalzalecki.com/nominal-typing-in-typescript/\n"},{"path":["TypeScript","Language Extensions"],"title":"Types vs Interfaces","name":"Types vs Interfaces.ts","lang":"en","id":"types-vs-interfaces","sortIndex":1,"hash":"67a617d7bfd910ff306d2be016f8de26afe975fc7df64fdc2528b19a6c2c7ebfd5dd9727fef7da92ca8d67e96d730af4d01f9d2f5b633fc8b224d4cb641f8b76","compilerSettings":{},"code":"// There are two main tools to declare the shape of an\n// object: interfaces and type aliases.\n//\n// They are very similar, and for the most common cases\n// act the same.\n\ntype BirdType = {\n wings: 2;\n};\n\ninterface BirdInterface {\n wings: 2;\n}\n\nconst bird1: BirdType = { wings: 2 };\nconst bird2: BirdInterface = { wings: 2 };\n\n// Because TypeScript is a structural type system,\n// it's possible to intermix their use too.\n\nconst bird3: BirdInterface = bird1;\n\n// They both support extending other interfaces and types.\n// Type aliases do this via intersection types, while\n// interfaces have a keyword.\n\ntype Owl = { nocturnal: true } & BirdType;\ntype Robin = { nocturnal: false } & BirdInterface;\n\ninterface Peacock extends BirdType {\n colourful: true;\n flies: false;\n}\ninterface Chicken extends BirdInterface {\n colourful: false;\n flies: false;\n}\n\nlet owl: Owl = { wings: 2, nocturnal: true };\nlet chicken: Chicken = { wings: 2, colourful: false, flies: false };\n\n// That said, we recommend you use interfaces over type\n// aliases. Specifically, because you will get better error\n// messages. If you hover over the following errors, you can\n// see how TypeScript can provide terser and more focused\n// messages when working with interfaces like Chicken.\n\nowl = chicken;\nchicken = owl;\n\n// One major difference between type aliases vs interfaces\n// are that interfaces are open and type aliases are closed.\n// This means you can extend an interface by declaring it\n// a second time.\n\ninterface Kitten {\n purrs: boolean;\n}\n\ninterface Kitten {\n colour: string;\n}\n\n// In the other case a type cannot be changed outside of\n// its declaration.\n\ntype Puppy = {\n color: string;\n};\n\ntype Puppy = {\n toys: number;\n};\n\n// Depending on your goals, this difference could be a\n// positive or a negative. However for publicly exposed\n// types, it's a better call to make them an interface.\n\n// One of the best resources for seeing all of the edge\n// cases around types vs interfaces, this stack overflow\n// thread is a good place to start:\n\n// https://stackoverflow.com/questions/37233735/typescript-interfaces-vs-types/52682220#52682220\n"},{"path":["TypeScript","Meta-Types"],"title":"Conditional Types","name":"Conditional Types.ts","lang":"en","id":"conditional-types","sortIndex":1,"hash":"be08b5b6b2850f4e9d1d3f0d4aeaff4ad8a3ba07f236cbda6871d92d9c12dbbedab3a7d1a625266ce888486eb7c3a33e52b8f71c3df2b1f6475215bc71f5d62e","compilerSettings":{},"code":"// Conditional Types provide a way to do simple logic in the\n// TypeScript type system. This is definitely an advanced\n// feature, and it's quite feasible that you won't need to\n// use this in your normal day to day code.\n\n// A conditional type looks like:\n//\n// A extends B ? C : D\n//\n// Where the condition is whether a type extends an\n// expression, and if so what type should be returned.\n\n// Let's go through some examples, for brevity we're\n// going to use single letters for generics. This is optional\n// but restricting ourselves to 60 characters makes it\n// hard to fit on screen.\n\ntype Cat = { meows: true };\ntype Dog = { barks: true };\ntype Cheetah = { meows: true; fast: true };\ntype Wolf = { barks: true; howls: true };\n\n// We can create a conditional type which lets extract\n// types which only conform to something which barks.\n\ntype ExtractDogish = A extends { barks: true } ? A : never;\n\n// Then we can create types which ExtractDogish wraps:\n\n// A cat doesn't bark, so it will return never\ntype NeverCat = ExtractDogish;\n// A wolf will bark, so it returns the wolf shape\ntype Wolfish = ExtractDogish;\n\n// This becomes useful when you want to work with a\n// union of many types and reduce the number of potential\n// options in a union:\n\ntype Animals = Cat | Dog | Cheetah | Wolf;\n\n// When you apply ExtractDogish to a union type, it is the\n// same as running the conditional against each member of\n// the type:\n\ntype Dogish = ExtractDogish;\n\n// = ExtractDogish | ExtractDogish |\n// ExtractDogish | ExtractDogish\n//\n// = never | Dog | never | Wolf\n//\n// = Dog | Wolf (see example:unknown-and-never)\n\n// This is called a distributive conditional type because\n// the type distributes over each member of the union.\n\n// Deferred Conditional Types\n\n// Conditional types can be used to tighten your APIs which\n// can return different types depending on the inputs.\n\n// For example this function which could return either a\n// string or number depending on the boolean passed in.\n\ndeclare function getID(fancy: T): T extends true ? string : number;\n\n// Then depending on how much the type-system knows about\n// the boolean, you will get different return types:\n\nlet stringReturnValue = getID(true);\nlet numberReturnValue = getID(false);\nlet stringOrNumber = getID(Math.random() < 0.5);\n\n// In this case above TypeScript can know the return value\n// instantly. However, you can use conditional types in functions\n// where the type isn't known yet. This is called a deferred\n// conditional type.\n\n// Same as our Dogish above, but as a function instead\ndeclare function isCatish(x: T): T extends { meows: true } ? T : undefined;\n\n// There is an extra useful tool within conditional types, which\n// is being able to specifically tell TypeScript that it should\n// infer the type when deferring. That is the 'infer' keyword.\n\n// infer is typically used to create meta-types which inspect\n// the existing types in your code, think of it as creating\n// a new variable inside the type.\n\ntype GetReturnValue = T extends (...args: any[]) => infer R ? R : T;\n\n// Roughly:\n//\n// - this is a conditional generic type called GetReturnValue\n// which takes a type in its first parameter\n//\n// - the conditional checks if the type is a function, and\n// if so create a new type called R based on the return\n// value for that function\n//\n// - If the check passes, the type value is the inferred\n// return value, otherwise it is the original type\n//\n\ntype getIDReturn = GetReturnValue;\n\n// This fails the check for being a function, and would\n// just return the type passed into it.\ntype getCat = GetReturnValue;\n"},{"path":["TypeScript","Meta-Types"],"title":"Discriminate Types","name":"Discriminate Types.ts","lang":"en","id":"discriminate-types","sortIndex":1,"hash":"acbffc6ea37dcc78d38df58e93ab1722a6f842715a9bc402150947ca07460b456bdb1b2b4a53183ba701466f95828be6e31519a0d9b82fbef5b7879ba45e7caf","compilerSettings":{},"code":"// A discriminated type union is where you use code flow\n// analysis to reduce a set of potential objects down to one\n// specific object.\n//\n// This pattern works really well for sets of similar\n// objects with a different string or number constant\n// for example: a list of named events, or versioned\n// sets of objects.\n\ntype TimingEvent = { name: \"start\"; userStarted: boolean } | { name: \"closed\"; duration: number };\n\n// When event comes into this function, it could be any\n// of the two potential types.\n\nconst handleEvent = (event: TimingEvent) => {\n // By using a switch against event.name TypeScript's code\n // flow analysis can determine that an object can only\n // be represented by one type in the union.\n\n switch (event.name) {\n case \"start\":\n // This means you can safely access userStarted\n // because it's the only type inside TimingEvent\n // where name is \"start\"\n const initiatedByUser = event.userStarted;\n break;\n\n case \"closed\":\n const timespan = event.duration;\n break;\n }\n};\n\n// This pattern is the same with numbers which we can use\n// as the discriminator.\n\n// In this example, we have a discriminate union and an\n// additional error state to handle.\n\ntype APIResponses = { version: 0; msg: string } | { version: 1; message: string; status: number } | { error: string };\n\nconst handleResponse = (response: APIResponses) => {\n // Handle the error case, and then return\n if (\"error\" in response) {\n console.error(response.error);\n return;\n }\n\n // TypeScript now knows that APIResponse cannot be\n // the error type. If it were the error, the function\n // would have returned. You can verify this by\n // hovering over response below.\n\n if (response.version === 0) {\n console.log(response.msg);\n } else if (response.version === 1) {\n console.log(response.status, response.message);\n }\n};\n\n// You're better off using a switch statement instead of\n// if statements because you can make assurances that all\n// parts of the union are checked. There is a good pattern\n// for this using the never type in the handbook:\n\n// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-never-type\n"},{"path":["TypeScript","Meta-Types"],"title":"Indexed Types","name":"Indexed Types.ts","lang":"en","id":"indexed-types","sortIndex":1,"hash":"dca5f127c41a3045f1e8278d00dbe4ab7d704f47a304ea817c5caa4866e74db2e1d2b0537aeaed1994901fbd39927d076dd4f989c30ea44895c93f7058ccd8cb","compilerSettings":{},"code":"// There are times when you find yourself duplicating types.\n// A common example is nested resources in an auto-generated\n// API response.\n\ninterface ArtworkSearchResponse {\n artists: {\n name: string;\n artworks: {\n name: string;\n deathdate: string | null;\n bio: string;\n }[];\n }[];\n}\n\n// If this interface were hand-crafted, it's pretty easy to\n// imagine pulling out the artworks into an interface like:\n\ninterface Artwork {\n name: string;\n deathdate: string | null;\n bio: string;\n}\n\n// However, in this case we don't control the API, and if\n// we hand-created the interface then it's possible that\n// the artworks part of ArtworkSearchResponse and\n// Artwork could get out of sync when the response changes.\n\n// The fix for this is indexed types, which replicate how\n// JavaScript allows accessing properties via strings.\n\ntype InferredArtwork = ArtworkSearchResponse[\"artists\"][0][\"artworks\"][0];\n\n// The InferredArtwork is generated by looking through the\n// type's properties and giving a new name to the subset which\n// you have indexed.\n"},{"path":["TypeScript","Meta-Types"],"title":"Mapped Types","name":"Mapped Types.ts","lang":"en","id":"mapped-types","sortIndex":1,"hash":"204215a787a9067adc4662dbf403830016f42bfbd12e27d9059b6537fbe5bff0ecbc298f97ebabfea50d6cb9fbb7af768bdebaaaae6419a57aaeeb8e1c9faf2e","compilerSettings":{},"code":"// Mapped types are a way to create new types based\n// on another type. Effectively a transformational type.\n\n// Common cases for using a mapped type is dealing with\n// partial subsets of an existing type. For example\n// an API may return an Artist:\n\ninterface Artist {\n id: number;\n name: string;\n bio: string;\n}\n\n// However, if you were to send an update to the API which\n// only changes a subset of the Artist then you would\n// typically have to create an additional type:\n\ninterface ArtistForEdit {\n id: number;\n name?: string;\n bio?: string;\n}\n\n// It's very likely that this would get out of sync with\n// the Artist above. Mapped types let you create a change\n// in an existing type.\n\ntype MyPartialType = {\n // For every existing property inside the type of Type\n // convert it to be a ?: version\n [Property in keyof Type]?: Type[Property];\n};\n\n// Now we can use the mapped type instead to create\n// our edit interface:\ntype MappedArtistForEdit = MyPartialType;\n\n// This is close to perfect, but it does allow id to be null\n// which should never happen. So, let's make one quick\n// improvement by using an intersection type (see:\n// example:union-and-intersection-types )\n\ntype MyPartialTypeForEdit = {\n [Property in keyof Type]?: Type[Property];\n} & { id: number };\n\n// This takes the partial result of the mapped type, and\n// merges it with an object which has id: number set.\n// Effectively forcing id to be in the type.\n\ntype CorrectMappedArtistForEdit = MyPartialTypeForEdit;\n\n// This is a pretty simple example of how mapped types\n// work, but covers most of the basics. If you'd like to\n// dive in with more depth, check out the handbook:\n//\n// https://www.typescriptlang.org/docs/handbook/2/mapped-types.html\n"},{"path":["TypeScript","Primitives"],"title":"Any","name":"Any.ts","lang":"en","id":"any","sortIndex":1,"hash":"9bae7af248b22e281d3cf8620ecc1cf697ea69ecfbffe2fc55d17e257df70322f169ba1541d75d1d4b2710995e36345d665f53d455c95f2d5151d56066182236","compilerSettings":{},"code":"// Any is the TypeScript escape clause. You can use any to\n// either declare a section of your code to be dynamic and\n// JavaScript like, or to work around limitations in the\n// type system.\n\n// A good case for any is JSON parsing:\n\nconst myObject = JSON.parse(\"{}\");\n\n// Any declares to TypeScript to trust your code as being\n// safe because you know more about it. Even if that is\n// not strictly true. For example, this code would crash:\n\nmyObject.x.y.z;\n\n// Using an any gives you the ability to write code closer to\n// original JavaScript with the trade-off of type safety.\n\n// any is much like a 'type wildcard' which you can replace\n// with any type (except never) to make one type assignable\n// to the other.\n\ndeclare function debug(value: any): void;\n\ndebug(\"a string\");\ndebug(23);\ndebug({ color: \"blue\" });\n\n// Each call to debug is allowed because you could replace the\n// any with the type of the argument to match.\n\n// TypeScript will take into account the position of the\n// anys in different forms, for example with these tuples\n// for the function argument.\n\ndeclare function swap(x: [number, string]): [string, number];\n\ndeclare const pair: [any, any];\nswap(pair);\n\n// The call to swap is allowed because the argument can be\n// matched by replacing the first any in pair with number\n// and the second `any` with string.\n\n// If tuples are new to you, see: example:tuples\n\n// Unknown is a sibling type to any, if any is about saying\n// \"I know what's best\", then unknown is a way to say \"I'm\n// not sure what is best, so you need to tell TS the type\"\n// example:unknown-and-never\n"},{"path":["TypeScript","Primitives"],"title":"Literals","name":"Literals.ts","lang":"en","id":"literals","sortIndex":1,"hash":"336cc1566f6927d81e138294c0ed2d8967310cfbbcb5260c8b7af2941045998b43994131d856f2a57aa7fea43c9308be04e0b92b4f0f5413a45004fda89bb6ee","compilerSettings":{},"code":"// TypeScript has some fun special cases for literals in\n// source code.\n\n// In part, a lot of the support is covered in type widening\n// and narrowing ( example:type-widening-and-narrowing ) and it's\n// worth covering that first.\n\n// A literal is a more concrete subtype of a collective type.\n// What this means is that \"Hello World\" is a string, but a\n// string is not \"Hello World\" inside the type system.\n\nconst helloWorld = \"Hello World\";\nlet hiWorld = \"Hi World\"; // this is a string because it is let\n\n// This function takes all strings\ndeclare function allowsAnyString(arg: string);\nallowsAnyString(helloWorld);\nallowsAnyString(hiWorld);\n\n// This function only accepts the string literal \"Hello World\"\ndeclare function allowsOnlyHello(arg: \"Hello World\");\nallowsOnlyHello(helloWorld);\nallowsOnlyHello(hiWorld);\n\n// This lets you declare APIs which use unions to say it\n// only accepts a particular literal:\n\ndeclare function allowsFirstFiveNumbers(arg: 1 | 2 | 3 | 4 | 5);\nallowsFirstFiveNumbers(1);\nallowsFirstFiveNumbers(10);\n\nlet potentiallyAnyNumber = 3;\nallowsFirstFiveNumbers(potentiallyAnyNumber);\n\n// At first glance, this rule isn't applied to complex objects.\n\nconst myUser = {\n name: \"Sabrina\",\n};\n\n// See how it transforms `name: \"Sabrina\"` to `name: string`\n// even though it is defined as a constant. This is because\n// the name can still change any time:\n\nmyUser.name = \"Cynthia\";\n\n// Because myUser's name property can change, TypeScript\n// cannot use the literal version in the type system. There\n// is a feature which will allow you to do this however.\n\nconst myUnchangingUser = {\n name: \"Fatma\",\n} as const;\n\n// When \"as const\" is applied to the object, then it becomes\n// a object literal which doesn't change instead of a\n// mutable object which can.\n\nmyUnchangingUser.name = \"Raîssa\";\n\n// \"as const\" is a great tool for fixtured data, and places\n// where you treat code as literals inline. \"as const\" also\n// works with arrays:\n\nconst exampleUsers = [{ name: \"Brian\" }, { name: \"Fahrooq\" }] as const;\n"},{"path":["TypeScript","Primitives"],"title":"Union and Intersection Types","name":"Union and Intersection Types.ts","lang":"en","id":"union-and-intersection-types","sortIndex":1,"hash":"6d0fedaf3680b2f2fe73328aadb2cf7ab30b31d0aa6634b0e111b561be18427e94574c937f67f62364b5c95b4abcf2ad84b64c5b0a3848b453cf6c310d30f177","compilerSettings":{},"code":"// Type unions are a way of declaring that an object\n// could be more than one type.\n\ntype StringOrNumber = string | number;\ntype ProcessStates = \"open\" | \"closed\";\ntype OddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;\ntype AMessyUnion = \"hello\" | 156 | { error: true };\n\n// If the use of \"open\" and \"closed\" vs string is\n// new to you, check out: example:literals\n\n// We can mix different types into a union, and\n// what we're saying is that the value is one of those types.\n\n// TypeScript will then leave you to figure out how to\n// determine which value it could be at runtime.\n\n// Unions can sometimes be undermined by type widening,\n// for example:\n\ntype WindowStates = \"open\" | \"closed\" | \"minimized\" | string;\n\n// If you hover above, you can see that WindowStates\n// becomes a string - not the union. This is covered in\n// example:type-widening-and-narrowing\n\n// If a union is an OR, then an intersection is an AND.\n// Intersection types are when two types intersect to create\n// a new type. This allows for type composition.\n\ninterface ErrorHandling {\n success: boolean;\n error?: { message: string };\n}\n\ninterface ArtworksData {\n artworks: { title: string }[];\n}\n\ninterface ArtistsData {\n artists: { name: string }[];\n}\n\n// These interfaces can be composed in responses which have\n// both consistent error handling, and their own data.\n\ntype ArtworksResponse = ArtworksData & ErrorHandling;\ntype ArtistsResponse = ArtistsData & ErrorHandling;\n\n// For example:\n\nconst handleArtistsResponse = (response: ArtistsResponse) => {\n if (response.error) {\n console.error(response.error.message);\n return;\n }\n\n console.log(response.artists);\n};\n\n// A mix of Intersection and Union types becomes really\n// useful when you have cases where an object has to\n// include one of two values:\n\ninterface CreateArtistBioBase {\n artistID: string;\n thirdParty?: boolean;\n}\n\ntype CreateArtistBioRequest = CreateArtistBioBase & ({ html: string } | { markdown: string });\n\n// Now you can only create a request when you include\n// artistID and either html or markdown\n\nconst workingRequest: CreateArtistBioRequest = {\n artistID: \"banksy\",\n markdown: \"Banksy is an anonymous England-based graffiti artist...\",\n};\n\nconst badRequest: CreateArtistBioRequest = {\n artistID: \"banksy\",\n};\n"},{"path":["TypeScript","Primitives"],"title":"Unknown and Never","name":"Unknown and Never.ts","lang":"en","id":"unknown-and-never","sortIndex":1,"hash":"4ef3055f695be7dbafd12bc863026f5e8ad2dbeaa43c28aa74f5c21e8e6e3f1029783eed4326efa0a5282d1bd0957b8f89f219a1aaa662a7bee226ed33323f82","compilerSettings":{},"code":"// Unknown\n\n// Unknown is one of those types that once it clicks, you\n// can find quite a lot of uses for it. It acts like a sibling\n// to the any type. Where any allows for ambiguity - unknown\n// requires specifics.\n\n// A good example would be in wrapping a JSON parser. JSON\n// data can come in many different forms and the creator\n// of the json parsing function won't know the shape of the\n// data - the person calling that function should.\n\nconst jsonParser = (jsonString: string) => JSON.parse(jsonString);\n\nconst myAccount = jsonParser(`{ \"name\": \"Dorothea\" }`);\n\nmyAccount.name;\nmyAccount.email;\n\n// If you hover on jsonParser, you can see that it has the\n// return type of any, so then does myAccount. It's possible\n// to fix this with generics - but it's also possible to fix\n// this with unknown.\n\nconst jsonParserUnknown = (jsonString: string): unknown => JSON.parse(jsonString);\n\nconst myOtherAccount = jsonParserUnknown(`{ \"name\": \"Samuel\" }`);\n\nmyOtherAccount.name;\n\n// The object myOtherAccount cannot be used until the type has\n// been declared to TypeScript. This can be used to ensure\n// that API consumers think about their typing up-front:\n\ntype User = { name: string };\nconst myUserAccount = jsonParserUnknown(`{ \"name\": \"Samuel\" }`) as User;\nmyUserAccount.name;\n\n// Unknown is a great tool, to understand it more read these:\n// https://mariusschulz.com/blog/the-unknown-type-in-typescript\n// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type\n\n// Never\n\n// Because TypeScript supports code flow analysis, the language\n// needs to be able to represent when code logically cannot\n// happen. For example, this function cannot return:\n\nconst neverReturns = () => {\n // If it throws on the first line\n throw new Error(\"Always throws, never returns\");\n};\n\n// If you hover on the type, you see it is a () => never\n// which means it should never happen. These can still be\n// passed around like other values:\n\nconst myValue = neverReturns();\n\n// Having a function never return can be useful when dealing\n// with the unpredictability of the JavaScript runtime and\n// API consumers that might not be using types:\n\nconst validateUser = (user: User) => {\n if (user) {\n return user.name !== \"NaN\";\n }\n\n // According to the type system, this code path can never\n // happen, which matches the return type of neverReturns.\n\n return neverReturns();\n};\n\n// The type definitions state that a user has to be passed in\n// but there are enough escape valves in JavaScript whereby\n// you can't guarantee that.\n\n// Using a function which returns never allows you to add\n// additional code in places which should not be possible.\n// This is useful for presenting better error messages,\n// or closing resources like files or loops.\n\n// A very popular use for never, is to ensure that a\n// switch is exhaustive. E.g., that every path is covered.\n\n// Here's an enum and an exhaustive switch, try adding\n// a new option to the enum (maybe Tulip?)\n\nenum Flower {\n Rose,\n Rhododendron,\n Violet,\n Daisy,\n}\n\nconst flowerLatinName = (flower: Flower) => {\n switch (flower) {\n case Flower.Rose:\n return \"Rosa rubiginosa\";\n case Flower.Rhododendron:\n return \"Rhododendron ferrugineum\";\n case Flower.Violet:\n return \"Viola reichenbachiana\";\n case Flower.Daisy:\n return \"Bellis perennis\";\n\n default:\n const _exhaustiveCheck: never = flower;\n return _exhaustiveCheck;\n }\n};\n\n// You will get a compiler error saying that your new\n// flower type cannot be converted into never.\n\n// Never in Unions\n\n// A never is something which is automatically removed from\n// a type union.\n\ntype NeverIsRemoved = string | never | number;\n\n// If you look at the type for NeverIsRemoved, you see that\n// it is string | number. This is because it should never\n// happen at runtime because you cannot assign to it.\n\n// This feature is used a lot in example:conditional-types\n"},{"path":["TypeScript","Type Primitives"],"title":"Built-in Utility Types","name":"Built-in Utility Types.ts","lang":"en","id":"built-in-utility-types","sortIndex":3,"hash":"995377dc2a2decfa7ddddbb36a443c82ee1dd4490464c0ab0f7dc29f091b52543734a5d53f7e8e976f834121a9a59c074bc5fd326e03dcf2867eea6cb9c821cb","compilerSettings":{"strictNullChecks":true},"code":"// When a particular type feels like it's useful in most\n// codebases, they are added into TypeScript and become\n// available for anyone which means you can consistently\n// rely on their availability\n\n// Partial\n\n// Takes a type and converts all of its properties\n// to optional ones.\n\ninterface Sticker {\n id: number;\n name: string;\n createdAt: string;\n updatedAt: string;\n submitter: undefined | string;\n}\n\ntype StickerUpdateParam = Partial;\n\n// Readonly\n\n// Takes an object and makes its properties read-only.\n\ntype StickerFromAPI = Readonly;\n\n// Record\n\n// Creates a type which uses the list of properties from\n// KeysFrom and gives them the value of Type.\n\n// List which keys come from:\ntype NavigationPages = \"home\" | \"stickers\" | \"about\" | \"contact\";\n\n// The shape of the data for which each of ^ is needed:\ninterface PageInfo {\n title: string;\n url: string;\n axTitle?: string;\n}\n\nconst navigationInfo: Record = {\n home: { title: \"Home\", url: \"/\" },\n about: { title: \"About\", url: \"/about\" },\n contact: { title: \"Contact\", url: \"/contact\" },\n stickers: { title: \"Stickers\", url: \"/stickers/all\" },\n};\n\n// Pick\n\n// Creates a type by picking the set of properties Keys\n// from Type. Essentially an allow-list for extracting type\n// information from a type.\n\ntype StickerSortPreview = Pick;\n\n// Omit\n\n// Creates a type by removing the set of properties Keys\n// from Type. Essentially a block-list for extracting type\n// information from a type.\n\ntype StickerTimeMetadata = Omit;\n\n// Exclude\n\n// Creates a type where any property in Type's properties\n// which don't overlap with RemoveUnion.\n\ntype HomeNavigationPages = Exclude;\n\n// Extract\n\n// Creates a type where any property in Type's properties\n// are included if they overlap with MatchUnion.\n\ntype DynamicPages = Extract;\n\n// NonNullable\n\n// Creates a type by excluding null and undefined from a set\n// of properties. Useful when you have a validation check.\n\ntype StickerLookupResult = Sticker | undefined | null;\ntype ValidatedResult = NonNullable;\n\n// ReturnType\n\n// Extracts the return value from a Type.\n\ndeclare function getStickerByID(id: number): Promise;\ntype StickerResponse = ReturnType;\n\n// InstanceType\n\n// Creates a type which is an instance of a class, or object\n// with a constructor function.\n\nclass StickerCollection {\n stickers: Sticker[];\n}\n\ntype CollectionItem = InstanceType;\n\n// Required