From 6ac0e976afe8f68d812e7790b5d0e8739c0e99b6 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:05:46 -0700 Subject: [PATCH 01/16] Add standalone TypeScript 7.1 playground Build a static Monaco 0.56 playground against the local wasip1 compiler API. Support Twoslash type queries, diagnostics, JavaScript emit, standard library types, and a browser console API without playground plugins. --- packages/ts7-playground/README.md | 22 ++ packages/ts7-playground/package.json | 20 + packages/ts7-playground/scripts/build.mjs | 106 ++++++ .../ts7-playground/scripts/smoke-test.mjs | 56 +++ packages/ts7-playground/src/global.d.ts | 11 + packages/ts7-playground/src/index.html | 38 ++ packages/ts7-playground/src/main.ts | 357 ++++++++++++++++++ packages/ts7-playground/src/styles.css | 151 ++++++++ packages/ts7-playground/tsconfig.json | 20 + pnpm-lock.yaml | 57 +++ 10 files changed, 838 insertions(+) create mode 100644 packages/ts7-playground/README.md create mode 100644 packages/ts7-playground/package.json create mode 100644 packages/ts7-playground/scripts/build.mjs create mode 100644 packages/ts7-playground/scripts/smoke-test.mjs create mode 100644 packages/ts7-playground/src/global.d.ts create mode 100644 packages/ts7-playground/src/index.html create mode 100644 packages/ts7-playground/src/main.ts create mode 100644 packages/ts7-playground/src/styles.css create mode 100644 packages/ts7-playground/tsconfig.json diff --git a/packages/ts7-playground/README.md b/packages/ts7-playground/README.md new file mode 100644 index 000000000000..91846b96aadb --- /dev/null +++ b/packages/ts7-playground/README.md @@ -0,0 +1,22 @@ +# TypeScript 7.1 playground + +A static, plugin-free Monaco playground backed by the `wasip1` TypeScript +compiler. It supports Twoslash `^?` type queries, compiler diagnostics, and +JavaScript emit. + +The build uses the sibling `../TypeScript` checkout by default. Override it +with `TYPESCRIPT_REPO` when needed: + +```sh +TYPESCRIPT_REPO=/path/to/TypeScript pnpm --filter @typescript/ts7-playground build +``` + +The generated static site is written to `dist`. Run the local development +server with: + +```sh +pnpm --filter @typescript/ts7-playground dev +``` + +The compiler API is also exposed as `window.ts` for experiments in the browser +development console. diff --git a/packages/ts7-playground/package.json b/packages/ts7-playground/package.json new file mode 100644 index 000000000000..485ece2ca2c4 --- /dev/null +++ b/packages/ts7-playground/package.json @@ -0,0 +1,20 @@ +{ + "name": "@typescript/ts7-playground", + "private": true, + "type": "module", + "scripts": { + "build": "node scripts/build.mjs", + "dev": "node scripts/build.mjs --serve", + "test": "node scripts/smoke-test.mjs", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@fontsource/nunito-sans": "^5.2.6", + "hack-font": "^3.3.0", + "monaco-editor": "0.56.0" + }, + "devDependencies": { + "esbuild": "^0.27.3", + "typescript": "*" + } +} diff --git a/packages/ts7-playground/scripts/build.mjs b/packages/ts7-playground/scripts/build.mjs new file mode 100644 index 000000000000..e25ea28d3688 --- /dev/null +++ b/packages/ts7-playground/scripts/build.mjs @@ -0,0 +1,106 @@ +import { context } from "esbuild" +import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises" +import { basename, 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 outputDirectory = resolve(packageDirectory, "dist") +const serve = process.argv.includes("--serve") + +const typescriptAPI = resolve(typescriptDirectory, "packages/typescript/dist/api/sync/api.js") +const wasmPackage = resolve(typescriptDirectory, "packages/typescript-wasip1-wasm/dist/index.js") +const wasmFile = resolve(typescriptDirectory, "packages/typescript-wasip1-wasm/dist/tsc.wasm") +const libDirectory = resolve(typescriptDirectory, "built/local") +const editorWorker = fileURLToPath( + import.meta.resolve("monaco-editor/editor/editor.worker"), +) + +const versionResult = spawnSync( + resolve(typescriptDirectory, "built/local/tsc"), + ["--version"], + { encoding: "utf8" }, +) +const version = versionResult.status === 0 + ? versionResult.stdout.trim().replace(/^Version\s+/, "") + : "7.1.0-dev" + +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"), + ]), + ), +) +await Promise.all([ + cp(resolve(packageDirectory, "src/index.html"), resolve(outputDirectory, "index.html")), + cp(wasmFile, resolve(outputDirectory, "tsc.wasm")), + writeFile(resolve(outputDirectory, "lib-files.json"), JSON.stringify(libFiles)), +]) + +const buildContext = await context({ + absWorkingDir: packageDirectory, + bundle: true, + conditions: ["browser", "default"], + define: { + __TS_VERSION__: JSON.stringify(version), + }, + entryNames: "[name]", + entryPoints: { + main: resolve(packageDirectory, "src/main.ts"), + "editor.worker": editorWorker, + }, + format: "esm", + loader: { + ".ttf": "file", + ".woff": "file", + ".woff2": "file", + }, + outdir: outputDirectory, + platform: "browser", + plugins: [ + { + name: "local-typescript-wasip1", + setup(build) { + build.onResolve( + { filter: /^@typescript\/typescript\/unstable\/sync$/ }, + () => ({ path: typescriptAPI }), + ) + build.onResolve( + { filter: /^@typescript\/typescript-wasip1-wasm$/ }, + () => ({ path: wasmPackage }), + ) + }, + }, + ], + 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 7.1 playground: http://${server.host}:${server.port}`) +} +else { + await buildContext.rebuild() + await buildContext.dispose() + const htmlPath = resolve(outputDirectory, "index.html") + const html = await readFile(htmlPath, "utf8") + await writeFile(htmlPath, html.replace("", `<title data-typescript-version="${version}">`)) + console.log(`Built TypeScript ${version} playground in ${outputDirectory}`) +} diff --git a/packages/ts7-playground/scripts/smoke-test.mjs b/packages/ts7-playground/scripts/smoke-test.mjs new file mode 100644 index 000000000000..c975235969eb --- /dev/null +++ b/packages/ts7-playground/scripts/smoke-test.mjs @@ -0,0 +1,56 @@ +import assert from "node:assert/strict" +import { readFile, readdir } from "node:fs/promises" +import { resolve } from "node:path" +import { pathToFileURL } from "node:url" + +const packageDirectory = resolve(import.meta.dirname, "..") +const typescriptDirectory = resolve( + process.env.TYPESCRIPT_REPO || resolve(packageDirectory, "../../..", "TypeScript"), +) +const apiModule = await import( + pathToFileURL(resolve(typescriptDirectory, "packages/typescript/dist/api/sync/api.js")) +) +const wasmModule = await import( + pathToFileURL(resolve(typescriptDirectory, "packages/typescript-wasip1-wasm/dist/index.js")) +) +const wasm = await readFile(resolve(packageDirectory, "dist/tsc.wasm")) +const module = await WebAssembly.compile(wasm) +const instance = await wasmModule.instantiateWasm(module) +const transport = new wasmModule.WasmTransport({ instance, cwd: "/" }) +const api = new apiModule.API({ transport }) + +try { + const libDirectory = resolve(typescriptDirectory, "built/local") + 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 source = "const answers = [40, 41, 42].map(value => value + 1);" + const emitted = api.transpileModule(source, { + compilerOptions: { module: 99, target: 99 }, + fileName: "/index.ts", + reportDiagnostics: true, + }) + assert.match(emitted.outputText, /const answers = \[40, 41, 42\]\.map/) + + transport.setFile("/index.ts", source) + const program = api.createProgram( + ["/index.ts"], + { compilerOptions: { strict: true, target: 99 } }, + ) + try { + assert.equal(program.getSyntacticDiagnostics("/index.ts").length, 0) + assert.equal(program.getSemanticDiagnostics("/index.ts").length, 0) + assert.ok(program.getSourceFile("/index.ts")) + } + finally { + program.dispose() + } +} +finally { + api.close() +} + +console.log("TypeScript WASM API smoke test passed") diff --git a/packages/ts7-playground/src/global.d.ts b/packages/ts7-playground/src/global.d.ts new file mode 100644 index 000000000000..5700390e1ee9 --- /dev/null +++ b/packages/ts7-playground/src/global.d.ts @@ -0,0 +1,11 @@ +declare module "*.css" + +declare module "monaco-editor/editor/editor.api" { + export * from "monaco-editor" +} + +declare module "monaco-editor/editor/contrib/find/browser/findController" +declare module "monaco-editor/editor/contrib/gotoError/browser/gotoError" +declare module "monaco-editor/editor/contrib/hover/browser/hoverContribution" +declare module "monaco-editor/editor/contrib/inlayHints/browser/inlayHintsContribution" +declare module "monaco-editor/editor/contrib/tokenization/browser/tokenization" diff --git a/packages/ts7-playground/src/index.html b/packages/ts7-playground/src/index.html new file mode 100644 index 000000000000..703285d497d5 --- /dev/null +++ b/packages/ts7-playground/src/index.html @@ -0,0 +1,38 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="color-scheme" content="light dark" /> + <title>TypeScript 7.1 Playground + + + +
+
+
+

TypeScript 7.1 Playground

+

Native TypeScript, compiled to WebAssembly

+
+ Loading compiler... +
+
+
+
+

TypeScript

+ Twoslash: align ^? below an expression +
+
+
+
+
+

JavaScript

+ Emitted by the TS 7.1 wasip1 build +
+
+
+
+
+ + + diff --git a/packages/ts7-playground/src/main.ts b/packages/ts7-playground/src/main.ts new file mode 100644 index 000000000000..cd81c6ce8fd4 --- /dev/null +++ b/packages/ts7-playground/src/main.ts @@ -0,0 +1,357 @@ +import * as monaco from "monaco-editor/editor/editor.api" +import "monaco-editor/editor/contrib/find/browser/findController" +import "monaco-editor/editor/contrib/gotoError/browser/gotoError" +import "monaco-editor/editor/contrib/hover/browser/hoverContribution" +import "monaco-editor/editor/contrib/inlayHints/browser/inlayHintsContribution" +import "monaco-editor/editor/contrib/tokenization/browser/tokenization" +import "monaco-editor/languages/definitions/typescript/register" +import "monaco-editor/languages/definitions/javascript/register" +import { API, DiagnosticCategory, type Diagnostic } from "@typescript/typescript/unstable/sync" +import { instantiateWasm, WasmTransport } from "@typescript/typescript-wasip1-wasm" +import "./styles.css" + +declare const __TS_VERSION__: string + +type CompilerNode = { + forEachChild(visitor: (node: CompilerNode) => T): T | undefined + getEnd(): number + getFullStart(): number +} + +type TypeQuery = { + lineNumber: number + column: number + label: string +} + +declare global { + interface Window { + ts: API & { + API: typeof API + DiagnosticCategory: typeof DiagnosticCategory + version: string + } + } +} + +;(self as typeof self & { + MonacoEnvironment: { getWorker(): Worker } +}).MonacoEnvironment = { + getWorker() { + return new Worker(new URL("./editor.worker.js", import.meta.url), { type: "module" }) + }, +} + +const sourceFileName = "/index.ts" +const defaultSource = `type Gopher = { + value: T + concurrent: true +} + +const result = { + value: "hello from TS 7.1", + concurrent: true, +} satisfies Gopher +// ^? + +console.log(result.value) +` + +const inputElement = getElement("input-editor") +const outputElement = getElement("output-editor") +const status = getElement("status") + +const darkMode = 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 ? "569cd6" : "3757ef" }, + { token: "type", foreground: darkMode ? "4ec9b0" : "1142af" }, + ], + colors: { + "editor.background": darkMode ? "#1e1e1e" : "#fafafa", + "editor.inlayHint.background": darkMode ? "#333333" : "#eeeeee", + "editor.inlayHint.foreground": darkMode ? "#d4d4d4" : "#333333", + }, +}) + +const inputModel = monaco.editor.createModel( + localStorage.getItem("ts7-playground-source") ?? defaultSource, + "typescript", + monaco.Uri.file(sourceFileName), +) +const outputModel = monaco.editor.createModel("", "javascript", monaco.Uri.file("/index.js")) +const sharedOptions: monaco.editor.IStandaloneEditorConstructionOptions = { + automaticLayout: true, + fontFamily: "Hack, monospace", + fontLigatures: true, + fontSize: 14, + minimap: { enabled: false }, + padding: { top: 10 }, + scrollBeyondLastLine: false, + tabSize: 2, + theme: "typescript-playground", +} +const inputEditor = monaco.editor.create(inputElement, { + ...sharedOptions, + model: inputModel, + inlayHints: { enabled: "on" }, +}) +monaco.editor.create(outputElement, { + ...sharedOptions, + model: outputModel, + readOnly: true, + renderValidationDecorations: "off", +}) + +const inlayEmitter = new monaco.Emitter() +let typeQueries: TypeQuery[] = [] +monaco.languages.registerInlayHintsProvider("typescript", { + onDidChangeInlayHints: inlayEmitter.event, + provideInlayHints() { + return { + hints: typeQueries.map(query => ({ + kind: monaco.languages.InlayHintKind.Type, + position: new monaco.Position(query.lineNumber, query.column), + label: query.label, + paddingLeft: true, + })), + dispose() {}, + } + }, +}) + +void initializeCompiler() + +async function initializeCompiler() { + try { + const [wasmResponse, libFilesResponse] = await Promise.all([ + fetch(new URL("./tsc.wasm", import.meta.url)), + fetch(new URL("./lib-files.json", import.meta.url)), + ]) + if (!wasmResponse.ok) { + throw new Error(`Unable to load tsc.wasm: ${wasmResponse.status} ${wasmResponse.statusText}`) + } + if (!libFilesResponse.ok) { + throw new Error( + `Unable to load lib-files.json: ${libFilesResponse.status} ${libFilesResponse.statusText}`, + ) + } + + const [module, libFiles] = await Promise.all([ + WebAssembly.compileStreaming(wasmResponse), + libFilesResponse.json() as Promise>, + ]) + const instance = await instantiateWasm(module) + const transport = new WasmTransport({ instance, cwd: "/" }) + const api = new API({ transport }) + for (const [fileName, content] of Object.entries(libFiles)) { + transport.setFile(fileName, content) + } + window.ts = Object.assign(api, { + API, + DiagnosticCategory, + version: __TS_VERSION__, + }) + + let updateTimer = 0 + const update = () => { + window.clearTimeout(updateTimer) + updateTimer = window.setTimeout(() => compile(api, transport), 180) + } + + inputModel.onDidChangeContent(() => { + localStorage.setItem("ts7-playground-source", inputModel.getValue()) + update() + }) + compile(api, transport) + inputEditor.focus() + } + catch (error) { + const message = error instanceof Error ? error.message : String(error) + setStatus(message, "error") + outputModel.setValue(`// Failed to initialize TypeScript 7.1\n// ${message}`) + console.error(error) + } +} + +function compile(api: API, transport: WasmTransport) { + const source = inputModel.getValue() + setStatus("Checking...", "loading") + + try { + transport.setFile(sourceFileName, source) + const transpiled = api.transpileModule(source, { + compilerOptions: { + module: 99, + target: 99, + }, + fileName: sourceFileName, + reportDiagnostics: true, + }) + outputModel.setValue(transpiled.outputText) + + const program = api.createProgram( + [sourceFileName], + { + compilerOptions: { + module: 99, + strict: true, + target: 99, + }, + }, + ) + + try { + const sourceFile = program.getSourceFile(sourceFileName) + if (!sourceFile) { + throw new Error(`Compiler did not return ${sourceFileName}`) + } + + const project = program.getProject() + const diagnostics = deduplicateDiagnostics([ + ...program.getSyntacticDiagnostics(sourceFileName), + ...program.getSemanticDiagnostics(sourceFileName), + ...(transpiled.diagnostics ?? []), + ]) + setDiagnostics(diagnostics) + typeQueries = collectTypeQueries(source, sourceFile, project.checker) + inlayEmitter.fire() + setStatus( + diagnostics.length === 0 + ? `${__TS_VERSION__} ready` + : `${__TS_VERSION__} · ${diagnostics.length} diagnostic${diagnostics.length === 1 ? "" : "s"}`, + "ready", + ) + } + finally { + program.dispose() + } + } + catch (error) { + const message = error instanceof Error ? error.message : String(error) + monaco.editor.setModelMarkers(inputModel, "typescript-7.1", []) + typeQueries = [] + inlayEmitter.fire() + setStatus(message, "error") + console.error(error) + } +} + +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"], +): TypeQuery[] { + 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 = inputModel.getPositionAt(queryEnd) + if (queryPosition.lineNumber === 1) continue + + const inspectedPosition = inputModel.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[]) { + monaco.editor.setModelMarkers( + inputModel, + "typescript-7.1", + diagnostics + .filter(diagnostic => diagnostic.fileName === undefined || diagnostic.fileName === sourceFileName) + .map(diagnostic => { + const start = inputModel.getPositionAt(diagnostic.pos) + const end = inputModel.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, + } + }), + ) +} + +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 + } +} + +function setStatus(message: string, state: "loading" | "ready" | "error") { + status.textContent = message + status.dataset.state = state +} + +function truncate(value: string, maxLength: number) { + return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}…` +} + +function getElement(id: string) { + const element = document.getElementById(id) + if (!element) throw new Error(`Missing #${id}`) + return element +} diff --git a/packages/ts7-playground/src/styles.css b/packages/ts7-playground/src/styles.css new file mode 100644 index 000000000000..938a2d5b890d --- /dev/null +++ b/packages/ts7-playground/src/styles.css @@ -0,0 +1,151 @@ +@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; + --border: #d6d6d6; + --muted: #5f6368; + --panel: #ffffff; + --toolbar: #3178c6; +} + +* { + box-sizing: border-box; +} + +html, +body, +.playground { + width: 100%; + height: 100%; + margin: 0; +} + +body { + overflow: hidden; +} + +.playground { + 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 p { + margin-top: 0.125rem; + font-size: 0.875rem; + opacity: 0.85; +} + +.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; +} + +.editors { + min-height: 0; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); +} + +.editor-panel { + min-width: 0; + min-height: 0; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + background: var(--panel); +} + +.editor-panel + .editor-panel { + border-left: 1px solid var(--border); +} + +.panel-heading { + min-height: 2.75rem; + padding: 0.55rem 0.75rem; + border-bottom: 1px solid var(--border); + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; +} + +.panel-heading h2 { + font-size: 0.95rem; + font-weight: 600; +} + +.panel-heading span { + color: var(--muted); + font-size: 0.75rem; + text-align: right; +} + +code { + font-family: Hack, monospace; +} + +.editor { + min-width: 0; + min-height: 0; +} + +@media (max-width: 800px) { + .editors { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); + } + + .editor-panel + .editor-panel { + border-top: 1px solid var(--border); + border-left: 0; + } +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f3f3f3; + background: #1e1e1e; + --border: #414141; + --muted: #b7b7b7; + --panel: #1e1e1e; + --toolbar: #235a97; + } +} diff --git a/packages/ts7-playground/tsconfig.json b/packages/ts7-playground/tsconfig.json new file mode 100644 index 000000000000..2d5458283012 --- /dev/null +++ b/packages/ts7-playground/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "ES2022"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "paths": { + "@typescript/typescript/unstable/sync": [ + "../../../TypeScript/packages/typescript/dist/api/sync/api.d.ts" + ], + "@typescript/typescript-wasip1-wasm": [ + "../../../TypeScript/packages/typescript-wasip1-wasm/dist/index.d.ts" + ] + } + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c8438854935..36a341509af5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -270,6 +270,25 @@ importers: specifier: 6.0.2 version: 6.0.2 + packages/ts7-playground: + dependencies: + '@fontsource/nunito-sans': + specifier: ^5.2.6 + version: 5.3.0 + hack-font: + specifier: ^3.3.0 + version: 3.3.0 + monaco-editor: + specifier: 0.56.0 + version: 0.56.0 + devDependencies: + esbuild: + specifier: ^0.27.3 + version: 0.27.3 + typescript: + specifier: 6.0.2 + version: 6.0.2 + packages/tsconfig-reference: devDependencies: '@types/json-schema': @@ -1565,6 +1584,9 @@ packages: '@expo/sudo-prompt@9.3.2': resolution: {integrity: sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==} + '@fontsource/nunito-sans@5.3.0': + resolution: {integrity: sha512-CctNhebQ2YYJUwVMCAkHexQg7yedfz9s8JzaETALyc9QIQuDFRlNTWXux9H5PsNLgptPkrzRDomAdLJt1sDFpQ==} + '@formatjs/ecma402-abstract@2.3.6': resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} @@ -2838,6 +2860,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -4536,6 +4561,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.4.8: + resolution: {integrity: sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==} + domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -5659,6 +5687,9 @@ packages: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} + hack-font@3.3.0: + resolution: {integrity: sha512-RohrcAr3UaKiIoxDlOytCjObcUAucfFc6V5fKu6gBrvmvTfIXeBqZwR0Q5kb9qpbluThJWt326LClLKIGiFyug==} + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -6972,6 +7003,11 @@ packages: markdown-table@2.0.0: resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -7217,6 +7253,9 @@ packages: monaco-editor@0.32.1: resolution: {integrity: sha512-LUt2wsUvQmEi2tfTOK+tjAPvt7eQ+K5C4rZPr6SeuyzjAuAHrIvlUloTcOiGjZW3fn3a/jFQCONrEJbNOaCqbA==} + monaco-editor@0.56.0: + resolution: {integrity: sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==} + monaco-typescript@4.10.0: resolution: {integrity: sha512-vzd7IGCshZO05YW/Kv2V/aKU/Pn8N3RjioHPmSJzG+4E9odnTrCOP8c/iOgkl2IyRS72WReDJ7xDYtMtg8WrOQ==} @@ -11193,6 +11232,8 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} + '@fontsource/nunito-sans@5.3.0': {} + '@formatjs/ecma402-abstract@2.3.6': dependencies: '@formatjs/fast-memoize': 2.2.7 @@ -12850,6 +12891,9 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@types/yargs-parser@21.0.3': {} @@ -14846,6 +14890,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.4.8: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@2.8.0: dependencies: dom-serializer: 1.4.1 @@ -16835,6 +16883,8 @@ snapshots: dependencies: duplexer: 0.1.2 + hack-font@3.3.0: {} + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -18682,6 +18732,8 @@ snapshots: dependencies: repeat-string: 1.6.1 + marked@14.0.0: {} + math-intrinsics@1.1.0: {} md5.js@1.3.5: @@ -18986,6 +19038,11 @@ snapshots: monaco-editor@0.32.1: {} + monaco-editor@0.56.0: + dependencies: + dompurify: 3.4.8 + marked: 14.0.0 + monaco-typescript@4.10.0: {} mri@1.2.0: {} From 21f6e304e9015c0084a8f97a988ec5d78e39c2e9 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:09:14 -0700 Subject: [PATCH 02/16] Integrate TS 7.1 playground into website Add the /play/7-1/ Gatsby route and materialize the standalone bundle as ignored static assets during site start and production builds. --- .gitignore | 1 + packages/ts7-playground/README.md | 7 ++++-- packages/ts7-playground/scripts/build.mjs | 6 +++++ packages/typescriptlang-org/package.json | 2 ++ .../src/pages/play/7-1.scss | 17 +++++++++++++ .../typescriptlang-org/src/pages/play/7-1.tsx | 25 +++++++++++++++++++ 6 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 packages/typescriptlang-org/src/pages/play/7-1.scss create mode 100644 packages/typescriptlang-org/src/pages/play/7-1.tsx diff --git a/.gitignore b/.gitignore index 5cc7faa8b479..9a63999ff612 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,7 @@ packages/documentation/output/attribution.json packages/sandbox/src/releases.json packages/sandbox/src/release_data.ts packages/typescriptlang-org/src/lib/documentationNavigation.ts +packages/typescriptlang-org/static/ts7-playground/ .idea/ diff --git a/packages/ts7-playground/README.md b/packages/ts7-playground/README.md index 91846b96aadb..8a1be2bda2e8 100644 --- a/packages/ts7-playground/README.md +++ b/packages/ts7-playground/README.md @@ -11,8 +11,9 @@ with `TYPESCRIPT_REPO` when needed: TYPESCRIPT_REPO=/path/to/TypeScript pnpm --filter @typescript/ts7-playground build ``` -The generated static site is written to `dist`. Run the local development -server with: +The generated static site is written to `dist` and copied into the website's +ignored `packages/typescriptlang-org/static/ts7-playground` directory. Run the +local development server with: ```sh pnpm --filter @typescript/ts7-playground dev @@ -20,3 +21,5 @@ pnpm --filter @typescript/ts7-playground dev The compiler API is also exposed as `window.ts` for experiments in the browser development console. + +When running the full website, the playground is available at `/play/7-1/`. diff --git a/packages/ts7-playground/scripts/build.mjs b/packages/ts7-playground/scripts/build.mjs index e25ea28d3688..24b2582016bf 100644 --- a/packages/ts7-playground/scripts/build.mjs +++ b/packages/ts7-playground/scripts/build.mjs @@ -10,6 +10,10 @@ const typescriptDirectory = resolve( process.env.TYPESCRIPT_REPO || resolve(websiteDirectory, "../TypeScript"), ) const outputDirectory = resolve(packageDirectory, "dist") +const websiteStaticDirectory = resolve( + websiteDirectory, + "packages/typescriptlang-org/static/ts7-playground", +) const serve = process.argv.includes("--serve") const typescriptAPI = resolve(typescriptDirectory, "packages/typescript/dist/api/sync/api.js") @@ -102,5 +106,7 @@ else { const htmlPath = resolve(outputDirectory, "index.html") const html = await readFile(htmlPath, "utf8") await writeFile(htmlPath, html.replace("", `<title data-typescript-version="${version}">`)) + await rm(websiteStaticDirectory, { force: true, recursive: true }) + await cp(outputDirectory, websiteStaticDirectory, { recursive: true }) console.log(`Built TypeScript ${version} playground in ${outputDirectory}`) } diff --git a/packages/typescriptlang-org/package.json b/packages/typescriptlang-org/package.json index 5e09f4d12c18..51094ab6068c 100644 --- a/packages/typescriptlang-org/package.json +++ b/packages/typescriptlang-org/package.json @@ -5,6 +5,7 @@ "version": "0.0.0", "license": "MIT", "scripts": { + "prebuild": "pnpm --filter @typescript/ts7-playground build", "build": "GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES=true gatsby build", "clean": "gatsby clean", "bootstrap": "pnpm update-versions", @@ -13,6 +14,7 @@ "setup-playground-cache-bust": "node scripts/cacheBustPlayground.mjs", "create-lighthouse-json": "node scripts/createLighthouseJSON.js", "compile-index-examples": "twoslash --reactAlso src/components/index/twoslash/*.ts src/components/index/twoslash/*.js src/components/index/twoslash/*.tsx src/components/index/twoslash/generated && node scripts/updateIndexTwoslashExamples.js", + "prestart": "pnpm --filter @typescript/ts7-playground build", "start": "gatsby develop", "serve": "gatsby serve", "test": "pnpm tsc && jest" diff --git a/packages/typescriptlang-org/src/pages/play/7-1.scss b/packages/typescriptlang-org/src/pages/play/7-1.scss new file mode 100644 index 000000000000..13e3545256a4 --- /dev/null +++ b/packages/typescriptlang-org/src/pages/play/7-1.scss @@ -0,0 +1,17 @@ +.ts7-playground-page { + position: fixed; + inset: 0; + background: #fafafa; +} + +.ts7-playground-page iframe { + width: 100%; + height: 100%; + border: 0; +} + +@media (prefers-color-scheme: dark) { + .ts7-playground-page { + background: #1e1e1e; + } +} diff --git a/packages/typescriptlang-org/src/pages/play/7-1.tsx b/packages/typescriptlang-org/src/pages/play/7-1.tsx new file mode 100644 index 000000000000..ebe47cc14500 --- /dev/null +++ b/packages/typescriptlang-org/src/pages/play/7-1.tsx @@ -0,0 +1,25 @@ +import React from "react" +import { withPrefix } from "gatsby" +import { Helmet } from "react-helmet" + +import "./7-1.scss" + +const TypeScript71Playground = () => ( + <> + <Helmet> + <title>TypeScript 7.1 Playground + + +
+