From f022a5cfc302345a9b0c81acd3460ee2e90bb37b Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:35:44 -0700 Subject: [PATCH 01/14] Avoid rebuilding packages after bootstrapping Bootstrap already builds the workspace packages. Repeating the build in CI and deployment adds about 45 seconds on the Linux runner without producing anything new. --- .github/workflows/CI.yml | 1 - .github/workflows/deploy-prod-static.yml | 1 - .github/workflows/publish-packages.yml | 1 - README.md | 3 +++ docs/Setup Troubleshooting.md | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index bff849e3510a..3f96db052e2d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -36,7 +36,6 @@ jobs: # Build the packages - run: pnpm bootstrap - - run: pnpm build # Verify it compiles - run: pnpm build-site diff --git a/.github/workflows/deploy-prod-static.yml b/.github/workflows/deploy-prod-static.yml index f70ec81bb839..8bbdcb00097f 100644 --- a/.github/workflows/deploy-prod-static.yml +++ b/.github/workflows/deploy-prod-static.yml @@ -44,7 +44,6 @@ jobs: pnpm docs-sync pull microsoft/TypeScript-Website-localizations#main 1 pnpm bootstrap pnpm run --filter=typescriptlang-org setup-playground-cache-bust - pnpm build - name: Makes the site run: | diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index 63632d66c56f..d58cf049e48c 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -35,7 +35,6 @@ jobs: run: | pnpm install pnpm bootstrap - pnpm build - uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 with: diff --git a/README.md b/README.md index 2fbe85f3a480..6ceb686530cb 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ Deployment is automatic: You can find the build logs in [GitHub Actions](https://github.com/microsoft/TypeScript-Website/actions) +`pnpm bootstrap` already builds the workspace packages; it does not need to be +followed by `pnpm build`. Run `pnpm build-site` afterward to build the static site. + ## Docs If you want to know _in-depth_ how this website works, there is an [hour long video covering the codebase, deployment and tooling on YouTube.](https://www.youtube.com/watch?v=HOvivt6B7hE). Otherwise there are some short guides: diff --git a/docs/Setup Troubleshooting.md b/docs/Setup Troubleshooting.md index 510336bc8a78..e51aa84016ab 100644 --- a/docs/Setup Troubleshooting.md +++ b/docs/Setup Troubleshooting.md @@ -2,7 +2,7 @@ #### Module `x` is not built -Sometimes, and it's not been tracked down exactly, some dependencies of the site aren't built even though it says they are. In those cases, re-run `pnpm bootstrap` and `pnpm build` to re-build all the internal site deps. +Sometimes, and it's not been tracked down exactly, some dependencies of the site aren't built even though it says they are. In those cases, re-run `pnpm bootstrap` to re-build all the internal site deps. #### Windows + Watchman From 899c793cbeb4773c6b8d1a7aa940973f1b9d62d8 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:49:02 -0700 Subject: [PATCH 02/14] Avoid redundant syntax-highlighting work Theme explanations dominate highlighting time but are only consumed by TSConfig's JSON property hovers. Keep those scopes without computing unused explanations for every other language, and reuse tokenization for repeated examples without sharing mutable tokens. All 920 localized page-data files remain identical. Rendered HTML is unchanged apart from existing random links and build identifiers. --- .../lib/bootup/shiki.test.js | 99 +++++++++++++++++++ .../gatsby-remark-shiki-twoslash@3.0.38.patch | 40 +++++++- pnpm-lock.yaml | 6 +- 3 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 packages/typescriptlang-org/lib/bootup/shiki.test.js diff --git a/packages/typescriptlang-org/lib/bootup/shiki.test.js b/packages/typescriptlang-org/lib/bootup/shiki.test.js new file mode 100644 index 000000000000..083076712c2a --- /dev/null +++ b/packages/typescriptlang-org/lib/bootup/shiki.test.js @@ -0,0 +1,99 @@ +jest.mock("remark-shiki-twoslash", () => ({ + setupForFile: jest.fn(), + remarkVisitor: jest.fn(), +})) + +const { setupForFile, remarkVisitor } = require("remark-shiki-twoslash") +const plugin = require("gatsby-remark-shiki-twoslash") + +const ast = (value = "const value = 1", lang = "ts", meta = undefined) => ({ + type: "root", + children: [{ type: "code", value, lang, meta }], +}) + +let highlighter + +beforeEach(() => { + jest.clearAllMocks() + highlighter = { + codeToThemedTokens: jest.fn(() => [[{ content: "token", color: "#000" }]]), + } + setupForFile.mockImplementation(async settings => ({ + settings, + highlighters: [highlighter], + })) + remarkVisitor.mockImplementation(highlighters => node => { + const tokens = highlighters[0].codeToThemedTokens(node.value, node.lang) + node.type = "html" + node.value = `
${tokens[0][0].content}
` + tokens[0][0].content = "mutated" + node.children = [] + }) +}) + +it("reuses highlighting without sharing mutable tokens or losing focusability", async () => { + const first = ast() + const second = ast() + await plugin({ markdownAST: first }, {}) + await plugin({ markdownAST: second }, {}) + expect(highlighter.codeToThemedTokens).toHaveBeenCalledTimes(1) + expect(highlighter.codeToThemedTokens).toHaveBeenCalledWith( + "const value = 1", + "ts", + undefined, + { includeExplanation: false } + ) + expect(second).toEqual(first) + expect(second.children[0].value).toContain("tabindex='0'>token") + expect(setupForFile).toHaveBeenCalledTimes(2) + expect(remarkVisitor).toHaveBeenCalledTimes(2) +}) + +it("keeps scopes for TSConfig property hovers", async () => { + await plugin({ markdownAST: ast('{"strict": true}', "json", "tsconfig") }, {}) + expect(highlighter.codeToThemedTokens).toHaveBeenCalledWith( + '{"strict": true}', + "json", + undefined, + { includeExplanation: true } + ) +}) + +it("distinguishes languages and code", async () => { + await plugin({ markdownAST: ast() }, {}) + await plugin({ markdownAST: ast("const value = 2") }, {}) + await plugin({ markdownAST: ast("const value = 1", "js") }, {}) + expect(highlighter.codeToThemedTokens).toHaveBeenCalledTimes(3) +}) + +it("does not cache highlighting failures", async () => { + highlighter.codeToThemedTokens.mockImplementationOnce(() => { + throw new Error("highlighting failed") + }) + await expect(plugin({ markdownAST: ast() }, {})).rejects.toThrow( + "highlighting failed" + ) + await plugin({ markdownAST: ast() }, {}) + expect(highlighter.codeToThemedTokens).toHaveBeenCalledTimes(2) +}) + +it("isolates highlighter instances", async () => { + const firstHighlighter = highlighter + await plugin({ markdownAST: ast() }, {}) + highlighter = { + codeToThemedTokens: jest.fn(() => [[{ content: "different theme" }]]), + } + const second = ast() + await plugin({ markdownAST: second }, {}) + expect(firstHighlighter.codeToThemedTokens).toHaveBeenCalledTimes(1) + expect(highlighter.codeToThemedTokens).toHaveBeenCalledTimes(1) + expect(second.children[0].value).toContain("different theme") +}) + +it("bounds the highlighting cache", async () => { + for (let i = 0; i < 257; i++) { + await plugin({ markdownAST: ast(`const value = ${i}`) }, {}) + } + await plugin({ markdownAST: ast("const value = 0") }, {}) + expect(highlighter.codeToThemedTokens).toHaveBeenCalledTimes(258) +}) diff --git a/patches/gatsby-remark-shiki-twoslash@3.0.38.patch b/patches/gatsby-remark-shiki-twoslash@3.0.38.patch index b398a78298ed..844d0f4ec4e8 100644 --- a/patches/gatsby-remark-shiki-twoslash@3.0.38.patch +++ b/patches/gatsby-remark-shiki-twoslash@3.0.38.patch @@ -2,15 +2,48 @@ diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 6036d987fc2889ec9bc230f7774013ae4f736dd9..0000000000000000000000000000000000000000 diff --git a/src/index.js b/src/index.js -index e16676ef50b660d520a0efc0b73b68411db349af..bb21bc500a34ac31063774ba4ba133daed2c4f14 100755 +index e16676ef50b660d520a0efc0b73b68411db349af..5c40ade16dce3f83390819e053ee12e53e770ef4 100755 --- a/src/index.js +++ b/src/index.js -@@ -4,7 +4,15 @@ const { setupForFile, remarkVisitor } = require("remark-shiki-twoslash") +@@ -2,9 +2,47 @@ + const visit = require("unist-util-visit") + const { setupForFile, remarkVisitor } = require("remark-shiki-twoslash") ++const optimizedHighlighters = new WeakMap() ++ ++const optimizeHighlighter = highlighter => { ++ if (optimizedHighlighters.has(highlighter)) { ++ return optimizedHighlighters.get(highlighter) ++ } ++ ++ const tokensByCode = new Map() ++ const optimized = { ++ ...highlighter, ++ codeToThemedTokens(code, lang, theme) { ++ const key = JSON.stringify([code, lang, theme]) ++ let tokens = tokensByCode.get(key) ++ if (tokens === undefined) { ++ // Only TSConfig's JSON renderer inspects TextMate scopes. ++ tokens = highlighter.codeToThemedTokens(code, lang, theme, { ++ includeExplanation: !!lang && lang.startsWith("json"), ++ }) ++ if (tokensByCode.size === 256) { ++ tokensByCode.delete(tokensByCode.keys().next().value) ++ } ++ tokensByCode.set(key, tokens) ++ } ++ // Twoslash's renderer splits and annotates tokens in place. ++ return structuredClone(tokens) ++ }, ++ } ++ optimizedHighlighters.set(highlighter, optimized) ++ return optimized ++} ++ const remarkShiki = async function ({ markdownAST }, userConfig) { const {settings, highlighters} = await setupForFile(userConfig) - visit(markdownAST, "code", remarkVisitor(highlighters, settings)) -+ const orig = remarkVisitor(highlighters, settings) ++ const orig = remarkVisitor(highlighters.map(optimizeHighlighter), settings) + /** @type {typeof orig} */ + const visitor = (node) => { + orig(node) @@ -21,4 +54,3 @@ index e16676ef50b660d520a0efc0b73b68411db349af..bb21bc500a34ac31063774ba4ba133da } module.exports = remarkShiki -+ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c8438854935..b6d60946b028 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ overrides: patchedDependencies: gatsby-remark-shiki-twoslash@3.0.38: - hash: 5584d3c994349e1a6796ab8e22811fc912fad257e9ad818930836c458ab23640 + hash: 3aa0dc2d9c0fd3921986acbadfef7a10df948fb30d02590f86cecab9b91800bf path: patches/gatsby-remark-shiki-twoslash@3.0.38.patch importers: @@ -395,7 +395,7 @@ importers: version: 6.16.0(gatsby@5.16.1(babel-eslint@10.1.0(eslint@8.57.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(type-fest@4.41.0)(typescript@6.0.2)) gatsby-remark-shiki-twoslash: specifier: ^3.0.38 - version: 3.0.38(patch_hash=5584d3c994349e1a6796ab8e22811fc912fad257e9ad818930836c458ab23640) + version: 3.0.38(patch_hash=3aa0dc2d9c0fd3921986acbadfef7a10df948fb30d02590f86cecab9b91800bf) gatsby-remark-smartypants: specifier: ^6.16.0 version: 6.16.0(gatsby@5.16.1(babel-eslint@10.1.0(eslint@8.57.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(type-fest@4.41.0)(typescript@6.0.2)) @@ -16338,7 +16338,7 @@ snapshots: lodash: 4.17.23 unist-util-visit: 2.0.3 - gatsby-remark-shiki-twoslash@3.0.38(patch_hash=5584d3c994349e1a6796ab8e22811fc912fad257e9ad818930836c458ab23640): + gatsby-remark-shiki-twoslash@3.0.38(patch_hash=3aa0dc2d9c0fd3921986acbadfef7a10df948fb30d02590f86cecab9b91800bf): dependencies: remark-shiki-twoslash: 3.1.3(typescript@6.0.2) typescript: 6.0.2 From 247c8fc7aadb221678d329f3309b97ac7fbbc41a Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:59:40 -0700 Subject: [PATCH 03/14] Build bootstrap-only aliases just once Several package bootstrap scripts are aliases for their build, which runs again at the end of workspace bootstrapping. TSConfig generation also repeats work already done by its build. Do the metadata setup and one topologically ordered build instead, preserving the package-level bootstrap commands for direct use. Compiled package outputs are unchanged. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 200cf3cd8336..81bdffa41620 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ }, "scripts": { "ci:publish": "pnpm changeset git-tag && pnpm publish -r", - "bootstrap": "pnpm -r run bootstrap && BOOTSTRAPPING=true pnpm -r run build", + "bootstrap": "pnpm -r --filter=!@typescript/ata --filter=!@typescript/vfs --filter=!@typescript/twoslash --filter=!playground-examples --filter=!tsconfig-reference run bootstrap && pnpm build", "start": "concurrently -p \"[{name}]\" -n \"BUILD,SITE\" -c \"bgBlue.bold,bgMagenta.bold\" \"node watcher.js\" \"pnpm run --filter=typescriptlang-org start\"", "build": "pnpm run --filter=!typescriptlang-org build", "build-site": "pnpm run --filter=typescriptlang-org build", From 5b21845c0ef3c0f2e64c9595d4ec76d2b256ea20 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:08:59 -0700 Subject: [PATCH 04/14] Distinguish semantic inputs in the Twoslash cache Reusing compiled examples across builds must not confuse identical code in different languages or with different compiler settings. Include those inputs and the VFS version before enabling persistent CI reuse. --- package.json | 3 +- .../lib/bootup/shiki.test.js | 80 +++++++++++++++++++ patches/remark-shiki-twoslash@3.1.3.patch | 34 ++++++++ pnpm-lock.yaml | 11 ++- 4 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 patches/remark-shiki-twoslash@3.1.3.patch diff --git a/package.json b/package.json index 81bdffa41620..c89c97ea466b 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "webpack": "^5.105.3" }, "patchedDependencies": { - "gatsby-remark-shiki-twoslash@3.0.38": "patches/gatsby-remark-shiki-twoslash@3.0.38.patch" + "gatsby-remark-shiki-twoslash@3.0.38": "patches/gatsby-remark-shiki-twoslash@3.0.38.patch", + "remark-shiki-twoslash@3.1.3": "patches/remark-shiki-twoslash@3.1.3.patch" }, "ignoredBuiltDependencies": [ "@parcel/watcher", diff --git a/packages/typescriptlang-org/lib/bootup/shiki.test.js b/packages/typescriptlang-org/lib/bootup/shiki.test.js index 083076712c2a..be86891c6443 100644 --- a/packages/typescriptlang-org/lib/bootup/shiki.test.js +++ b/packages/typescriptlang-org/lib/bootup/shiki.test.js @@ -97,3 +97,83 @@ it("bounds the highlighting cache", async () => { await plugin({ markdownAST: ast("const value = 0") }, {}) expect(highlighter.codeToThemedTokens).toHaveBeenCalledTimes(258) }) + +describe("persistent Twoslash cache", () => { + const fs = require("fs") + const shikiTwoslash = require("shiki-twoslash") + const { runTwoSlashOnNode } = jest.requireActual("remark-shiki-twoslash") + const fence = lang => ({ lang, meta: { twoslash: true } }) + let runTwoSlash + + beforeEach(() => { + const files = new Map() + const existsSync = fs.existsSync + const readFileSync = fs.readFileSync + const writeFileSync = fs.writeFileSync + const isCache = path => + typeof path === "string" && + (path.includes(".cache/twoslash") || path.includes(".cache\\twoslash")) + jest + .spyOn(fs, "existsSync") + .mockImplementation(path => + isCache(path) ? files.has(path) : existsSync(path) + ) + const mkdirSync = fs.mkdirSync + jest + .spyOn(fs, "mkdirSync") + .mockImplementation((path, ...args) => + isCache(path) ? undefined : mkdirSync(path, ...args) + ) + jest + .spyOn(fs, "readFileSync") + .mockImplementation((path, ...args) => + isCache(path) ? files.get(path) : readFileSync(path, ...args) + ) + jest + .spyOn(fs, "writeFileSync") + .mockImplementation((path, data, ...args) => { + if (isCache(path)) files.set(path, data) + else writeFileSync(path, data, ...args) + }) + runTwoSlash = jest + .spyOn(shikiTwoslash, "runTwoSlash") + .mockImplementation((code, lang, settings) => ({ code, lang, settings })) + }) + + afterEach(() => jest.restoreAllMocks()) + + it("reuses identical semantic inputs", () => { + const first = runTwoSlashOnNode("const value = 1", fence("ts"), {}) + const second = runTwoSlashOnNode("const value = 1", fence("ts"), {}) + expect(second).toEqual(first) + expect(runTwoSlash).toHaveBeenCalledTimes(1) + }) + + it("distinguishes TypeScript from JavaScript", () => { + runTwoSlashOnNode("const value = 1", fence("ts"), {}) + runTwoSlashOnNode("const value = 1", fence("js"), {}) + expect(runTwoSlash).toHaveBeenCalledTimes(2) + }) + + it("invalidates results when compiler or Twoslash options change", () => { + runTwoSlashOnNode("const value = 1", fence("ts"), {}) + runTwoSlashOnNode("const value = 1", fence("ts"), { + defaultCompilerOptions: { strict: true }, + }) + runTwoSlashOnNode("const value = 1", fence("ts"), { + defaultOptions: { noErrors: true }, + }) + expect(runTwoSlash).toHaveBeenCalledTimes(3) + }) + + it("does not save failed compilations", () => { + runTwoSlash.mockImplementationOnce(() => { + throw new Error("compilation failed") + }) + expect(() => runTwoSlashOnNode("bad code", fence("ts"), {})).toThrow( + "compilation failed" + ) + runTwoSlashOnNode("bad code", fence("ts"), {}) + expect(runTwoSlash).toHaveBeenCalledTimes(2) + }) +}) diff --git a/patches/remark-shiki-twoslash@3.1.3.patch b/patches/remark-shiki-twoslash@3.1.3.patch new file mode 100644 index 000000000000..39865d603cc9 --- /dev/null +++ b/patches/remark-shiki-twoslash@3.1.3.patch @@ -0,0 +1,34 @@ +diff --git a/dist/remark-shiki-twoslash.cjs.development.js b/dist/remark-shiki-twoslash.cjs.development.js +index efaed7711553c44c31abcaf53abab08b8780b248..dab4a3abc11b39e4a2a6361d47b2259eef722ed2 100644 +--- a/dist/remark-shiki-twoslash.cjs.development.js ++++ b/dist/remark-shiki-twoslash.cjs.development.js +@@ -825,7 +825,7 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { + var tsVersion = require('typescript/package.json').version; + + var shasum = createHash("sha1"); +- var codeSha = shasum.update(code + "-" + shikiVersion + "-" + tsVersion).digest("hex"); ++ var codeSha = shasum.update(JSON.stringify([code, lang, settings, shikiVersion, tsVersion, require('@typescript/vfs/package.json').version])).digest("hex"); + + var getNmCache = function getNmCache() { + if (__dirname.includes("node_modules")) { +diff --git a/dist/remark-shiki-twoslash.cjs.production.min.js b/dist/remark-shiki-twoslash.cjs.production.min.js +index 9ee26741d08d555a0ef3c0e0917ef0d6342020a5..2a8689e22c1b2aafdb1509f266a44ecf16899447 100644 +--- a/dist/remark-shiki-twoslash.cjs.production.min.js ++++ b/dist/remark-shiki-twoslash.cjs.production.min.js +@@ -1,2 +1,2 @@ +-"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,t=require("@typescript/twoslash"),r=(e=require("unist-util-visit"))&&"object"==typeof e&&"default"in e?e.default:e,n=require("fenceparser"),o=require("shiki"),i=require("shiki-twoslash");function a(e,t,r,n,o,i,a){try{var s=e[i](a),c=s.value}catch(e){return void r(e)}s.done?t(c):Promise.resolve(c).then(n,o)}function s(e){return function(){var t=this,r=arguments;return new Promise((function(n,o){var i=e.apply(t,r);function s(e){a(i,n,o,s,c,"next",e)}function c(e){a(i,n,o,s,c,"throw",e)}s(void 0)}))}}function c(){return(c=Object.assign||function(e){for(var t=1;t=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var s=r.call(i,"catchLoc"),c=r.call(i,"finallyLoc");if(s&&c){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),k(r),l}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;k(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:L(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),l}},e}(e.exports);try{regeneratorRuntime=t}catch(e){Function("r","regeneratorRuntime = r")(t)}}));function f(e){return e.replace(//g,">")}var p=function(e,r,n){var o="undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch,i="undefined"!=typeof jest,a=o||i?function(e){}:console.error,s="
"+n+"
";"object"!=typeof n?(s=String(n),a("### Unexpected error:"),a(n)):n instanceof t.TwoslashError?(s=function(e){return"\n

"+f(e.title)+"

\n

"+f(e.description).replace(/(?:\r\n|\r|\n)/g,"
")+"

\n"+f(e.recommendation).replace(/(?:\r\n|\r|\n)/g,"
")+"
\n"}(n),a("### Twoslash error: "+n.title),a(n.description),a(n.recommendation),a("\n### Code Sample"),a(e)):n instanceof Error&&(s=function(e){return"
"+e.message.split("## Code")[0]+"
"}(n),a("### Unexpected error:"),a(n));var c="\n
Twoslash failure
\n
"+s+"

Raising Code:

"+f(e)+"
";r.type="html",r.value='
'+c+"
",r.children=[]},h=new Map;function d(e,t,r,n,o){var a,s={json5:"json"};if(s[t.lang]&&(t.lang=s[t.lang]),"twoslash"===t.lang){if(!t.meta.include||"string"!=typeof t.meta.include)throw new Error("A twoslash code block needs a pragma like 'twoslash include [name]'");!function(e,t,r){var n=[];r.split("\n").forEach((function(r,o){var i=r.trim();if(i.startsWith("// - ")){var a=i.split("// - ")[1].split(" ")[0];e.set(t+"-"+a,n.join("\n"))}else n.push(r)})),e.set(t,n.join("\n"))}(h,t.meta.include,e),a=o.wrapFragments?'
':""}else a=r.map((function(r){var a=r.customName.split("/").pop().replace(".json","");return i.renderCodeToHTML(e,t.lang,t.meta,c({themeName:a},o),r,n)})).join("\n"),r.length>1&&o.wrapFragments&&(a='
'+a+"
");return a}var v=function(e,t,r){var n=t.lang;if(void 0===r&&(r={}),("undefined"==typeof process||!process.env||!process.env.TWOSLASH_DISABLE)&&t.meta.twoslash)return function(e,t,r){if("undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch)return i.runTwoSlash(e,t,r);var n=require("crypto").createHash,o=require("fs"),a=o.readFileSync,s=o.existsSync,c=o.mkdirSync,u=o.writeFileSync,l=require("path").join,f=require("@typescript/twoslash/package.json").version,p=require("typescript/package.json").version,h=n("sha1").update(e+"-"+f+"-"+p).digest("hex"),d=function(){return __dirname.includes("node_modules")?l(__dirname.split("node_modules")[0],"node_modules",".cache","twoslash"):l(__dirname,"..","..",".cache","twoslash")},v=process.versions.pnp?function(){try{var e=require("pnpapi");return l(e.getPackageInformation(e.topLevel).packageLocation,"node_modules",".cache","twoslash")}catch(e){return d()}}():d(),m=l(v,h+".json");if(s(m))return process.env.debug&&console.log("Using cached twoslash results from "+m),JSON.parse(a(m,"utf8"));var g=i.runTwoSlash(e,t,r);return s(v)||c(v,{recursive:!0}),u(m,JSON.stringify(g),"utf8"),g}(function(e,t){for(var r,n=/\/\/ @include: (.*)$/gm,o=[];null!==(r=n.exec(t));){r.index===n.lastIndex&&n.lastIndex++;var i=r[1],a=e.get(i);if(!a){var s="Could not find an include with the key: '"+i+"'.\nThere is: "+Array.from(e.keys())+".";throw new Error(s)}o.push([r.index,r[0].length,a])}var c=t.toString();return o.reverse().forEach((function(e){c=c.substring(0,e[0])+e[2]+c.substring(e[0]+e[1])})),c}(h,e),n,r)},m=new Map,g=function(e){return Promise.all((e.themes||(e.theme?[e.theme]:["light-plus"])).map(function(){var t=s(l.mark((function t(r){var n,i;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=r.name||r,t.next=3,o.getHighlighter(c({},e,{theme:r,themes:void 0}));case 3:return(i=t.sent).customName=n,t.abrupt("return",i);case 6:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()))},y=function(){return h.clear()},w=function(e){var t,r=n.lex(e),o=r[0],i=r.slice(1);if("twoslash"===o){var a=i.indexOf("include");-1!==a&&i.splice(a+1,0,"=")}var s=null!=(t=n.parse(i))?t:{};return{lang:(o||"").toString(),meta:s}},x=function(e,r){return void 0===r&&(r={}),function(n){var o=n.value,i=void 0;try{i=w([n.lang,n.meta].filter(Boolean).join(" "))}catch(e){var a=new t.TwoslashError("Codefence error","Could not parse the codefence for this code sample","It's usually an unclosed string",o);return p(o,n,a)}if(!(Object.keys(i.meta).filter((function(e){return(r.ignoreCodeblocksWithCodefenceMeta||[]).includes(e)})).length>0)){var s;try{s=n.twoslash||v(o,i,r)}catch(e){var c=process&&process.env&&!!process.env.CI,u="undefined"==typeof jest;if(c&&u||r.alwayRaiseForTwoslashExceptions)throw e;return p(o,n,e)}s&&(n.value=s.code,n.lang=s.extension,n.twoslash=s);var l=d(n.value,i,e,s,r);n.type="html",n.value=l,n.children=[]}}},b=function(){var e=s(l.mark((function e(t){return l.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return void 0===t&&(t={}),y(),m.has(t)||m.set(t,g(t)),e.next=5,m.get(t);case 5:return e.abrupt("return",{settings:t,highlighters:e.sent});case 7:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}();exports.default=function(e){return void 0===e&&(e={}),m.has(e)||m.set(e,g(e)),function(){var t=s(l.mark((function t(n){var o;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,m.get(e);case 2:o=t.sent,y(),r(n,"code",x(o,e));case 5:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()},exports.highlightersFromSettings=g,exports.remarkVisitor=x,exports.runTwoSlashOnNode=v,exports.setupForFile=b,exports.transformAttributesToHTML=function(e,t,r,n){var o=w(t),i=v(e,o,n);return d(i&&i.code||e,o,r,i,n)}; ++"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,t=require("@typescript/twoslash"),r=(e=require("unist-util-visit"))&&"object"==typeof e&&"default"in e?e.default:e,n=require("fenceparser"),o=require("shiki"),i=require("shiki-twoslash");function a(e,t,r,n,o,i,a){try{var s=e[i](a),c=s.value}catch(e){return void r(e)}s.done?t(c):Promise.resolve(c).then(n,o)}function s(e){return function(){var t=this,r=arguments;return new Promise((function(n,o){var i=e.apply(t,r);function s(e){a(i,n,o,s,c,"next",e)}function c(e){a(i,n,o,s,c,"throw",e)}s(void 0)}))}}function c(){return(c=Object.assign||function(e){for(var t=1;t=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var s=r.call(i,"catchLoc"),c=r.call(i,"finallyLoc");if(s&&c){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),k(r),l}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;k(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:L(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),l}},e}(e.exports);try{regeneratorRuntime=t}catch(e){Function("r","regeneratorRuntime = r")(t)}}));function f(e){return e.replace(//g,">")}var p=function(e,r,n){var o="undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch,i="undefined"!=typeof jest,a=o||i?function(e){}:console.error,s="
"+n+"
";"object"!=typeof n?(s=String(n),a("### Unexpected error:"),a(n)):n instanceof t.TwoslashError?(s=function(e){return"\n

"+f(e.title)+"

\n

"+f(e.description).replace(/(?:\r\n|\r|\n)/g,"
")+"

\n"+f(e.recommendation).replace(/(?:\r\n|\r|\n)/g,"
")+"
\n"}(n),a("### Twoslash error: "+n.title),a(n.description),a(n.recommendation),a("\n### Code Sample"),a(e)):n instanceof Error&&(s=function(e){return"
"+e.message.split("## Code")[0]+"
"}(n),a("### Unexpected error:"),a(n));var c="\n
Twoslash failure
\n
"+s+"

Raising Code:

"+f(e)+"
";r.type="html",r.value='
'+c+"
",r.children=[]},h=new Map;function d(e,t,r,n,o){var a,s={json5:"json"};if(s[t.lang]&&(t.lang=s[t.lang]),"twoslash"===t.lang){if(!t.meta.include||"string"!=typeof t.meta.include)throw new Error("A twoslash code block needs a pragma like 'twoslash include [name]'");!function(e,t,r){var n=[];r.split("\n").forEach((function(r,o){var i=r.trim();if(i.startsWith("// - ")){var a=i.split("// - ")[1].split(" ")[0];e.set(t+"-"+a,n.join("\n"))}else n.push(r)})),e.set(t,n.join("\n"))}(h,t.meta.include,e),a=o.wrapFragments?'
':""}else a=r.map((function(r){var a=r.customName.split("/").pop().replace(".json","");return i.renderCodeToHTML(e,t.lang,t.meta,c({themeName:a},o),r,n)})).join("\n"),r.length>1&&o.wrapFragments&&(a='
'+a+"
");return a}var v=function(e,t,r){var n=t.lang;if(void 0===r&&(r={}),("undefined"==typeof process||!process.env||!process.env.TWOSLASH_DISABLE)&&t.meta.twoslash)return function(e,t,r){if("undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch)return i.runTwoSlash(e,t,r);var n=require("crypto").createHash,o=require("fs"),a=o.readFileSync,s=o.existsSync,c=o.mkdirSync,u=o.writeFileSync,l=require("path").join,f=require("@typescript/twoslash/package.json").version,p=require("typescript/package.json").version,h=n("sha1").update(JSON.stringify([e,t,r,f,p,require("@typescript/vfs/package.json").version])).digest("hex"),d=function(){return __dirname.includes("node_modules")?l(__dirname.split("node_modules")[0],"node_modules",".cache","twoslash"):l(__dirname,"..","..",".cache","twoslash")},v=process.versions.pnp?function(){try{var e=require("pnpapi");return l(e.getPackageInformation(e.topLevel).packageLocation,"node_modules",".cache","twoslash")}catch(e){return d()}}():d(),m=l(v,h+".json");if(s(m))return process.env.debug&&console.log("Using cached twoslash results from "+m),JSON.parse(a(m,"utf8"));var g=i.runTwoSlash(e,t,r);return s(v)||c(v,{recursive:!0}),u(m,JSON.stringify(g),"utf8"),g}(function(e,t){for(var r,n=/\/\/ @include: (.*)$/gm,o=[];null!==(r=n.exec(t));){r.index===n.lastIndex&&n.lastIndex++;var i=r[1],a=e.get(i);if(!a){var s="Could not find an include with the key: '"+i+"'.\nThere is: "+Array.from(e.keys())+".";throw new Error(s)}o.push([r.index,r[0].length,a])}var c=t.toString();return o.reverse().forEach((function(e){c=c.substring(0,e[0])+e[2]+c.substring(e[0]+e[1])})),c}(h,e),n,r)},m=new Map,g=function(e){return Promise.all((e.themes||(e.theme?[e.theme]:["light-plus"])).map(function(){var t=s(l.mark((function t(r){var n,i;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=r.name||r,t.next=3,o.getHighlighter(c({},e,{theme:r,themes:void 0}));case 3:return(i=t.sent).customName=n,t.abrupt("return",i);case 6:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()))},y=function(){return h.clear()},w=function(e){var t,r=n.lex(e),o=r[0],i=r.slice(1);if("twoslash"===o){var a=i.indexOf("include");-1!==a&&i.splice(a+1,0,"=")}var s=null!=(t=n.parse(i))?t:{};return{lang:(o||"").toString(),meta:s}},x=function(e,r){return void 0===r&&(r={}),function(n){var o=n.value,i=void 0;try{i=w([n.lang,n.meta].filter(Boolean).join(" "))}catch(e){var a=new t.TwoslashError("Codefence error","Could not parse the codefence for this code sample","It's usually an unclosed string",o);return p(o,n,a)}if(!(Object.keys(i.meta).filter((function(e){return(r.ignoreCodeblocksWithCodefenceMeta||[]).includes(e)})).length>0)){var s;try{s=n.twoslash||v(o,i,r)}catch(e){var c=process&&process.env&&!!process.env.CI,u="undefined"==typeof jest;if(c&&u||r.alwayRaiseForTwoslashExceptions)throw e;return p(o,n,e)}s&&(n.value=s.code,n.lang=s.extension,n.twoslash=s);var l=d(n.value,i,e,s,r);n.type="html",n.value=l,n.children=[]}}},b=function(){var e=s(l.mark((function e(t){return l.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return void 0===t&&(t={}),y(),m.has(t)||m.set(t,g(t)),e.next=5,m.get(t);case 5:return e.abrupt("return",{settings:t,highlighters:e.sent});case 7:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}();exports.default=function(e){return void 0===e&&(e={}),m.has(e)||m.set(e,g(e)),function(){var t=s(l.mark((function t(n){var o;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,m.get(e);case 2:o=t.sent,y(),r(n,"code",x(o,e));case 5:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()},exports.highlightersFromSettings=g,exports.remarkVisitor=x,exports.runTwoSlashOnNode=v,exports.setupForFile=b,exports.transformAttributesToHTML=function(e,t,r,n){var o=w(t),i=v(e,o,n);return d(i&&i.code||e,o,r,i,n)}; + //# sourceMappingURL=remark-shiki-twoslash.cjs.production.min.js.map +diff --git a/dist/remark-shiki-twoslash.esm.js b/dist/remark-shiki-twoslash.esm.js +index 7d005c0ae14fff368e1808ffae3ecef5dab756b9..ea4496065aa297d9cf1aefc0b8d24eba6b985b5a 100644 +--- a/dist/remark-shiki-twoslash.esm.js ++++ b/dist/remark-shiki-twoslash.esm.js +@@ -819,7 +819,7 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { + var tsVersion = require('typescript/package.json').version; + + var shasum = createHash("sha1"); +- var codeSha = shasum.update(code + "-" + shikiVersion + "-" + tsVersion).digest("hex"); ++ var codeSha = shasum.update(JSON.stringify([code, lang, settings, shikiVersion, tsVersion, require('@typescript/vfs/package.json').version])).digest("hex"); + + var getNmCache = function getNmCache() { + if (__dirname.includes("node_modules")) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6d60946b028..e03eed948ac6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ patchedDependencies: gatsby-remark-shiki-twoslash@3.0.38: hash: 3aa0dc2d9c0fd3921986acbadfef7a10df948fb30d02590f86cecab9b91800bf path: patches/gatsby-remark-shiki-twoslash@3.0.38.patch + remark-shiki-twoslash@3.1.3: + hash: c293ebf6939b77de68ccb4cc5d7120b789a307c28f00abb9b3e7ea6bb4a64767 + path: patches/remark-shiki-twoslash@3.1.3.patch importers: @@ -81,7 +84,7 @@ importers: version: 13.0.2 remark-shiki-twoslash: specifier: ^3.1.3 - version: 3.1.3(typescript@6.0.2) + version: 3.1.3(patch_hash=c293ebf6939b77de68ccb4cc5d7120b789a307c28f00abb9b3e7ea6bb4a64767)(typescript@6.0.2) packages/ata: dependencies: @@ -16340,7 +16343,7 @@ snapshots: gatsby-remark-shiki-twoslash@3.0.38(patch_hash=3aa0dc2d9c0fd3921986acbadfef7a10df948fb30d02590f86cecab9b91800bf): dependencies: - remark-shiki-twoslash: 3.1.3(typescript@6.0.2) + remark-shiki-twoslash: 3.1.3(patch_hash=c293ebf6939b77de68ccb4cc5d7120b789a307c28f00abb9b3e7ea6bb4a64767)(typescript@6.0.2) typescript: 6.0.2 unist-util-visit: 2.0.3 @@ -20181,7 +20184,7 @@ snapshots: dependencies: mdast-util-to-nlcst: 4.0.1 - remark-shiki-twoslash@3.1.3(typescript@6.0.2): + remark-shiki-twoslash@3.1.3(patch_hash=c293ebf6939b77de68ccb4cc5d7120b789a307c28f00abb9b3e7ea6bb4a64767)(typescript@6.0.2): dependencies: '@types/unist': 2.0.11 '@typescript/twoslash': link:packages/ts-twoslasher @@ -21433,7 +21436,7 @@ snapshots: hast-util-to-html: 7.1.3 mdast-util-to-hast: 10.2.0 remark: 13.0.0 - remark-shiki-twoslash: 3.1.3(typescript@6.0.2) + remark-shiki-twoslash: 3.1.3(patch_hash=c293ebf6939b77de68ccb4cc5d7120b789a307c28f00abb9b3e7ea6bb4a64767)(typescript@6.0.2) unist-util-visit: 3.1.0 transitivePeerDependencies: - supports-color From 41e4d556e81967e49edbf585d703db998c441a53 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:10:42 -0700 Subject: [PATCH 05/14] Reuse dependency and compiler caches in website workflows Production Gatsby builds spend most of their time compiling examples and recreating Webpack bundles. Reuse that work across compatible builds without restoring Gatsby's page-query cache or generated pages. Keep translations, metadata, and playground cache busting fresh. A fully localized local site build took 253 seconds without caches and 31 seconds restoring only Twoslash and Webpack work, including the production cache-busting step. All 920 page queries still run. Separate build steps also make future Actions bottlenecks visible. --- .github/actions/website-cache/action.yml | 19 ++++++++++++++ .github/workflows/CI.yml | 8 ++++-- .github/workflows/deploy-prod-static.yml | 32 ++++++++++++++---------- README.md | 7 ++++++ 4 files changed, 51 insertions(+), 15 deletions(-) create mode 100644 .github/actions/website-cache/action.yml diff --git a/.github/actions/website-cache/action.yml b/.github/actions/website-cache/action.yml new file mode 100644 index 000000000000..39994a465908 --- /dev/null +++ b/.github/actions/website-cache/action.yml @@ -0,0 +1,19 @@ +name: Cache website build work +description: Reuse compiled examples and Webpack work without caching generated pages + +runs: + using: composite + steps: + - name: Get Node version + id: node + shell: bash + run: echo "version=$(node --version)" >> "$GITHUB_OUTPUT" + + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + node_modules/.cache/twoslash + packages/typescriptlang-org/.cache/webpack + key: website-${{ runner.os }}-${{ steps.node.outputs.version }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'patches/**', 'packages/*/package.json', 'packages/*/tsconfig*.json', 'packages/*/src/**', 'packages/*/lib/**', 'packages/*/scripts/**', 'packages/*/script/**', 'packages/*/data/**', 'packages/typescriptlang-org/gatsby-*.js', '.github/actions/website-cache/action.yml') }}-${{ github.sha }} + restore-keys: | + website-${{ runner.os }}-${{ steps.node.outputs.version }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'patches/**', 'packages/*/package.json', 'packages/*/tsconfig*.json', 'packages/*/src/**', 'packages/*/lib/**', 'packages/*/scripts/**', 'packages/*/script/**', 'packages/*/data/**', 'packages/typescriptlang-org/gatsby-*.js', '.github/actions/website-cache/action.yml') }}- diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3f96db052e2d..98642502e59f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -24,12 +24,13 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "20.x" - - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + cache: pnpm - - run: pnpm install + - run: pnpm install --frozen-lockfile # Grab localizations - run: pnpm docs-sync pull microsoft/TypeScript-Website-localizations#main 1 @@ -37,6 +38,9 @@ jobs: # Build the packages - run: pnpm bootstrap + - name: Restore website build caches + uses: ./.github/actions/website-cache + # Verify it compiles - run: pnpm build-site diff --git a/.github/workflows/deploy-prod-static.yml b/.github/workflows/deploy-prod-static.yml index 8bbdcb00097f..a9dc51b078b7 100644 --- a/.github/workflows/deploy-prod-static.yml +++ b/.github/workflows/deploy-prod-static.yml @@ -31,24 +31,30 @@ jobs: with: # Fetch the full history, to build attribution.json fetch-depth: 0 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "20.x" + cache: pnpm - - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Pull localizations + run: pnpm docs-sync pull microsoft/TypeScript-Website-localizations#main 1 + + - name: Build packages + run: pnpm bootstrap + + - name: Restore website build caches + uses: ./.github/actions/website-cache - # Builds the modules, and boostraps the other modules - - name: Build website - run: | - pnpm install - pnpm docs-sync pull microsoft/TypeScript-Website-localizations#main 1 - pnpm bootstrap - pnpm run --filter=typescriptlang-org setup-playground-cache-bust + - name: Set up playground cache busting + run: pnpm run --filter=typescriptlang-org setup-playground-cache-bust - - name: Makes the site - run: | - pnpm build-site - cp -r packages/typescriptlang-org/public site + - name: Build static site + run: pnpm build-site - name: Setup Pages uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 @@ -56,7 +62,7 @@ jobs: - name: Upload artifact uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v3 with: - path: './site' + path: packages/typescriptlang-org/public deploy: environment: diff --git a/README.md b/README.md index 6ceb686530cb..a7fdbffc05bc 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,13 @@ You can find the build logs in [GitHub Actions](https://github.com/microsoft/Typ `pnpm bootstrap` already builds the workspace packages; it does not need to be followed by `pnpm build`. Run `pnpm build-site` afterward to build the static site. +CI and production deployment cache the pnpm store, compiled Twoslash examples, +and Webpack compilations. They always pull current translations, regenerate +metadata, and build every static page. Generated pages and Gatsby's page-query +cache are not restored. Build caches are isolated by OS and Node version and +invalidated when dependencies, build configuration, or workspace code change; +Twoslash entries also distinguish code, language, and compiler settings. + ## Docs If you want to know _in-depth_ how this website works, there is an [hour long video covering the codebase, deployment and tooling on YouTube.](https://www.youtube.com/watch?v=HOvivt6B7hE). Otherwise there are some short guides: From f79440d52e6b59b71e1bc440a899699bdc545bcf Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:33:32 -0700 Subject: [PATCH 06/14] Leave build documentation unchanged --- README.md | 10 ---------- docs/Setup Troubleshooting.md | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/README.md b/README.md index a7fdbffc05bc..2fbe85f3a480 100644 --- a/README.md +++ b/README.md @@ -42,16 +42,6 @@ Deployment is automatic: You can find the build logs in [GitHub Actions](https://github.com/microsoft/TypeScript-Website/actions) -`pnpm bootstrap` already builds the workspace packages; it does not need to be -followed by `pnpm build`. Run `pnpm build-site` afterward to build the static site. - -CI and production deployment cache the pnpm store, compiled Twoslash examples, -and Webpack compilations. They always pull current translations, regenerate -metadata, and build every static page. Generated pages and Gatsby's page-query -cache are not restored. Build caches are isolated by OS and Node version and -invalidated when dependencies, build configuration, or workspace code change; -Twoslash entries also distinguish code, language, and compiler settings. - ## Docs If you want to know _in-depth_ how this website works, there is an [hour long video covering the codebase, deployment and tooling on YouTube.](https://www.youtube.com/watch?v=HOvivt6B7hE). Otherwise there are some short guides: diff --git a/docs/Setup Troubleshooting.md b/docs/Setup Troubleshooting.md index e51aa84016ab..510336bc8a78 100644 --- a/docs/Setup Troubleshooting.md +++ b/docs/Setup Troubleshooting.md @@ -2,7 +2,7 @@ #### Module `x` is not built -Sometimes, and it's not been tracked down exactly, some dependencies of the site aren't built even though it says they are. In those cases, re-run `pnpm bootstrap` to re-build all the internal site deps. +Sometimes, and it's not been tracked down exactly, some dependencies of the site aren't built even though it says they are. In those cases, re-run `pnpm bootstrap` and `pnpm build` to re-build all the internal site deps. #### Windows + Watchman From 9b9508e19a73907934bf5ecae8ddb008365daad1 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:56:38 -0700 Subject: [PATCH 07/14] Bound saved Twoslash entries with LRU pruning Restored snapshots carry obsolete examples forward indefinitely unless entries are explicitly evicted. Record usage on successful cache reads and prune the least-recently-used entries to a 64 MiB budget before saving, preserving usage timestamps across archive roundtrips. --- .../website-cache/prune-twoslash-cache.js | 51 +++++++ .../lib/bootup/shiki.test.js | 135 ++++++++++++++++++ patches/remark-shiki-twoslash@3.1.3.patch | 32 ++++- pnpm-lock.yaml | 10 +- 4 files changed, 219 insertions(+), 9 deletions(-) create mode 100644 .github/actions/website-cache/prune-twoslash-cache.js diff --git a/.github/actions/website-cache/prune-twoslash-cache.js b/.github/actions/website-cache/prune-twoslash-cache.js new file mode 100644 index 000000000000..0daa6ed972b1 --- /dev/null +++ b/.github/actions/website-cache/prune-twoslash-cache.js @@ -0,0 +1,51 @@ +const fs = require("fs") +const path = require("path") + +const MAX_BYTES = 64 * 1024 * 1024 + +function pruneTwoslashCache(directory) { + let entries + try { + if (!fs.lstatSync(directory).isDirectory()) { + throw new Error(`Expected a Twoslash cache directory: ${directory}`) + } + entries = fs.readdirSync(directory, { withFileTypes: true }) + } catch (error) { + if (error.code !== "ENOENT") throw error + return { bytes: 0, removed: 0, removedBytes: 0 } + } + + const files = entries.map(entry => { + if (!entry.isFile() || !/^[a-f0-9]{40}\.json$/.test(entry.name)) { + throw new Error(`Unexpected Twoslash cache entry: ${entry.name}`) + } + const filename = path.join(directory, entry.name) + const { size, mtimeMs } = fs.statSync(filename) + return { filename, size, mtimeMs } + }) + files.sort((a, b) => a.mtimeMs - b.mtimeMs || a.filename.localeCompare(b.filename)) + + let bytes = files.reduce((total, file) => total + file.size, 0) + let removed = 0 + let removedBytes = 0 + for (const file of files) { + if (bytes <= MAX_BYTES) break + fs.unlinkSync(file.filename) + bytes -= file.size + removedBytes += file.size + removed++ + } + return { bytes, removed, removedBytes } +} + +module.exports = { MAX_BYTES, pruneTwoslashCache } + +if (require.main === module) { + const directory = path.resolve(__dirname, "../../../node_modules/.cache/twoslash") + const { bytes, removed, removedBytes } = pruneTwoslashCache(directory) + console.log( + `Twoslash cache: ${(bytes / 1024 / 1024).toFixed(2)} MiB retained; ` + + `removed ${removed} entries (${(removedBytes / 1024 / 1024).toFixed(2)} MiB). ` + + `Limit: ${MAX_BYTES / 1024 / 1024} MiB.` + ) +} diff --git a/packages/typescriptlang-org/lib/bootup/shiki.test.js b/packages/typescriptlang-org/lib/bootup/shiki.test.js index be86891c6443..e468d80deb5e 100644 --- a/packages/typescriptlang-org/lib/bootup/shiki.test.js +++ b/packages/typescriptlang-org/lib/bootup/shiki.test.js @@ -110,6 +110,7 @@ describe("persistent Twoslash cache", () => { const existsSync = fs.existsSync const readFileSync = fs.readFileSync const writeFileSync = fs.writeFileSync + const utimesSync = fs.utimesSync const isCache = path => typeof path === "string" && (path.includes(".cache/twoslash") || path.includes(".cache\\twoslash")) @@ -135,6 +136,9 @@ describe("persistent Twoslash cache", () => { if (isCache(path)) files.set(path, data) else writeFileSync(path, data, ...args) }) + jest.spyOn(fs, "utimesSync").mockImplementation((path, ...args) => { + if (!isCache(path)) return utimesSync(path, ...args) + }) runTwoSlash = jest .spyOn(shikiTwoslash, "runTwoSlash") .mockImplementation((code, lang, settings) => ({ code, lang, settings })) @@ -147,6 +151,12 @@ describe("persistent Twoslash cache", () => { const second = runTwoSlashOnNode("const value = 1", fence("ts"), {}) expect(second).toEqual(first) expect(runTwoSlash).toHaveBeenCalledTimes(1) + expect(fs.utimesSync).toHaveBeenCalledTimes(1) + expect(fs.utimesSync).toHaveBeenCalledWith( + expect.any(String), + expect.any(Date), + expect.any(Date) + ) }) it("distinguishes TypeScript from JavaScript", () => { @@ -176,4 +186,129 @@ describe("persistent Twoslash cache", () => { runTwoSlashOnNode("bad code", fence("ts"), {}) expect(runTwoSlash).toHaveBeenCalledTimes(2) }) + + it("does not hide errors recording cache usage", () => { + runTwoSlashOnNode("const value = 1", fence("ts"), {}) + fs.utimesSync.mockImplementationOnce(() => { + throw new Error("access time update failed") + }) + expect(() => runTwoSlashOnNode("const value = 1", fence("ts"), {})).toThrow( + "access time update failed" + ) + }) +}) + +describe("Twoslash LRU pruning", () => { + const fs = require("fs") + const os = require("os") + const path = require("path") + const { + MAX_BYTES, + pruneTwoslashCache, + } = require("../../../../.github/actions/website-cache/prune-twoslash-cache") + let directory + + beforeEach(() => { + directory = fs.mkdtempSync( + path.join(os.tmpdir(), "website-twoslash-cache-") + ) + }) + + afterEach(() => { + jest.restoreAllMocks() + fs.rmSync(directory, { recursive: true, force: true }) + }) + + const entry = (id, bytes, accessed) => { + const filename = path.join( + directory, + id.toString(16).padStart(40, "0") + ".json" + ) + fs.writeFileSync(filename, "") + fs.truncateSync(filename, bytes) + fs.utimesSync(filename, accessed, accessed) + return filename + } + + it("keeps an empty or absent cache", () => { + const empty = { bytes: 0, removed: 0, removedBytes: 0 } + expect(pruneTwoslashCache(directory)).toEqual(empty) + expect(pruneTwoslashCache(path.join(directory, "missing"))).toEqual(empty) + }) + + it("retains exactly 64 MiB without pruning", () => { + const file = entry(1, MAX_BYTES, 1) + expect(pruneTwoslashCache(directory)).toEqual({ + bytes: MAX_BYTES, + removed: 0, + removedBytes: 0, + }) + expect(fs.existsSync(file)).toBe(true) + }) + + it("evicts least-recently-used entries rather than oldest creations", () => { + const oldest = entry(1, MAX_BYTES / 2, 1) + const middle = entry(2, MAX_BYTES / 2, 2) + const newest = entry(3, MAX_BYTES / 2, 3) + fs.utimesSync(oldest, 4, 4) + expect(pruneTwoslashCache(directory)).toEqual({ + bytes: MAX_BYTES, + removed: 1, + removedBytes: MAX_BYTES / 2, + }) + expect(fs.existsSync(oldest)).toBe(true) + expect(fs.existsSync(middle)).toBe(false) + expect(fs.existsSync(newest)).toBe(true) + }) + + it("evicts as many entries as needed and drops oversized entries", () => { + const oldest = entry(1, MAX_BYTES, 1) + const middle = entry(2, MAX_BYTES, 2) + const newest = entry(3, MAX_BYTES + 1, 3) + expect(pruneTwoslashCache(directory)).toEqual({ + bytes: 0, + removed: 3, + removedBytes: 3 * MAX_BYTES + 1, + }) + expect([oldest, middle, newest].some(file => fs.existsSync(file))).toBe( + false + ) + }) + + it("breaks timestamp ties deterministically", () => { + const first = entry(1, MAX_BYTES / 2, 1) + const second = entry(2, MAX_BYTES / 2, 1) + const third = entry(3, MAX_BYTES / 2, 1) + pruneTwoslashCache(directory) + expect(fs.existsSync(first)).toBe(false) + expect(fs.existsSync(second)).toBe(true) + expect(fs.existsSync(third)).toBe(true) + }) + + it("preserves usage timestamps across repeated pruning", () => { + const first = entry(1, MAX_BYTES / 2, 1) + entry(2, MAX_BYTES / 2, 2) + pruneTwoslashCache(directory) + expect(fs.statSync(first).mtimeMs).toBe(1000) + entry(3, MAX_BYTES / 2, 3) + pruneTwoslashCache(directory) + expect(fs.existsSync(first)).toBe(false) + }) + + it("rejects unexpected entries instead of deleting unrelated files", () => { + const filename = path.join(directory, "unexpected.txt") + fs.writeFileSync(filename, "preserve") + expect(() => pruneTwoslashCache(directory)).toThrow( + "Unexpected Twoslash cache entry" + ) + expect(fs.readFileSync(filename, "utf8")).toBe("preserve") + }) + + it("surfaces filesystem failures", () => { + entry(1, MAX_BYTES + 1, 1) + jest.spyOn(fs, "unlinkSync").mockImplementationOnce(() => { + throw new Error("cannot prune cache") + }) + expect(() => pruneTwoslashCache(directory)).toThrow("cannot prune cache") + }) }) diff --git a/patches/remark-shiki-twoslash@3.1.3.patch b/patches/remark-shiki-twoslash@3.1.3.patch index 39865d603cc9..16e7e0012f82 100644 --- a/patches/remark-shiki-twoslash@3.1.3.patch +++ b/patches/remark-shiki-twoslash@3.1.3.patch @@ -1,5 +1,5 @@ diff --git a/dist/remark-shiki-twoslash.cjs.development.js b/dist/remark-shiki-twoslash.cjs.development.js -index efaed7711553c44c31abcaf53abab08b8780b248..dab4a3abc11b39e4a2a6361d47b2259eef722ed2 100644 +index efaed7711553c44c31abcaf53abab08b8780b248..8235d8f05641cd04e8d3ba9bef5a9ba4af03a605 100644 --- a/dist/remark-shiki-twoslash.cjs.development.js +++ b/dist/remark-shiki-twoslash.cjs.development.js @@ -825,7 +825,7 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { @@ -11,16 +11,28 @@ index efaed7711553c44c31abcaf53abab08b8780b248..dab4a3abc11b39e4a2a6361d47b2259e var getNmCache = function getNmCache() { if (__dirname.includes("node_modules")) { +@@ -850,7 +850,10 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { + + if (existsSync(cachePath)) { + if (process.env.debug) console.log("Using cached twoslash results from " + cachePath); +- return JSON.parse(readFileSync(cachePath, "utf8")); ++ var results = JSON.parse(readFileSync(cachePath, "utf8")); ++ var now = new Date(); ++ require("fs").utimesSync(cachePath, now, now); ++ return results; + } else { + var results = shikiTwoslash.runTwoSlash(code, lang, settings); + if (!existsSync(cacheRoot)) mkdirSync(cacheRoot, { diff --git a/dist/remark-shiki-twoslash.cjs.production.min.js b/dist/remark-shiki-twoslash.cjs.production.min.js -index 9ee26741d08d555a0ef3c0e0917ef0d6342020a5..2a8689e22c1b2aafdb1509f266a44ecf16899447 100644 +index 9ee26741d08d555a0ef3c0e0917ef0d6342020a5..c0aa7edf0323c4047c9167afcc51412d646efc64 100644 --- a/dist/remark-shiki-twoslash.cjs.production.min.js +++ b/dist/remark-shiki-twoslash.cjs.production.min.js @@ -1,2 +1,2 @@ -"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,t=require("@typescript/twoslash"),r=(e=require("unist-util-visit"))&&"object"==typeof e&&"default"in e?e.default:e,n=require("fenceparser"),o=require("shiki"),i=require("shiki-twoslash");function a(e,t,r,n,o,i,a){try{var s=e[i](a),c=s.value}catch(e){return void r(e)}s.done?t(c):Promise.resolve(c).then(n,o)}function s(e){return function(){var t=this,r=arguments;return new Promise((function(n,o){var i=e.apply(t,r);function s(e){a(i,n,o,s,c,"next",e)}function c(e){a(i,n,o,s,c,"throw",e)}s(void 0)}))}}function c(){return(c=Object.assign||function(e){for(var t=1;t=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var s=r.call(i,"catchLoc"),c=r.call(i,"finallyLoc");if(s&&c){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),k(r),l}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;k(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:L(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),l}},e}(e.exports);try{regeneratorRuntime=t}catch(e){Function("r","regeneratorRuntime = r")(t)}}));function f(e){return e.replace(//g,">")}var p=function(e,r,n){var o="undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch,i="undefined"!=typeof jest,a=o||i?function(e){}:console.error,s="
"+n+"
";"object"!=typeof n?(s=String(n),a("### Unexpected error:"),a(n)):n instanceof t.TwoslashError?(s=function(e){return"\n

"+f(e.title)+"

\n

"+f(e.description).replace(/(?:\r\n|\r|\n)/g,"
")+"

\n"+f(e.recommendation).replace(/(?:\r\n|\r|\n)/g,"
")+"
\n"}(n),a("### Twoslash error: "+n.title),a(n.description),a(n.recommendation),a("\n### Code Sample"),a(e)):n instanceof Error&&(s=function(e){return"
"+e.message.split("## Code")[0]+"
"}(n),a("### Unexpected error:"),a(n));var c="\n
Twoslash failure
\n
"+s+"

Raising Code:

"+f(e)+"
";r.type="html",r.value='
'+c+"
",r.children=[]},h=new Map;function d(e,t,r,n,o){var a,s={json5:"json"};if(s[t.lang]&&(t.lang=s[t.lang]),"twoslash"===t.lang){if(!t.meta.include||"string"!=typeof t.meta.include)throw new Error("A twoslash code block needs a pragma like 'twoslash include [name]'");!function(e,t,r){var n=[];r.split("\n").forEach((function(r,o){var i=r.trim();if(i.startsWith("// - ")){var a=i.split("// - ")[1].split(" ")[0];e.set(t+"-"+a,n.join("\n"))}else n.push(r)})),e.set(t,n.join("\n"))}(h,t.meta.include,e),a=o.wrapFragments?'
':""}else a=r.map((function(r){var a=r.customName.split("/").pop().replace(".json","");return i.renderCodeToHTML(e,t.lang,t.meta,c({themeName:a},o),r,n)})).join("\n"),r.length>1&&o.wrapFragments&&(a='
'+a+"
");return a}var v=function(e,t,r){var n=t.lang;if(void 0===r&&(r={}),("undefined"==typeof process||!process.env||!process.env.TWOSLASH_DISABLE)&&t.meta.twoslash)return function(e,t,r){if("undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch)return i.runTwoSlash(e,t,r);var n=require("crypto").createHash,o=require("fs"),a=o.readFileSync,s=o.existsSync,c=o.mkdirSync,u=o.writeFileSync,l=require("path").join,f=require("@typescript/twoslash/package.json").version,p=require("typescript/package.json").version,h=n("sha1").update(e+"-"+f+"-"+p).digest("hex"),d=function(){return __dirname.includes("node_modules")?l(__dirname.split("node_modules")[0],"node_modules",".cache","twoslash"):l(__dirname,"..","..",".cache","twoslash")},v=process.versions.pnp?function(){try{var e=require("pnpapi");return l(e.getPackageInformation(e.topLevel).packageLocation,"node_modules",".cache","twoslash")}catch(e){return d()}}():d(),m=l(v,h+".json");if(s(m))return process.env.debug&&console.log("Using cached twoslash results from "+m),JSON.parse(a(m,"utf8"));var g=i.runTwoSlash(e,t,r);return s(v)||c(v,{recursive:!0}),u(m,JSON.stringify(g),"utf8"),g}(function(e,t){for(var r,n=/\/\/ @include: (.*)$/gm,o=[];null!==(r=n.exec(t));){r.index===n.lastIndex&&n.lastIndex++;var i=r[1],a=e.get(i);if(!a){var s="Could not find an include with the key: '"+i+"'.\nThere is: "+Array.from(e.keys())+".";throw new Error(s)}o.push([r.index,r[0].length,a])}var c=t.toString();return o.reverse().forEach((function(e){c=c.substring(0,e[0])+e[2]+c.substring(e[0]+e[1])})),c}(h,e),n,r)},m=new Map,g=function(e){return Promise.all((e.themes||(e.theme?[e.theme]:["light-plus"])).map(function(){var t=s(l.mark((function t(r){var n,i;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=r.name||r,t.next=3,o.getHighlighter(c({},e,{theme:r,themes:void 0}));case 3:return(i=t.sent).customName=n,t.abrupt("return",i);case 6:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()))},y=function(){return h.clear()},w=function(e){var t,r=n.lex(e),o=r[0],i=r.slice(1);if("twoslash"===o){var a=i.indexOf("include");-1!==a&&i.splice(a+1,0,"=")}var s=null!=(t=n.parse(i))?t:{};return{lang:(o||"").toString(),meta:s}},x=function(e,r){return void 0===r&&(r={}),function(n){var o=n.value,i=void 0;try{i=w([n.lang,n.meta].filter(Boolean).join(" "))}catch(e){var a=new t.TwoslashError("Codefence error","Could not parse the codefence for this code sample","It's usually an unclosed string",o);return p(o,n,a)}if(!(Object.keys(i.meta).filter((function(e){return(r.ignoreCodeblocksWithCodefenceMeta||[]).includes(e)})).length>0)){var s;try{s=n.twoslash||v(o,i,r)}catch(e){var c=process&&process.env&&!!process.env.CI,u="undefined"==typeof jest;if(c&&u||r.alwayRaiseForTwoslashExceptions)throw e;return p(o,n,e)}s&&(n.value=s.code,n.lang=s.extension,n.twoslash=s);var l=d(n.value,i,e,s,r);n.type="html",n.value=l,n.children=[]}}},b=function(){var e=s(l.mark((function e(t){return l.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return void 0===t&&(t={}),y(),m.has(t)||m.set(t,g(t)),e.next=5,m.get(t);case 5:return e.abrupt("return",{settings:t,highlighters:e.sent});case 7:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}();exports.default=function(e){return void 0===e&&(e={}),m.has(e)||m.set(e,g(e)),function(){var t=s(l.mark((function t(n){var o;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,m.get(e);case 2:o=t.sent,y(),r(n,"code",x(o,e));case 5:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()},exports.highlightersFromSettings=g,exports.remarkVisitor=x,exports.runTwoSlashOnNode=v,exports.setupForFile=b,exports.transformAttributesToHTML=function(e,t,r,n){var o=w(t),i=v(e,o,n);return d(i&&i.code||e,o,r,i,n)}; -+"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,t=require("@typescript/twoslash"),r=(e=require("unist-util-visit"))&&"object"==typeof e&&"default"in e?e.default:e,n=require("fenceparser"),o=require("shiki"),i=require("shiki-twoslash");function a(e,t,r,n,o,i,a){try{var s=e[i](a),c=s.value}catch(e){return void r(e)}s.done?t(c):Promise.resolve(c).then(n,o)}function s(e){return function(){var t=this,r=arguments;return new Promise((function(n,o){var i=e.apply(t,r);function s(e){a(i,n,o,s,c,"next",e)}function c(e){a(i,n,o,s,c,"throw",e)}s(void 0)}))}}function c(){return(c=Object.assign||function(e){for(var t=1;t=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var s=r.call(i,"catchLoc"),c=r.call(i,"finallyLoc");if(s&&c){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),k(r),l}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;k(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:L(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),l}},e}(e.exports);try{regeneratorRuntime=t}catch(e){Function("r","regeneratorRuntime = r")(t)}}));function f(e){return e.replace(//g,">")}var p=function(e,r,n){var o="undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch,i="undefined"!=typeof jest,a=o||i?function(e){}:console.error,s="
"+n+"
";"object"!=typeof n?(s=String(n),a("### Unexpected error:"),a(n)):n instanceof t.TwoslashError?(s=function(e){return"\n

"+f(e.title)+"

\n

"+f(e.description).replace(/(?:\r\n|\r|\n)/g,"
")+"

\n"+f(e.recommendation).replace(/(?:\r\n|\r|\n)/g,"
")+"
\n"}(n),a("### Twoslash error: "+n.title),a(n.description),a(n.recommendation),a("\n### Code Sample"),a(e)):n instanceof Error&&(s=function(e){return"
"+e.message.split("## Code")[0]+"
"}(n),a("### Unexpected error:"),a(n));var c="\n
Twoslash failure
\n
"+s+"

Raising Code:

"+f(e)+"
";r.type="html",r.value='
'+c+"
",r.children=[]},h=new Map;function d(e,t,r,n,o){var a,s={json5:"json"};if(s[t.lang]&&(t.lang=s[t.lang]),"twoslash"===t.lang){if(!t.meta.include||"string"!=typeof t.meta.include)throw new Error("A twoslash code block needs a pragma like 'twoslash include [name]'");!function(e,t,r){var n=[];r.split("\n").forEach((function(r,o){var i=r.trim();if(i.startsWith("// - ")){var a=i.split("// - ")[1].split(" ")[0];e.set(t+"-"+a,n.join("\n"))}else n.push(r)})),e.set(t,n.join("\n"))}(h,t.meta.include,e),a=o.wrapFragments?'
':""}else a=r.map((function(r){var a=r.customName.split("/").pop().replace(".json","");return i.renderCodeToHTML(e,t.lang,t.meta,c({themeName:a},o),r,n)})).join("\n"),r.length>1&&o.wrapFragments&&(a='
'+a+"
");return a}var v=function(e,t,r){var n=t.lang;if(void 0===r&&(r={}),("undefined"==typeof process||!process.env||!process.env.TWOSLASH_DISABLE)&&t.meta.twoslash)return function(e,t,r){if("undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch)return i.runTwoSlash(e,t,r);var n=require("crypto").createHash,o=require("fs"),a=o.readFileSync,s=o.existsSync,c=o.mkdirSync,u=o.writeFileSync,l=require("path").join,f=require("@typescript/twoslash/package.json").version,p=require("typescript/package.json").version,h=n("sha1").update(JSON.stringify([e,t,r,f,p,require("@typescript/vfs/package.json").version])).digest("hex"),d=function(){return __dirname.includes("node_modules")?l(__dirname.split("node_modules")[0],"node_modules",".cache","twoslash"):l(__dirname,"..","..",".cache","twoslash")},v=process.versions.pnp?function(){try{var e=require("pnpapi");return l(e.getPackageInformation(e.topLevel).packageLocation,"node_modules",".cache","twoslash")}catch(e){return d()}}():d(),m=l(v,h+".json");if(s(m))return process.env.debug&&console.log("Using cached twoslash results from "+m),JSON.parse(a(m,"utf8"));var g=i.runTwoSlash(e,t,r);return s(v)||c(v,{recursive:!0}),u(m,JSON.stringify(g),"utf8"),g}(function(e,t){for(var r,n=/\/\/ @include: (.*)$/gm,o=[];null!==(r=n.exec(t));){r.index===n.lastIndex&&n.lastIndex++;var i=r[1],a=e.get(i);if(!a){var s="Could not find an include with the key: '"+i+"'.\nThere is: "+Array.from(e.keys())+".";throw new Error(s)}o.push([r.index,r[0].length,a])}var c=t.toString();return o.reverse().forEach((function(e){c=c.substring(0,e[0])+e[2]+c.substring(e[0]+e[1])})),c}(h,e),n,r)},m=new Map,g=function(e){return Promise.all((e.themes||(e.theme?[e.theme]:["light-plus"])).map(function(){var t=s(l.mark((function t(r){var n,i;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=r.name||r,t.next=3,o.getHighlighter(c({},e,{theme:r,themes:void 0}));case 3:return(i=t.sent).customName=n,t.abrupt("return",i);case 6:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()))},y=function(){return h.clear()},w=function(e){var t,r=n.lex(e),o=r[0],i=r.slice(1);if("twoslash"===o){var a=i.indexOf("include");-1!==a&&i.splice(a+1,0,"=")}var s=null!=(t=n.parse(i))?t:{};return{lang:(o||"").toString(),meta:s}},x=function(e,r){return void 0===r&&(r={}),function(n){var o=n.value,i=void 0;try{i=w([n.lang,n.meta].filter(Boolean).join(" "))}catch(e){var a=new t.TwoslashError("Codefence error","Could not parse the codefence for this code sample","It's usually an unclosed string",o);return p(o,n,a)}if(!(Object.keys(i.meta).filter((function(e){return(r.ignoreCodeblocksWithCodefenceMeta||[]).includes(e)})).length>0)){var s;try{s=n.twoslash||v(o,i,r)}catch(e){var c=process&&process.env&&!!process.env.CI,u="undefined"==typeof jest;if(c&&u||r.alwayRaiseForTwoslashExceptions)throw e;return p(o,n,e)}s&&(n.value=s.code,n.lang=s.extension,n.twoslash=s);var l=d(n.value,i,e,s,r);n.type="html",n.value=l,n.children=[]}}},b=function(){var e=s(l.mark((function e(t){return l.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return void 0===t&&(t={}),y(),m.has(t)||m.set(t,g(t)),e.next=5,m.get(t);case 5:return e.abrupt("return",{settings:t,highlighters:e.sent});case 7:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}();exports.default=function(e){return void 0===e&&(e={}),m.has(e)||m.set(e,g(e)),function(){var t=s(l.mark((function t(n){var o;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,m.get(e);case 2:o=t.sent,y(),r(n,"code",x(o,e));case 5:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()},exports.highlightersFromSettings=g,exports.remarkVisitor=x,exports.runTwoSlashOnNode=v,exports.setupForFile=b,exports.transformAttributesToHTML=function(e,t,r,n){var o=w(t),i=v(e,o,n);return d(i&&i.code||e,o,r,i,n)}; ++"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,t=require("@typescript/twoslash"),r=(e=require("unist-util-visit"))&&"object"==typeof e&&"default"in e?e.default:e,n=require("fenceparser"),o=require("shiki"),i=require("shiki-twoslash");function a(e,t,r,n,o,i,a){try{var s=e[i](a),c=s.value}catch(e){return void r(e)}s.done?t(c):Promise.resolve(c).then(n,o)}function s(e){return function(){var t=this,r=arguments;return new Promise((function(n,o){var i=e.apply(t,r);function s(e){a(i,n,o,s,c,"next",e)}function c(e){a(i,n,o,s,c,"throw",e)}s(void 0)}))}}function c(){return(c=Object.assign||function(e){for(var t=1;t=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var s=r.call(i,"catchLoc"),c=r.call(i,"finallyLoc");if(s&&c){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),k(r),l}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;k(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:L(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),l}},e}(e.exports);try{regeneratorRuntime=t}catch(e){Function("r","regeneratorRuntime = r")(t)}}));function f(e){return e.replace(//g,">")}var p=function(e,r,n){var o="undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch,i="undefined"!=typeof jest,a=o||i?function(e){}:console.error,s="
"+n+"
";"object"!=typeof n?(s=String(n),a("### Unexpected error:"),a(n)):n instanceof t.TwoslashError?(s=function(e){return"\n

"+f(e.title)+"

\n

"+f(e.description).replace(/(?:\r\n|\r|\n)/g,"
")+"

\n"+f(e.recommendation).replace(/(?:\r\n|\r|\n)/g,"
")+"
\n"}(n),a("### Twoslash error: "+n.title),a(n.description),a(n.recommendation),a("\n### Code Sample"),a(e)):n instanceof Error&&(s=function(e){return"
"+e.message.split("## Code")[0]+"
"}(n),a("### Unexpected error:"),a(n));var c="\n
Twoslash failure
\n
"+s+"

Raising Code:

"+f(e)+"
";r.type="html",r.value='
'+c+"
",r.children=[]},h=new Map;function d(e,t,r,n,o){var a,s={json5:"json"};if(s[t.lang]&&(t.lang=s[t.lang]),"twoslash"===t.lang){if(!t.meta.include||"string"!=typeof t.meta.include)throw new Error("A twoslash code block needs a pragma like 'twoslash include [name]'");!function(e,t,r){var n=[];r.split("\n").forEach((function(r,o){var i=r.trim();if(i.startsWith("// - ")){var a=i.split("// - ")[1].split(" ")[0];e.set(t+"-"+a,n.join("\n"))}else n.push(r)})),e.set(t,n.join("\n"))}(h,t.meta.include,e),a=o.wrapFragments?'
':""}else a=r.map((function(r){var a=r.customName.split("/").pop().replace(".json","");return i.renderCodeToHTML(e,t.lang,t.meta,c({themeName:a},o),r,n)})).join("\n"),r.length>1&&o.wrapFragments&&(a='
'+a+"
");return a}var v=function(e,t,r){var n=t.lang;if(void 0===r&&(r={}),("undefined"==typeof process||!process.env||!process.env.TWOSLASH_DISABLE)&&t.meta.twoslash)return function(e,t,r){if("undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch)return i.runTwoSlash(e,t,r);var n=require("crypto").createHash,o=require("fs"),a=o.readFileSync,s=o.existsSync,c=o.mkdirSync,u=o.writeFileSync,l=require("path").join,f=require("@typescript/twoslash/package.json").version,p=require("typescript/package.json").version,h=n("sha1").update(JSON.stringify([e,t,r,f,p,require("@typescript/vfs/package.json").version])).digest("hex"),d=function(){return __dirname.includes("node_modules")?l(__dirname.split("node_modules")[0],"node_modules",".cache","twoslash"):l(__dirname,"..","..",".cache","twoslash")},v=process.versions.pnp?function(){try{var e=require("pnpapi");return l(e.getPackageInformation(e.topLevel).packageLocation,"node_modules",".cache","twoslash")}catch(e){return d()}}():d(),m=l(v,h+".json");if(s(m))return process.env.debug&&console.log("Using cached twoslash results from "+m),function(e){var t=new Date;return require("fs").utimesSync(m,t,t),e}(JSON.parse(a(m,"utf8")));var g=i.runTwoSlash(e,t,r);return s(v)||c(v,{recursive:!0}),u(m,JSON.stringify(g),"utf8"),g}(function(e,t){for(var r,n=/\/\/ @include: (.*)$/gm,o=[];null!==(r=n.exec(t));){r.index===n.lastIndex&&n.lastIndex++;var i=r[1],a=e.get(i);if(!a){var s="Could not find an include with the key: '"+i+"'.\nThere is: "+Array.from(e.keys())+".";throw new Error(s)}o.push([r.index,r[0].length,a])}var c=t.toString();return o.reverse().forEach((function(e){c=c.substring(0,e[0])+e[2]+c.substring(e[0]+e[1])})),c}(h,e),n,r)},m=new Map,g=function(e){return Promise.all((e.themes||(e.theme?[e.theme]:["light-plus"])).map(function(){var t=s(l.mark((function t(r){var n,i;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=r.name||r,t.next=3,o.getHighlighter(c({},e,{theme:r,themes:void 0}));case 3:return(i=t.sent).customName=n,t.abrupt("return",i);case 6:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()))},y=function(){return h.clear()},w=function(e){var t,r=n.lex(e),o=r[0],i=r.slice(1);if("twoslash"===o){var a=i.indexOf("include");-1!==a&&i.splice(a+1,0,"=")}var s=null!=(t=n.parse(i))?t:{};return{lang:(o||"").toString(),meta:s}},x=function(e,r){return void 0===r&&(r={}),function(n){var o=n.value,i=void 0;try{i=w([n.lang,n.meta].filter(Boolean).join(" "))}catch(e){var a=new t.TwoslashError("Codefence error","Could not parse the codefence for this code sample","It's usually an unclosed string",o);return p(o,n,a)}if(!(Object.keys(i.meta).filter((function(e){return(r.ignoreCodeblocksWithCodefenceMeta||[]).includes(e)})).length>0)){var s;try{s=n.twoslash||v(o,i,r)}catch(e){var c=process&&process.env&&!!process.env.CI,u="undefined"==typeof jest;if(c&&u||r.alwayRaiseForTwoslashExceptions)throw e;return p(o,n,e)}s&&(n.value=s.code,n.lang=s.extension,n.twoslash=s);var l=d(n.value,i,e,s,r);n.type="html",n.value=l,n.children=[]}}},b=function(){var e=s(l.mark((function e(t){return l.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return void 0===t&&(t={}),y(),m.has(t)||m.set(t,g(t)),e.next=5,m.get(t);case 5:return e.abrupt("return",{settings:t,highlighters:e.sent});case 7:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}();exports.default=function(e){return void 0===e&&(e={}),m.has(e)||m.set(e,g(e)),function(){var t=s(l.mark((function t(n){var o;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,m.get(e);case 2:o=t.sent,y(),r(n,"code",x(o,e));case 5:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()},exports.highlightersFromSettings=g,exports.remarkVisitor=x,exports.runTwoSlashOnNode=v,exports.setupForFile=b,exports.transformAttributesToHTML=function(e,t,r,n){var o=w(t),i=v(e,o,n);return d(i&&i.code||e,o,r,i,n)}; //# sourceMappingURL=remark-shiki-twoslash.cjs.production.min.js.map diff --git a/dist/remark-shiki-twoslash.esm.js b/dist/remark-shiki-twoslash.esm.js -index 7d005c0ae14fff368e1808ffae3ecef5dab756b9..ea4496065aa297d9cf1aefc0b8d24eba6b985b5a 100644 +index 7d005c0ae14fff368e1808ffae3ecef5dab756b9..fa645ebd99f6020b5eab3bc50e25762335d44054 100644 --- a/dist/remark-shiki-twoslash.esm.js +++ b/dist/remark-shiki-twoslash.esm.js @@ -819,7 +819,7 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { @@ -32,3 +44,15 @@ index 7d005c0ae14fff368e1808ffae3ecef5dab756b9..ea4496065aa297d9cf1aefc0b8d24eba var getNmCache = function getNmCache() { if (__dirname.includes("node_modules")) { +@@ -844,7 +844,10 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { + + if (existsSync(cachePath)) { + if (process.env.debug) console.log("Using cached twoslash results from " + cachePath); +- return JSON.parse(readFileSync(cachePath, "utf8")); ++ var results = JSON.parse(readFileSync(cachePath, "utf8")); ++ var now = new Date(); ++ require("fs").utimesSync(cachePath, now, now); ++ return results; + } else { + var results = runTwoSlash(code, lang, settings); + if (!existsSync(cacheRoot)) mkdirSync(cacheRoot, { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e03eed948ac6..caa4de3cd806 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ patchedDependencies: hash: 3aa0dc2d9c0fd3921986acbadfef7a10df948fb30d02590f86cecab9b91800bf path: patches/gatsby-remark-shiki-twoslash@3.0.38.patch remark-shiki-twoslash@3.1.3: - hash: c293ebf6939b77de68ccb4cc5d7120b789a307c28f00abb9b3e7ea6bb4a64767 + hash: 6a88dcbef5f4a7db6aa5d7a1fbaf9a293d04068303b7400ccb46fbae51d3e8b7 path: patches/remark-shiki-twoslash@3.1.3.patch importers: @@ -84,7 +84,7 @@ importers: version: 13.0.2 remark-shiki-twoslash: specifier: ^3.1.3 - version: 3.1.3(patch_hash=c293ebf6939b77de68ccb4cc5d7120b789a307c28f00abb9b3e7ea6bb4a64767)(typescript@6.0.2) + version: 3.1.3(patch_hash=6a88dcbef5f4a7db6aa5d7a1fbaf9a293d04068303b7400ccb46fbae51d3e8b7)(typescript@6.0.2) packages/ata: dependencies: @@ -16343,7 +16343,7 @@ snapshots: gatsby-remark-shiki-twoslash@3.0.38(patch_hash=3aa0dc2d9c0fd3921986acbadfef7a10df948fb30d02590f86cecab9b91800bf): dependencies: - remark-shiki-twoslash: 3.1.3(patch_hash=c293ebf6939b77de68ccb4cc5d7120b789a307c28f00abb9b3e7ea6bb4a64767)(typescript@6.0.2) + remark-shiki-twoslash: 3.1.3(patch_hash=6a88dcbef5f4a7db6aa5d7a1fbaf9a293d04068303b7400ccb46fbae51d3e8b7)(typescript@6.0.2) typescript: 6.0.2 unist-util-visit: 2.0.3 @@ -20184,7 +20184,7 @@ snapshots: dependencies: mdast-util-to-nlcst: 4.0.1 - remark-shiki-twoslash@3.1.3(patch_hash=c293ebf6939b77de68ccb4cc5d7120b789a307c28f00abb9b3e7ea6bb4a64767)(typescript@6.0.2): + remark-shiki-twoslash@3.1.3(patch_hash=6a88dcbef5f4a7db6aa5d7a1fbaf9a293d04068303b7400ccb46fbae51d3e8b7)(typescript@6.0.2): dependencies: '@types/unist': 2.0.11 '@typescript/twoslash': link:packages/ts-twoslasher @@ -21436,7 +21436,7 @@ snapshots: hast-util-to-html: 7.1.3 mdast-util-to-hast: 10.2.0 remark: 13.0.0 - remark-shiki-twoslash: 3.1.3(patch_hash=c293ebf6939b77de68ccb4cc5d7120b789a307c28f00abb9b3e7ea6bb4a64767)(typescript@6.0.2) + remark-shiki-twoslash: 3.1.3(patch_hash=6a88dcbef5f4a7db6aa5d7a1fbaf9a293d04068303b7400ccb46fbae51d3e8b7)(typescript@6.0.2) unist-util-visit: 3.1.0 transitivePeerDependencies: - supports-color From e2c8d0a1a9bd31eff766ab7e10b223335b0588bb Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:09:53 -0700 Subject: [PATCH 08/14] Checkpoint website caches after successful builds GitHub cache archives are immutable, so repeated builds of the same commit cannot otherwise save newly compiled examples or refreshed usage order. Restore the latest compatible snapshot and save a pruned replacement under a unique run-and-attempt key as soon as the site build succeeds, even if subsequent tests or deployment fail. --- .github/actions/website-cache/action.yml | 14 +++++++--- .github/actions/website-cache/save/action.yml | 28 +++++++++++++++++++ .github/workflows/CI.yml | 6 ++++ .github/workflows/deploy-prod-static.yml | 6 ++++ 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 .github/actions/website-cache/save/action.yml diff --git a/.github/actions/website-cache/action.yml b/.github/actions/website-cache/action.yml index 39994a465908..2ccdaf6b0306 100644 --- a/.github/actions/website-cache/action.yml +++ b/.github/actions/website-cache/action.yml @@ -1,6 +1,11 @@ -name: Cache website build work +name: Restore website build work description: Reuse compiled examples and Webpack work without caching generated pages +outputs: + key: + description: Primary key to use when checkpointing the completed build + value: ${{ steps.restore.outputs.cache-primary-key }} + runs: using: composite steps: @@ -9,11 +14,12 @@ runs: shell: bash run: echo "version=$(node --version)" >> "$GITHUB_OUTPUT" - - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + id: restore with: path: | node_modules/.cache/twoslash packages/typescriptlang-org/.cache/webpack - key: website-${{ runner.os }}-${{ steps.node.outputs.version }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'patches/**', 'packages/*/package.json', 'packages/*/tsconfig*.json', 'packages/*/src/**', 'packages/*/lib/**', 'packages/*/scripts/**', 'packages/*/script/**', 'packages/*/data/**', 'packages/typescriptlang-org/gatsby-*.js', '.github/actions/website-cache/action.yml') }}-${{ github.sha }} + key: website-${{ runner.os }}-${{ steps.node.outputs.version }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'patches/**', 'packages/*/package.json', 'packages/*/tsconfig*.json', 'packages/*/src/**', 'packages/*/lib/**', 'packages/*/scripts/**', 'packages/*/script/**', 'packages/*/data/**', 'packages/typescriptlang-org/gatsby-*.js', '.github/actions/website-cache/**') }}-${{ github.run_id }}-${{ github.run_attempt }} restore-keys: | - website-${{ runner.os }}-${{ steps.node.outputs.version }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'patches/**', 'packages/*/package.json', 'packages/*/tsconfig*.json', 'packages/*/src/**', 'packages/*/lib/**', 'packages/*/scripts/**', 'packages/*/script/**', 'packages/*/data/**', 'packages/typescriptlang-org/gatsby-*.js', '.github/actions/website-cache/action.yml') }}- + website-${{ runner.os }}-${{ steps.node.outputs.version }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'patches/**', 'packages/*/package.json', 'packages/*/tsconfig*.json', 'packages/*/src/**', 'packages/*/lib/**', 'packages/*/scripts/**', 'packages/*/script/**', 'packages/*/data/**', 'packages/typescriptlang-org/gatsby-*.js', '.github/actions/website-cache/**') }}- diff --git a/.github/actions/website-cache/save/action.yml b/.github/actions/website-cache/save/action.yml new file mode 100644 index 000000000000..436959eef2e7 --- /dev/null +++ b/.github/actions/website-cache/save/action.yml @@ -0,0 +1,28 @@ +name: Save website build work +description: Prune least-recently-used compiled examples and checkpoint the successful build + +inputs: + key: + description: Primary key returned by the website cache restore action + required: true + +runs: + using: composite + steps: + - name: Prune Twoslash cache to 64 MiB + shell: bash + env: + CACHE_KEY: ${{ inputs.key }} + run: | + if [[ -z "$CACHE_KEY" ]]; then + echo "::error::Missing website cache restore key" + exit 1 + fi + node "$GITHUB_ACTION_PATH/../prune-twoslash-cache.js" + + - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + node_modules/.cache/twoslash + packages/typescriptlang-org/.cache/webpack + key: ${{ inputs.key }} diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 98642502e59f..133168abb8ff 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -39,10 +39,16 @@ jobs: - run: pnpm bootstrap - name: Restore website build caches + id: website-cache uses: ./.github/actions/website-cache # Verify it compiles - run: pnpm build-site + + - name: Save website build caches + uses: ./.github/actions/website-cache/save + with: + key: ${{ steps.website-cache.outputs.key }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: github.event_name == 'pull_request' && matrix.os == 'ubuntu-latest' diff --git a/.github/workflows/deploy-prod-static.yml b/.github/workflows/deploy-prod-static.yml index a9dc51b078b7..b962fc44c5a8 100644 --- a/.github/workflows/deploy-prod-static.yml +++ b/.github/workflows/deploy-prod-static.yml @@ -48,6 +48,7 @@ jobs: run: pnpm bootstrap - name: Restore website build caches + id: website-cache uses: ./.github/actions/website-cache - name: Set up playground cache busting @@ -56,6 +57,11 @@ jobs: - name: Build static site run: pnpm build-site + - name: Save website build caches + uses: ./.github/actions/website-cache/save + with: + key: ${{ steps.website-cache.outputs.key }} + - name: Setup Pages uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 From 880c307ce1b8b1d92d794707bbe40b98fa954470 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:10:48 -0700 Subject: [PATCH 09/14] Reuse website cache snapshots across source changes Playground changes should not force unchanged documentation examples to compile again. Hashing workspace sources, including generated inputs, makes those iterations cold. Broader snapshot reuse requires example-level checks of compiler implementations and filesystem dependencies. Package versions alone cannot detect rebuilt workspace compilers or changed declarations, and missed module lookups must be reconsidered when new files appear. --- .github/actions/website-cache/action.yml | 4 +- .../lib/bootup/shiki.test.js | 153 ++++++++- patches/remark-shiki-twoslash@3.1.3.patch | 314 +++++++++++++++--- pnpm-lock.yaml | 10 +- 4 files changed, 432 insertions(+), 49 deletions(-) diff --git a/.github/actions/website-cache/action.yml b/.github/actions/website-cache/action.yml index 2ccdaf6b0306..040d1d460f45 100644 --- a/.github/actions/website-cache/action.yml +++ b/.github/actions/website-cache/action.yml @@ -20,6 +20,6 @@ runs: path: | node_modules/.cache/twoslash packages/typescriptlang-org/.cache/webpack - key: website-${{ runner.os }}-${{ steps.node.outputs.version }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'patches/**', 'packages/*/package.json', 'packages/*/tsconfig*.json', 'packages/*/src/**', 'packages/*/lib/**', 'packages/*/scripts/**', 'packages/*/script/**', 'packages/*/data/**', 'packages/typescriptlang-org/gatsby-*.js', '.github/actions/website-cache/**') }}-${{ github.run_id }}-${{ github.run_attempt }} + key: website-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.node.outputs.version }}-${{ github.run_id }}-${{ github.run_attempt }} restore-keys: | - website-${{ runner.os }}-${{ steps.node.outputs.version }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'patches/**', 'packages/*/package.json', 'packages/*/tsconfig*.json', 'packages/*/src/**', 'packages/*/lib/**', 'packages/*/scripts/**', 'packages/*/script/**', 'packages/*/data/**', 'packages/typescriptlang-org/gatsby-*.js', '.github/actions/website-cache/**') }}- + website-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.node.outputs.version }}- diff --git a/packages/typescriptlang-org/lib/bootup/shiki.test.js b/packages/typescriptlang-org/lib/bootup/shiki.test.js index e468d80deb5e..b90024399937 100644 --- a/packages/typescriptlang-org/lib/bootup/shiki.test.js +++ b/packages/typescriptlang-org/lib/bootup/shiki.test.js @@ -141,7 +141,9 @@ describe("persistent Twoslash cache", () => { }) runTwoSlash = jest .spyOn(shikiTwoslash, "runTwoSlash") - .mockImplementation((code, lang, settings) => ({ code, lang, settings })) + .mockImplementation((code, lang, { tsModule, ...settings }) => ({ + code, lang, settings, + })) }) afterEach(() => jest.restoreAllMocks()) @@ -165,6 +167,14 @@ describe("persistent Twoslash cache", () => { expect(runTwoSlash).toHaveBeenCalledTimes(2) }) + it("invalidates changed examples without discarding other examples", () => { + runTwoSlashOnNode("const value = 1", fence("ts"), {}) + runTwoSlashOnNode("const other = 1", fence("ts"), {}) + runTwoSlashOnNode("const value = 2", fence("ts"), {}) + runTwoSlashOnNode("const other = 1", fence("ts"), {}) + expect(runTwoSlash).toHaveBeenCalledTimes(3) + }) + it("invalidates results when compiler or Twoslash options change", () => { runTwoSlashOnNode("const value = 1", fence("ts"), {}) runTwoSlashOnNode("const value = 1", fence("ts"), { @@ -196,6 +206,147 @@ describe("persistent Twoslash cache", () => { "access time update failed" ) }) + + it.each(["runtime", "manifest"])("invalidates changed compiler %s without a version bump", kind => { + const freshCache = () => { + let cache + jest.isolateModules(() => { + cache = jest.requireActual("remark-shiki-twoslash/dist/twoslash-cache.js") + }) + return cache + } + freshCache()("const value = 1", "ts", {}, runTwoSlash) + freshCache()("const value = 1", "ts", {}, runTwoSlash) + expect(runTwoSlash).toHaveBeenCalledTimes(1) + const readFileSync = fs.readFileSync.getMockImplementation() + fs.readFileSync.mockImplementation((filename, ...args) => { + const contents = readFileSync(filename, ...args) + if (typeof filename === "string" && filename.replace(/\\/g, "/").includes("ts-twoslasher/")) { + if (kind === "runtime" && filename.endsWith(".js")) { + return Buffer.concat([Buffer.from(contents), Buffer.from("\n// compiler rebuild\n")]) + } + if (kind === "manifest" && filename.endsWith("package.json")) { + return Buffer.from(JSON.stringify({ ...JSON.parse(contents), main: "./dist/changed-compiler.js" })) + } + } + return contents + }) + freshCache()("const value = 1", "ts", {}, runTwoSlash) + expect(runTwoSlash).toHaveBeenCalledTimes(2) + }) + + it.each([ + { tsModule: { version: "custom compiler" } }, + { fsMap: new Map() }, + { customTransformers: { before: [] } }, + { lzstringModule: { compressToEncodedURIComponent: () => "custom" } }, + ])("does not reuse non-serializable compiler inputs: %p", settings => { + runTwoSlashOnNode("const value = 1", fence("ts"), settings) + runTwoSlashOnNode("const value = 1", fence("ts"), settings) + expect(runTwoSlash).toHaveBeenCalledTimes(2) + expect(fs.writeFileSync).not.toHaveBeenCalled() + }) + + describe("filesystem inputs", () => { + const os = require("os") + const path = require("path") + let directory + + beforeEach(() => { + directory = fs.mkdtempSync(path.join(os.tmpdir(), "website-twoslash-input-")) + }) + + afterEach(() => fs.rmSync(directory, { recursive: true, force: true })) + + it("invalidates changed declarations", () => { + const filename = path.join(directory, "dependency.d.ts") + fs.writeFileSync(filename, "export const value: number") + runTwoSlash.mockImplementation((code, lang, settings) => ({ + declaration: settings.tsModule.sys.readFile(filename), + })) + const first = runTwoSlashOnNode("import 'dependency'", fence("ts"), {}) + expect(runTwoSlashOnNode("import 'dependency'", fence("ts"), {})).toEqual(first) + expect(runTwoSlash).toHaveBeenCalledTimes(1) + fs.writeFileSync(filename, "export const value: string") + const second = runTwoSlashOnNode("import 'dependency'", fence("ts"), {}) + expect(second.declaration).toBe("export const value: string") + expect(runTwoSlash).toHaveBeenCalledTimes(2) + expect(runTwoSlashOnNode("import 'dependency'", fence("ts"), {})).toEqual(second) + expect(runTwoSlash).toHaveBeenCalledTimes(2) + }) + + it("recompiles real imports when their declarations change", () => { + runTwoSlash.mockRestore() + runTwoSlash = jest.spyOn(shikiTwoslash, "runTwoSlash") + const filename = path.join(directory, "dependency.d.ts") + const code = 'import { value } from "./dependency"\nconst answer = value\n// ^?' + const settings = { vfsRoot: directory, defaultCompilerOptions: { types: [] } } + fs.writeFileSync(filename, "export const value: number") + const first = runTwoSlashOnNode(code, fence("ts"), settings) + expect(first.queries[0].text).toBe("const answer: number") + expect(runTwoSlashOnNode(code, fence("ts"), settings)).toEqual(first) + expect(runTwoSlash).toHaveBeenCalledTimes(1) + fs.writeFileSync(filename, "export const value: string") + const second = runTwoSlashOnNode(code, fence("ts"), settings) + expect(second.queries[0].text).toBe("const answer: string") + expect(runTwoSlashOnNode(code, fence("ts"), settings)).toEqual(second) + expect(runTwoSlash).toHaveBeenCalledTimes(2) + }) + + it("invalidates missing imports when a file becomes available", () => { + const filename = path.join(directory, "dependency.d.ts") + runTwoSlash.mockImplementation((code, lang, settings) => ({ + exists: settings.tsModule.sys.fileExists(filename), + })) + expect(runTwoSlashOnNode("import 'dependency'", fence("ts"), {}).exists).toBe(false) + runTwoSlashOnNode("import 'dependency'", fence("ts"), {}) + expect(runTwoSlash).toHaveBeenCalledTimes(1) + fs.writeFileSync(filename, "export const value: number") + expect(runTwoSlashOnNode("import 'dependency'", fence("ts"), {}).exists).toBe(true) + expect(runTwoSlash).toHaveBeenCalledTimes(2) + }) + + it("invalidates removed declarations", () => { + const filename = path.join(directory, "dependency.d.ts") + fs.writeFileSync(filename, "export const value: number") + runTwoSlash.mockImplementation((code, lang, settings) => ({ + declaration: settings.tsModule.sys.readFile(filename), + })) + runTwoSlashOnNode("import 'dependency'", fence("ts"), {}) + fs.unlinkSync(filename) + expect(runTwoSlashOnNode("import 'dependency'", fence("ts"), {}).declaration).toBeUndefined() + expect(runTwoSlash).toHaveBeenCalledTimes(2) + }) + + it("validates directory listings and preserves omitted arguments", () => { + runTwoSlash.mockImplementation((code, lang, settings) => ({ + files: settings.tsModule.sys.readDirectory(directory, undefined, undefined, ["**/*"]) + .map(filename => path.resolve(filename)), + })) + expect(runTwoSlashOnNode("import 'dependency'", fence("ts"), {}).files).toEqual([]) + runTwoSlashOnNode("import 'dependency'", fence("ts"), {}) + expect(runTwoSlash).toHaveBeenCalledTimes(1) + const filename = path.join(directory, "dependency.d.ts") + fs.writeFileSync(filename, "export const value: number") + expect(runTwoSlashOnNode("import 'dependency'", fence("ts"), {}).files).toEqual([filename]) + expect(runTwoSlash).toHaveBeenCalledTimes(2) + }) + + it("surfaces errors validating dependencies", () => { + const filename = path.join(directory, "dependency.d.ts") + fs.writeFileSync(filename, "export const value: number") + runTwoSlash.mockImplementation((code, lang, settings) => ({ + declaration: settings.tsModule.sys.readFile(filename), + })) + runTwoSlashOnNode("import 'dependency'", fence("ts"), {}) + jest.spyOn(fs, "statSync").mockImplementation(() => { + throw new Error("dependency access failed") + }) + expect(() => runTwoSlashOnNode("import 'dependency'", fence("ts"), {})).toThrow( + "dependency access failed" + ) + }) + }) }) describe("Twoslash LRU pruning", () => { diff --git a/patches/remark-shiki-twoslash@3.1.3.patch b/patches/remark-shiki-twoslash@3.1.3.patch index 16e7e0012f82..2b3d5adcc5e6 100644 --- a/patches/remark-shiki-twoslash@3.1.3.patch +++ b/patches/remark-shiki-twoslash@3.1.3.patch @@ -1,58 +1,290 @@ diff --git a/dist/remark-shiki-twoslash.cjs.development.js b/dist/remark-shiki-twoslash.cjs.development.js -index efaed7711553c44c31abcaf53abab08b8780b248..8235d8f05641cd04e8d3ba9bef5a9ba4af03a605 100644 +index efaed7711553c44c31abcaf53abab08b8780b248..782e3362bfd5be74d087c01571ce7e858697dec7 100644 --- a/dist/remark-shiki-twoslash.cjs.development.js +++ b/dist/remark-shiki-twoslash.cjs.development.js -@@ -825,7 +825,7 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { - var tsVersion = require('typescript/package.json').version; +@@ -808,57 +808,7 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { + return shikiTwoslash.runTwoSlash(code, lang, settings); + } - var shasum = createHash("sha1"); +- var _require = require("crypto"), +- createHash = _require.createHash; +- +- var _require2 = require("fs"), +- readFileSync = _require2.readFileSync, +- existsSync = _require2.existsSync, +- mkdirSync = _require2.mkdirSync, +- writeFileSync = _require2.writeFileSync; +- +- var _require3 = require("path"), +- join = _require3.join; +- +- var shikiVersion = require('@typescript/twoslash/package.json').version; +- +- var tsVersion = require('typescript/package.json').version; +- +- var shasum = createHash("sha1"); - var codeSha = shasum.update(code + "-" + shikiVersion + "-" + tsVersion).digest("hex"); -+ var codeSha = shasum.update(JSON.stringify([code, lang, settings, shikiVersion, tsVersion, require('@typescript/vfs/package.json').version])).digest("hex"); - - var getNmCache = function getNmCache() { - if (__dirname.includes("node_modules")) { -@@ -850,7 +850,10 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { - - if (existsSync(cachePath)) { - if (process.env.debug) console.log("Using cached twoslash results from " + cachePath); +- +- var getNmCache = function getNmCache() { +- if (__dirname.includes("node_modules")) { +- return join(__dirname.split("node_modules")[0], "node_modules", ".cache", "twoslash"); +- } else { +- return join(__dirname, "..", "..", ".cache", "twoslash"); +- } +- }; +- +- var getPnpCache = function getPnpCache() { +- try { +- var pnp = require("pnpapi"); +- +- return join(pnp.getPackageInformation(pnp.topLevel).packageLocation, "node_modules", ".cache", "twoslash"); +- } catch (error) { +- return getNmCache(); +- } +- }; +- +- var cacheRoot = process.versions.pnp ? getPnpCache() : getNmCache(); +- var cachePath = join(cacheRoot, codeSha + ".json"); +- +- if (existsSync(cachePath)) { +- if (process.env.debug) console.log("Using cached twoslash results from " + cachePath); - return JSON.parse(readFileSync(cachePath, "utf8")); -+ var results = JSON.parse(readFileSync(cachePath, "utf8")); -+ var now = new Date(); -+ require("fs").utimesSync(cachePath, now, now); -+ return results; - } else { - var results = shikiTwoslash.runTwoSlash(code, lang, settings); - if (!existsSync(cacheRoot)) mkdirSync(cacheRoot, { +- } else { +- var results = shikiTwoslash.runTwoSlash(code, lang, settings); +- if (!existsSync(cacheRoot)) mkdirSync(cacheRoot, { +- recursive: true +- }); +- writeFileSync(cachePath, JSON.stringify(results), "utf8"); +- return results; +- } ++ return require("./twoslash-cache.js")(code, lang, settings, shikiTwoslash.runTwoSlash); + }; + + var addIncludes = function addIncludes(map, name, code) { diff --git a/dist/remark-shiki-twoslash.cjs.production.min.js b/dist/remark-shiki-twoslash.cjs.production.min.js -index 9ee26741d08d555a0ef3c0e0917ef0d6342020a5..c0aa7edf0323c4047c9167afcc51412d646efc64 100644 +index 9ee26741d08d555a0ef3c0e0917ef0d6342020a5..2df325d745cc341393c3c38c72ad29342f2631b7 100644 --- a/dist/remark-shiki-twoslash.cjs.production.min.js +++ b/dist/remark-shiki-twoslash.cjs.production.min.js -@@ -1,2 +1,2 @@ +@@ -1,2 +1,3 @@ -"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,t=require("@typescript/twoslash"),r=(e=require("unist-util-visit"))&&"object"==typeof e&&"default"in e?e.default:e,n=require("fenceparser"),o=require("shiki"),i=require("shiki-twoslash");function a(e,t,r,n,o,i,a){try{var s=e[i](a),c=s.value}catch(e){return void r(e)}s.done?t(c):Promise.resolve(c).then(n,o)}function s(e){return function(){var t=this,r=arguments;return new Promise((function(n,o){var i=e.apply(t,r);function s(e){a(i,n,o,s,c,"next",e)}function c(e){a(i,n,o,s,c,"throw",e)}s(void 0)}))}}function c(){return(c=Object.assign||function(e){for(var t=1;t=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var s=r.call(i,"catchLoc"),c=r.call(i,"finallyLoc");if(s&&c){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),k(r),l}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;k(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:L(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),l}},e}(e.exports);try{regeneratorRuntime=t}catch(e){Function("r","regeneratorRuntime = r")(t)}}));function f(e){return e.replace(//g,">")}var p=function(e,r,n){var o="undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch,i="undefined"!=typeof jest,a=o||i?function(e){}:console.error,s="
"+n+"
";"object"!=typeof n?(s=String(n),a("### Unexpected error:"),a(n)):n instanceof t.TwoslashError?(s=function(e){return"\n

"+f(e.title)+"

\n

"+f(e.description).replace(/(?:\r\n|\r|\n)/g,"
")+"

\n"+f(e.recommendation).replace(/(?:\r\n|\r|\n)/g,"
")+"
\n"}(n),a("### Twoslash error: "+n.title),a(n.description),a(n.recommendation),a("\n### Code Sample"),a(e)):n instanceof Error&&(s=function(e){return"
"+e.message.split("## Code")[0]+"
"}(n),a("### Unexpected error:"),a(n));var c="\n
Twoslash failure
\n
"+s+"

Raising Code:

"+f(e)+"
";r.type="html",r.value='
'+c+"
",r.children=[]},h=new Map;function d(e,t,r,n,o){var a,s={json5:"json"};if(s[t.lang]&&(t.lang=s[t.lang]),"twoslash"===t.lang){if(!t.meta.include||"string"!=typeof t.meta.include)throw new Error("A twoslash code block needs a pragma like 'twoslash include [name]'");!function(e,t,r){var n=[];r.split("\n").forEach((function(r,o){var i=r.trim();if(i.startsWith("// - ")){var a=i.split("// - ")[1].split(" ")[0];e.set(t+"-"+a,n.join("\n"))}else n.push(r)})),e.set(t,n.join("\n"))}(h,t.meta.include,e),a=o.wrapFragments?'
':""}else a=r.map((function(r){var a=r.customName.split("/").pop().replace(".json","");return i.renderCodeToHTML(e,t.lang,t.meta,c({themeName:a},o),r,n)})).join("\n"),r.length>1&&o.wrapFragments&&(a='
'+a+"
");return a}var v=function(e,t,r){var n=t.lang;if(void 0===r&&(r={}),("undefined"==typeof process||!process.env||!process.env.TWOSLASH_DISABLE)&&t.meta.twoslash)return function(e,t,r){if("undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch)return i.runTwoSlash(e,t,r);var n=require("crypto").createHash,o=require("fs"),a=o.readFileSync,s=o.existsSync,c=o.mkdirSync,u=o.writeFileSync,l=require("path").join,f=require("@typescript/twoslash/package.json").version,p=require("typescript/package.json").version,h=n("sha1").update(e+"-"+f+"-"+p).digest("hex"),d=function(){return __dirname.includes("node_modules")?l(__dirname.split("node_modules")[0],"node_modules",".cache","twoslash"):l(__dirname,"..","..",".cache","twoslash")},v=process.versions.pnp?function(){try{var e=require("pnpapi");return l(e.getPackageInformation(e.topLevel).packageLocation,"node_modules",".cache","twoslash")}catch(e){return d()}}():d(),m=l(v,h+".json");if(s(m))return process.env.debug&&console.log("Using cached twoslash results from "+m),JSON.parse(a(m,"utf8"));var g=i.runTwoSlash(e,t,r);return s(v)||c(v,{recursive:!0}),u(m,JSON.stringify(g),"utf8"),g}(function(e,t){for(var r,n=/\/\/ @include: (.*)$/gm,o=[];null!==(r=n.exec(t));){r.index===n.lastIndex&&n.lastIndex++;var i=r[1],a=e.get(i);if(!a){var s="Could not find an include with the key: '"+i+"'.\nThere is: "+Array.from(e.keys())+".";throw new Error(s)}o.push([r.index,r[0].length,a])}var c=t.toString();return o.reverse().forEach((function(e){c=c.substring(0,e[0])+e[2]+c.substring(e[0]+e[1])})),c}(h,e),n,r)},m=new Map,g=function(e){return Promise.all((e.themes||(e.theme?[e.theme]:["light-plus"])).map(function(){var t=s(l.mark((function t(r){var n,i;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=r.name||r,t.next=3,o.getHighlighter(c({},e,{theme:r,themes:void 0}));case 3:return(i=t.sent).customName=n,t.abrupt("return",i);case 6:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()))},y=function(){return h.clear()},w=function(e){var t,r=n.lex(e),o=r[0],i=r.slice(1);if("twoslash"===o){var a=i.indexOf("include");-1!==a&&i.splice(a+1,0,"=")}var s=null!=(t=n.parse(i))?t:{};return{lang:(o||"").toString(),meta:s}},x=function(e,r){return void 0===r&&(r={}),function(n){var o=n.value,i=void 0;try{i=w([n.lang,n.meta].filter(Boolean).join(" "))}catch(e){var a=new t.TwoslashError("Codefence error","Could not parse the codefence for this code sample","It's usually an unclosed string",o);return p(o,n,a)}if(!(Object.keys(i.meta).filter((function(e){return(r.ignoreCodeblocksWithCodefenceMeta||[]).includes(e)})).length>0)){var s;try{s=n.twoslash||v(o,i,r)}catch(e){var c=process&&process.env&&!!process.env.CI,u="undefined"==typeof jest;if(c&&u||r.alwayRaiseForTwoslashExceptions)throw e;return p(o,n,e)}s&&(n.value=s.code,n.lang=s.extension,n.twoslash=s);var l=d(n.value,i,e,s,r);n.type="html",n.value=l,n.children=[]}}},b=function(){var e=s(l.mark((function e(t){return l.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return void 0===t&&(t={}),y(),m.has(t)||m.set(t,g(t)),e.next=5,m.get(t);case 5:return e.abrupt("return",{settings:t,highlighters:e.sent});case 7:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}();exports.default=function(e){return void 0===e&&(e={}),m.has(e)||m.set(e,g(e)),function(){var t=s(l.mark((function t(n){var o;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,m.get(e);case 2:o=t.sent,y(),r(n,"code",x(o,e));case 5:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()},exports.highlightersFromSettings=g,exports.remarkVisitor=x,exports.runTwoSlashOnNode=v,exports.setupForFile=b,exports.transformAttributesToHTML=function(e,t,r,n){var o=w(t),i=v(e,o,n);return d(i&&i.code||e,o,r,i,n)}; -+"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,t=require("@typescript/twoslash"),r=(e=require("unist-util-visit"))&&"object"==typeof e&&"default"in e?e.default:e,n=require("fenceparser"),o=require("shiki"),i=require("shiki-twoslash");function a(e,t,r,n,o,i,a){try{var s=e[i](a),c=s.value}catch(e){return void r(e)}s.done?t(c):Promise.resolve(c).then(n,o)}function s(e){return function(){var t=this,r=arguments;return new Promise((function(n,o){var i=e.apply(t,r);function s(e){a(i,n,o,s,c,"next",e)}function c(e){a(i,n,o,s,c,"throw",e)}s(void 0)}))}}function c(){return(c=Object.assign||function(e){for(var t=1;t=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var s=r.call(i,"catchLoc"),c=r.call(i,"finallyLoc");if(s&&c){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),k(r),l}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;k(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:L(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),l}},e}(e.exports);try{regeneratorRuntime=t}catch(e){Function("r","regeneratorRuntime = r")(t)}}));function f(e){return e.replace(//g,">")}var p=function(e,r,n){var o="undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch,i="undefined"!=typeof jest,a=o||i?function(e){}:console.error,s="
"+n+"
";"object"!=typeof n?(s=String(n),a("### Unexpected error:"),a(n)):n instanceof t.TwoslashError?(s=function(e){return"\n

"+f(e.title)+"

\n

"+f(e.description).replace(/(?:\r\n|\r|\n)/g,"
")+"

\n"+f(e.recommendation).replace(/(?:\r\n|\r|\n)/g,"
")+"
\n"}(n),a("### Twoslash error: "+n.title),a(n.description),a(n.recommendation),a("\n### Code Sample"),a(e)):n instanceof Error&&(s=function(e){return"
"+e.message.split("## Code")[0]+"
"}(n),a("### Unexpected error:"),a(n));var c="\n
Twoslash failure
\n
"+s+"

Raising Code:

"+f(e)+"
";r.type="html",r.value='
'+c+"
",r.children=[]},h=new Map;function d(e,t,r,n,o){var a,s={json5:"json"};if(s[t.lang]&&(t.lang=s[t.lang]),"twoslash"===t.lang){if(!t.meta.include||"string"!=typeof t.meta.include)throw new Error("A twoslash code block needs a pragma like 'twoslash include [name]'");!function(e,t,r){var n=[];r.split("\n").forEach((function(r,o){var i=r.trim();if(i.startsWith("// - ")){var a=i.split("// - ")[1].split(" ")[0];e.set(t+"-"+a,n.join("\n"))}else n.push(r)})),e.set(t,n.join("\n"))}(h,t.meta.include,e),a=o.wrapFragments?'
':""}else a=r.map((function(r){var a=r.customName.split("/").pop().replace(".json","");return i.renderCodeToHTML(e,t.lang,t.meta,c({themeName:a},o),r,n)})).join("\n"),r.length>1&&o.wrapFragments&&(a='
'+a+"
");return a}var v=function(e,t,r){var n=t.lang;if(void 0===r&&(r={}),("undefined"==typeof process||!process.env||!process.env.TWOSLASH_DISABLE)&&t.meta.twoslash)return function(e,t,r){if("undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch)return i.runTwoSlash(e,t,r);var n=require("crypto").createHash,o=require("fs"),a=o.readFileSync,s=o.existsSync,c=o.mkdirSync,u=o.writeFileSync,l=require("path").join,f=require("@typescript/twoslash/package.json").version,p=require("typescript/package.json").version,h=n("sha1").update(JSON.stringify([e,t,r,f,p,require("@typescript/vfs/package.json").version])).digest("hex"),d=function(){return __dirname.includes("node_modules")?l(__dirname.split("node_modules")[0],"node_modules",".cache","twoslash"):l(__dirname,"..","..",".cache","twoslash")},v=process.versions.pnp?function(){try{var e=require("pnpapi");return l(e.getPackageInformation(e.topLevel).packageLocation,"node_modules",".cache","twoslash")}catch(e){return d()}}():d(),m=l(v,h+".json");if(s(m))return process.env.debug&&console.log("Using cached twoslash results from "+m),function(e){var t=new Date;return require("fs").utimesSync(m,t,t),e}(JSON.parse(a(m,"utf8")));var g=i.runTwoSlash(e,t,r);return s(v)||c(v,{recursive:!0}),u(m,JSON.stringify(g),"utf8"),g}(function(e,t){for(var r,n=/\/\/ @include: (.*)$/gm,o=[];null!==(r=n.exec(t));){r.index===n.lastIndex&&n.lastIndex++;var i=r[1],a=e.get(i);if(!a){var s="Could not find an include with the key: '"+i+"'.\nThere is: "+Array.from(e.keys())+".";throw new Error(s)}o.push([r.index,r[0].length,a])}var c=t.toString();return o.reverse().forEach((function(e){c=c.substring(0,e[0])+e[2]+c.substring(e[0]+e[1])})),c}(h,e),n,r)},m=new Map,g=function(e){return Promise.all((e.themes||(e.theme?[e.theme]:["light-plus"])).map(function(){var t=s(l.mark((function t(r){var n,i;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=r.name||r,t.next=3,o.getHighlighter(c({},e,{theme:r,themes:void 0}));case 3:return(i=t.sent).customName=n,t.abrupt("return",i);case 6:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()))},y=function(){return h.clear()},w=function(e){var t,r=n.lex(e),o=r[0],i=r.slice(1);if("twoslash"===o){var a=i.indexOf("include");-1!==a&&i.splice(a+1,0,"=")}var s=null!=(t=n.parse(i))?t:{};return{lang:(o||"").toString(),meta:s}},x=function(e,r){return void 0===r&&(r={}),function(n){var o=n.value,i=void 0;try{i=w([n.lang,n.meta].filter(Boolean).join(" "))}catch(e){var a=new t.TwoslashError("Codefence error","Could not parse the codefence for this code sample","It's usually an unclosed string",o);return p(o,n,a)}if(!(Object.keys(i.meta).filter((function(e){return(r.ignoreCodeblocksWithCodefenceMeta||[]).includes(e)})).length>0)){var s;try{s=n.twoslash||v(o,i,r)}catch(e){var c=process&&process.env&&!!process.env.CI,u="undefined"==typeof jest;if(c&&u||r.alwayRaiseForTwoslashExceptions)throw e;return p(o,n,e)}s&&(n.value=s.code,n.lang=s.extension,n.twoslash=s);var l=d(n.value,i,e,s,r);n.type="html",n.value=l,n.children=[]}}},b=function(){var e=s(l.mark((function e(t){return l.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return void 0===t&&(t={}),y(),m.has(t)||m.set(t,g(t)),e.next=5,m.get(t);case 5:return e.abrupt("return",{settings:t,highlighters:e.sent});case 7:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}();exports.default=function(e){return void 0===e&&(e={}),m.has(e)||m.set(e,g(e)),function(){var t=s(l.mark((function t(n){var o;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,m.get(e);case 2:o=t.sent,y(),r(n,"code",x(o,e));case 5:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()},exports.highlightersFromSettings=g,exports.remarkVisitor=x,exports.runTwoSlashOnNode=v,exports.setupForFile=b,exports.transformAttributesToHTML=function(e,t,r,n){var o=w(t),i=v(e,o,n);return d(i&&i.code||e,o,r,i,n)}; ++"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,t=require("@typescript/twoslash"),r=(e=require("unist-util-visit"))&&"object"==typeof e&&"default"in e?e.default:e,n=require("fenceparser"),o=require("shiki"),i=require("shiki-twoslash");function a(e,t,r,n,o,i,a){try{var s=e[i](a),c=s.value}catch(e){return void r(e)}s.done?t(c):Promise.resolve(c).then(n,o)}function s(e){return function(){var t=this,r=arguments;return new Promise((function(n,o){var i=e.apply(t,r);function s(e){a(i,n,o,s,c,"next",e)}function c(e){a(i,n,o,s,c,"throw",e)}s(void 0)}))}}function c(){return(c=Object.assign||function(e){for(var t=1;t=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var s=r.call(i,"catchLoc"),c=r.call(i,"finallyLoc");if(s&&c){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),k(r),l}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;k(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:L(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),l}},e}(e.exports);try{regeneratorRuntime=t}catch(e){Function("r","regeneratorRuntime = r")(t)}}));function f(e){return e.replace(//g,">")}var p=function(e,r,n){var o="undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch,i="undefined"!=typeof jest,a=o||i?function(e){}:console.error,s="
"+n+"
";"object"!=typeof n?(s=String(n),a("### Unexpected error:"),a(n)):n instanceof t.TwoslashError?(s=function(e){return"\n

"+f(e.title)+"

\n

"+f(e.description).replace(/(?:\r\n|\r|\n)/g,"
")+"

\n"+f(e.recommendation).replace(/(?:\r\n|\r|\n)/g,"
")+"
\n"}(n),a("### Twoslash error: "+n.title),a(n.description),a(n.recommendation),a("\n### Code Sample"),a(e)):n instanceof Error&&(s=function(e){return"
"+e.message.split("## Code")[0]+"
"}(n),a("### Unexpected error:"),a(n));var c="\n
Twoslash failure
\n
"+s+"

Raising Code:

"+f(e)+"
";r.type="html",r.value='
'+c+"
",r.children=[]},h=new Map;function d(e,t,r,n,o){var a,s={json5:"json"};if(s[t.lang]&&(t.lang=s[t.lang]),"twoslash"===t.lang){if(!t.meta.include||"string"!=typeof t.meta.include)throw new Error("A twoslash code block needs a pragma like 'twoslash include [name]'");!function(e,t,r){var n=[];r.split("\n").forEach((function(r,o){var i=r.trim();if(i.startsWith("// - ")){var a=i.split("// - ")[1].split(" ")[0];e.set(t+"-"+a,n.join("\n"))}else n.push(r)})),e.set(t,n.join("\n"))}(h,t.meta.include,e),a=o.wrapFragments?'
':""}else a=r.map((function(r){var a=r.customName.split("/").pop().replace(".json","");return i.renderCodeToHTML(e,t.lang,t.meta,c({themeName:a},o),r,n)})).join("\n"),r.length>1&&o.wrapFragments&&(a='
'+a+"
");return a}var v=function(e,t,r){var n=t.lang;if(void 0===r&&(r={}),("undefined"==typeof process||!process.env||!process.env.TWOSLASH_DISABLE)&&t.meta.twoslash)return function(e,t,r){if("undefined"!=typeof self&&void 0!==self.WorkerGlobalScope||"undefined"!=typeof window&&void 0!==window.document&&"undefined"!=typeof fetch)return i.runTwoSlash(e,t,r);return require("./twoslash-cache.js")(e, t, r, i.runTwoSlash); ++}(function(e,t){for(var r,n=/\/\/ @include: (.*)$/gm,o=[];null!==(r=n.exec(t));){r.index===n.lastIndex&&n.lastIndex++;var i=r[1],a=e.get(i);if(!a){var s="Could not find an include with the key: '"+i+"'.\nThere is: "+Array.from(e.keys())+".";throw new Error(s)}o.push([r.index,r[0].length,a])}var c=t.toString();return o.reverse().forEach((function(e){c=c.substring(0,e[0])+e[2]+c.substring(e[0]+e[1])})),c}(h,e),n,r)},m=new Map,g=function(e){return Promise.all((e.themes||(e.theme?[e.theme]:["light-plus"])).map(function(){var t=s(l.mark((function t(r){var n,i;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=r.name||r,t.next=3,o.getHighlighter(c({},e,{theme:r,themes:void 0}));case 3:return(i=t.sent).customName=n,t.abrupt("return",i);case 6:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()))},y=function(){return h.clear()},w=function(e){var t,r=n.lex(e),o=r[0],i=r.slice(1);if("twoslash"===o){var a=i.indexOf("include");-1!==a&&i.splice(a+1,0,"=")}var s=null!=(t=n.parse(i))?t:{};return{lang:(o||"").toString(),meta:s}},x=function(e,r){return void 0===r&&(r={}),function(n){var o=n.value,i=void 0;try{i=w([n.lang,n.meta].filter(Boolean).join(" "))}catch(e){var a=new t.TwoslashError("Codefence error","Could not parse the codefence for this code sample","It's usually an unclosed string",o);return p(o,n,a)}if(!(Object.keys(i.meta).filter((function(e){return(r.ignoreCodeblocksWithCodefenceMeta||[]).includes(e)})).length>0)){var s;try{s=n.twoslash||v(o,i,r)}catch(e){var c=process&&process.env&&!!process.env.CI,u="undefined"==typeof jest;if(c&&u||r.alwayRaiseForTwoslashExceptions)throw e;return p(o,n,e)}s&&(n.value=s.code,n.lang=s.extension,n.twoslash=s);var l=d(n.value,i,e,s,r);n.type="html",n.value=l,n.children=[]}}},b=function(){var e=s(l.mark((function e(t){return l.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return void 0===t&&(t={}),y(),m.has(t)||m.set(t,g(t)),e.next=5,m.get(t);case 5:return e.abrupt("return",{settings:t,highlighters:e.sent});case 7:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}();exports.default=function(e){return void 0===e&&(e={}),m.has(e)||m.set(e,g(e)),function(){var t=s(l.mark((function t(n){var o;return l.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,m.get(e);case 2:o=t.sent,y(),r(n,"code",x(o,e));case 5:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()},exports.highlightersFromSettings=g,exports.remarkVisitor=x,exports.runTwoSlashOnNode=v,exports.setupForFile=b,exports.transformAttributesToHTML=function(e,t,r,n){var o=w(t),i=v(e,o,n);return d(i&&i.code||e,o,r,i,n)}; //# sourceMappingURL=remark-shiki-twoslash.cjs.production.min.js.map diff --git a/dist/remark-shiki-twoslash.esm.js b/dist/remark-shiki-twoslash.esm.js -index 7d005c0ae14fff368e1808ffae3ecef5dab756b9..fa645ebd99f6020b5eab3bc50e25762335d44054 100644 +index 7d005c0ae14fff368e1808ffae3ecef5dab756b9..cabf08a27259ac62316f3459c95ecd5bf9ae0082 100644 --- a/dist/remark-shiki-twoslash.esm.js +++ b/dist/remark-shiki-twoslash.esm.js -@@ -819,7 +819,7 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { - var tsVersion = require('typescript/package.json').version; +@@ -802,57 +802,7 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { + return runTwoSlash(code, lang, settings); + } - var shasum = createHash("sha1"); +- var _require = require("crypto"), +- createHash = _require.createHash; +- +- var _require2 = require("fs"), +- readFileSync = _require2.readFileSync, +- existsSync = _require2.existsSync, +- mkdirSync = _require2.mkdirSync, +- writeFileSync = _require2.writeFileSync; +- +- var _require3 = require("path"), +- join = _require3.join; +- +- var shikiVersion = require('@typescript/twoslash/package.json').version; +- +- var tsVersion = require('typescript/package.json').version; +- +- var shasum = createHash("sha1"); - var codeSha = shasum.update(code + "-" + shikiVersion + "-" + tsVersion).digest("hex"); -+ var codeSha = shasum.update(JSON.stringify([code, lang, settings, shikiVersion, tsVersion, require('@typescript/vfs/package.json').version])).digest("hex"); - - var getNmCache = function getNmCache() { - if (__dirname.includes("node_modules")) { -@@ -844,7 +844,10 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { - - if (existsSync(cachePath)) { - if (process.env.debug) console.log("Using cached twoslash results from " + cachePath); +- +- var getNmCache = function getNmCache() { +- if (__dirname.includes("node_modules")) { +- return join(__dirname.split("node_modules")[0], "node_modules", ".cache", "twoslash"); +- } else { +- return join(__dirname, "..", "..", ".cache", "twoslash"); +- } +- }; +- +- var getPnpCache = function getPnpCache() { +- try { +- var pnp = require("pnpapi"); +- +- return join(pnp.getPackageInformation(pnp.topLevel).packageLocation, "node_modules", ".cache", "twoslash"); +- } catch (error) { +- return getNmCache(); +- } +- }; +- +- var cacheRoot = process.versions.pnp ? getPnpCache() : getNmCache(); +- var cachePath = join(cacheRoot, codeSha + ".json"); +- +- if (existsSync(cachePath)) { +- if (process.env.debug) console.log("Using cached twoslash results from " + cachePath); - return JSON.parse(readFileSync(cachePath, "utf8")); -+ var results = JSON.parse(readFileSync(cachePath, "utf8")); -+ var now = new Date(); -+ require("fs").utimesSync(cachePath, now, now); -+ return results; - } else { - var results = runTwoSlash(code, lang, settings); - if (!existsSync(cacheRoot)) mkdirSync(cacheRoot, { +- } else { +- var results = runTwoSlash(code, lang, settings); +- if (!existsSync(cacheRoot)) mkdirSync(cacheRoot, { +- recursive: true +- }); +- writeFileSync(cachePath, JSON.stringify(results), "utf8"); +- return results; +- } ++ return require("./twoslash-cache.js")(code, lang, settings, runTwoSlash); + }; + + var addIncludes = function addIncludes(map, name, code) { +diff --git a/dist/twoslash-cache.js b/dist/twoslash-cache.js +new file mode 100644 +index 0000000000000000000000000000000000000000..7ae5d5ad41e6284ba9eefcfbfb86235800c94a9d +--- /dev/null ++++ b/dist/twoslash-cache.js +@@ -0,0 +1,149 @@ ++const fs = require("fs") ++const path = require("path") ++const { createHash } = require("crypto") ++const { createRequire } = require("module") ++ ++const shikiRequire = createRequire(require.resolve("shiki-twoslash")) ++const twoslashRequire = createRequire(shikiRequire.resolve("@typescript/twoslash")) ++const ts = twoslashRequire("typescript") ++const readTokens = new Map() ++let implementation ++ ++const digest = value => createHash("sha1").update(value).digest("hex") ++const portable = filename => ++ path.isAbsolute(filename) ? path.relative(process.cwd(), filename) : filename ++ ++function compilerImplementation() { ++ if (implementation) return implementation ++ const hash = createHash("sha1") ++ hash.update(fs.readFileSync(__filename)) ++ for (const [name, resolver] of [ ++ ["shiki-twoslash", require], ++ ["@typescript/twoslash", shikiRequire], ++ ["@typescript/vfs", twoslashRequire], ++ ]) { ++ hash.update(name).update(fs.readFileSync(resolver.resolve(name + "/package.json"))) ++ const directory = path.join( ++ path.dirname(resolver.resolve(name + "/package.json")), ++ "dist" ++ ) ++ for (const filename of fs.readdirSync(directory).filter(f => f.endsWith(".js")).sort()) { ++ hash.update(filename).update(fs.readFileSync(path.join(directory, filename))) ++ } ++ } ++ for (const name of ["typescript", "lz-string"]) { ++ hash.update(name).update(fs.readFileSync(twoslashRequire.resolve(name))) ++ } ++ implementation = hash.digest("hex") ++ return implementation ++} ++ ++function rememberRead(filename, contents, stat) { ++ filename = path.resolve(filename) ++ const token = contents === undefined ? null : digest(contents) ++ if (stat) { ++ const signature = [stat.size, stat.mtimeMs, stat.ctimeMs, stat.ino].join(":") ++ readTokens.delete(filename) ++ readTokens.set(filename, { signature, token }) ++ if (readTokens.size > 256) readTokens.delete(readTokens.keys().next().value) ++ } ++ return token ++} ++ ++function readToken(filename) { ++ filename = path.resolve(filename) ++ const stat = fs.statSync(filename, { throwIfNoEntry: false }) ++ if (!stat) return null ++ const signature = [stat.size, stat.mtimeMs, stat.ctimeMs, stat.ino].join(":") ++ const cached = readTokens.get(filename) ++ if (cached && cached.signature === signature) { ++ readTokens.delete(filename) ++ readTokens.set(filename, cached) ++ return cached.token ++ } ++ return rememberRead(filename, ts.sys.readFile(filename), stat) ++} ++ ++function inputToken(value) { ++ if (Array.isArray(value)) return JSON.stringify(value.map(portable)) ++ if (typeof value === "string") return portable(value) ++ return value ++} ++ ++function inputsMatch(inputs) { ++ return inputs.every(input => { ++ const args = input.args.map((arg, i) => ++ input.undefinedArgs.includes(i) ? undefined : arg ++ ) ++ args[0] = path.resolve(args[0]) ++ const token = input.method === "readFile" ++ ? readToken(args[0]) ++ : inputToken(ts.sys[input.method](...args)) ++ return token === input.token ++ }) ++} ++ ++function compile(code, lang, settings, runTwoSlash) { ++ const inputs = new Map() ++ const sys = { ...ts.sys } ++ // Misses and directory probes matter when new typings become available. ++ for (const method of [ ++ "readFile", "fileExists", "directoryExists", "getDirectories", ++ "readDirectory", "realpath", "resolvePath", ++ ]) { ++ if (!ts.sys[method]) continue ++ sys[method] = (...args) => { ++ const value = ts.sys[method](...args) ++ const input = { ++ method, ++ args: args.map((arg, i) => i === 0 ? portable(path.resolve(arg)) : arg), ++ undefinedArgs: args.flatMap((arg, i) => arg === undefined ? [i] : []), ++ } ++ const key = JSON.stringify(input) ++ input.token = method === "readFile" ++ ? rememberRead(args[0], value, fs.statSync(args[0], { throwIfNoEntry: false })) ++ : inputToken(value) ++ inputs.set(key, input) ++ return value ++ } ++ } ++ const options = { ...settings, tsModule: { ...ts, sys } } ++ const results = runTwoSlash(code, lang, options) ++ return { results, inputs: [...inputs.values()] } ++} ++ ++function cacheDirectory() { ++ if (process.versions.pnp) { ++ const pnp = require("pnpapi") ++ return path.join( ++ pnp.getPackageInformation(pnp.topLevel).packageLocation, ++ "node_modules", ".cache", "twoslash" ++ ) ++ } ++ return __dirname.includes("node_modules") ++ ? path.join(__dirname.split("node_modules")[0], "node_modules", ".cache", "twoslash") ++ : path.join(__dirname, "..", "..", ".cache", "twoslash") ++} ++ ++module.exports = function cachedTwoslash(code, lang, settings, runTwoSlash) { ++ if (settings.tsModule || settings.fsMap || settings.customTransformers || settings.lzstringModule) { ++ if (process.env.debug) console.log("Skipping Twoslash cache for custom compiler inputs") ++ return runTwoSlash(code, lang, settings) ++ } ++ const key = digest(JSON.stringify([code, lang, settings, compilerImplementation()])) ++ const directory = cacheDirectory() ++ const filename = path.join(directory, key + ".json") ++ if (fs.existsSync(filename)) { ++ const cached = JSON.parse(fs.readFileSync(filename, "utf8")) ++ if (inputsMatch(cached.inputs)) { ++ if (process.env.debug) console.log("Using cached twoslash results from " + filename) ++ const now = new Date() ++ fs.utimesSync(filename, now, now) ++ return cached.results ++ } ++ } ++ const cached = compile(code, lang, settings, runTwoSlash) ++ fs.mkdirSync(directory, { recursive: true }) ++ fs.writeFileSync(filename, JSON.stringify(cached), "utf8") ++ return cached.results ++} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index caa4de3cd806..fa0560b58e97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ patchedDependencies: hash: 3aa0dc2d9c0fd3921986acbadfef7a10df948fb30d02590f86cecab9b91800bf path: patches/gatsby-remark-shiki-twoslash@3.0.38.patch remark-shiki-twoslash@3.1.3: - hash: 6a88dcbef5f4a7db6aa5d7a1fbaf9a293d04068303b7400ccb46fbae51d3e8b7 + hash: 5bd6570cdb97b323924ced9d2e68aae4b5f7bea40b13ee9a3ca712e774883c30 path: patches/remark-shiki-twoslash@3.1.3.patch importers: @@ -84,7 +84,7 @@ importers: version: 13.0.2 remark-shiki-twoslash: specifier: ^3.1.3 - version: 3.1.3(patch_hash=6a88dcbef5f4a7db6aa5d7a1fbaf9a293d04068303b7400ccb46fbae51d3e8b7)(typescript@6.0.2) + version: 3.1.3(patch_hash=5bd6570cdb97b323924ced9d2e68aae4b5f7bea40b13ee9a3ca712e774883c30)(typescript@6.0.2) packages/ata: dependencies: @@ -16343,7 +16343,7 @@ snapshots: gatsby-remark-shiki-twoslash@3.0.38(patch_hash=3aa0dc2d9c0fd3921986acbadfef7a10df948fb30d02590f86cecab9b91800bf): dependencies: - remark-shiki-twoslash: 3.1.3(patch_hash=6a88dcbef5f4a7db6aa5d7a1fbaf9a293d04068303b7400ccb46fbae51d3e8b7)(typescript@6.0.2) + remark-shiki-twoslash: 3.1.3(patch_hash=5bd6570cdb97b323924ced9d2e68aae4b5f7bea40b13ee9a3ca712e774883c30)(typescript@6.0.2) typescript: 6.0.2 unist-util-visit: 2.0.3 @@ -20184,7 +20184,7 @@ snapshots: dependencies: mdast-util-to-nlcst: 4.0.1 - remark-shiki-twoslash@3.1.3(patch_hash=6a88dcbef5f4a7db6aa5d7a1fbaf9a293d04068303b7400ccb46fbae51d3e8b7)(typescript@6.0.2): + remark-shiki-twoslash@3.1.3(patch_hash=5bd6570cdb97b323924ced9d2e68aae4b5f7bea40b13ee9a3ca712e774883c30)(typescript@6.0.2): dependencies: '@types/unist': 2.0.11 '@typescript/twoslash': link:packages/ts-twoslasher @@ -21436,7 +21436,7 @@ snapshots: hast-util-to-html: 7.1.3 mdast-util-to-hast: 10.2.0 remark: 13.0.0 - remark-shiki-twoslash: 3.1.3(patch_hash=6a88dcbef5f4a7db6aa5d7a1fbaf9a293d04068303b7400ccb46fbae51d3e8b7)(typescript@6.0.2) + remark-shiki-twoslash: 3.1.3(patch_hash=5bd6570cdb97b323924ced9d2e68aae4b5f7bea40b13ee9a3ca712e774883c30)(typescript@6.0.2) unist-util-visit: 3.1.0 transitivePeerDependencies: - supports-color From 988d0aab0b64b3c06baab88c4ac3b4853d9af020 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 1 Oct 2026 07:54:16 -0700 Subject: [PATCH 10/14] Exercise restored website build caches From 2ed2488ed1e8fba79f1f2a661273ebc27c0e9c7b Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 1 Oct 2026 08:16:42 -0700 Subject: [PATCH 11/14] Generate the TSConfig reference just once per build Regenerating the same reference after the CLI documentation adds eight seconds to package preparation on CI without changing its output. --- packages/tsconfig-reference/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tsconfig-reference/package.json b/packages/tsconfig-reference/package.json index cc54552f67a8..7447712e62ee 100644 --- a/packages/tsconfig-reference/package.json +++ b/packages/tsconfig-reference/package.json @@ -12,7 +12,7 @@ "generate:md:tsconfig": "tsc --build && node scripts/tsconfig/generateMarkdown", "generate:md:cli": "tsc --build && node scripts/cli/generateMarkdown", "generate:md:msbuild": "tsc --build && node scripts/msbuild/generateMarkdown", - "generate:md": "pnpm generate:md:tsconfig && pnpm generate:md:cli && pnpm generate:md:tsconfig", + "generate:md": "pnpm generate:md:tsconfig && pnpm generate:md:cli", "download": "node scripts/schema/downloadSchemaBase.js", "bootstrap": "pnpm generate:json", "build": "pnpm generate:json && pnpm generate:md", From 40a711ad2ec946a0f5090778f88229816a38a357 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 1 Oct 2026 08:22:13 -0700 Subject: [PATCH 12/14] Reuse prepared packages when running CI tests CI already builds every package before building the site. Repeating the generators during tests adds 42-54 seconds without providing another check. Keep standalone tests unchanged. Validate generated documentation before running independent package tests with bounded concurrency, avoiding the unrelated dependency barriers in the test phase. --- .github/workflows/CI.yml | 2 +- package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 133168abb8ff..eaba1385e152 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -57,7 +57,7 @@ jobs: path: packages/typescriptlang-org/public # Run all the package's tests - - run: pnpm test + - run: pnpm test:ci - run: | git add . diff --git a/package.json b/package.json index c89c97ea466b..352b35b1ec80 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "clean": "pnpm run --filter=typescriptlang-org gatsby clean", "clean-twoslash": "rm -rf packages/.cache/twoslash", "test": "CI=true pnpm -r run test", + "test:ci": "CI=true pnpm -r --filter=glossary --filter=tsconfig-reference run lint && CI=true pnpm -r --filter=!root --filter=!glossary --filter=!tsconfig-reference --no-sort run test", "update-test-snapshots": "CI=true pnpm run --filter=@typescript/twoslash --filter=@typescript/vfs test -u" }, "repository": "microsoft/TypeScript-Website", From 905cbf2ceabb5be4b06d4db06593a6f53927e8f0 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 1 Oct 2026 08:24:20 -0700 Subject: [PATCH 13/14] Checkpoint compiled examples after validation Documentation linting compiles examples with different settings from the site build. Saving only before tests discards that work, costing 65-76 seconds on each CI run. Save a second bounded snapshot after successful tests with a distinct immutable key. Keep the earlier checkpoint so failed tests still preserve the completed site build. --- .github/workflows/CI.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index eaba1385e152..b27bd2b73546 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -59,6 +59,11 @@ jobs: # Run all the package's tests - run: pnpm test:ci + - name: Save website validation caches + uses: ./.github/actions/website-cache/save + with: + key: ${{ steps.website-cache.outputs.key }}-tests + - run: | git add . if ! git diff --staged --exit-code --quiet; then From 43ade33597ff1b3f5420ff43400df5d9bac6721d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:57:00 -0700 Subject: [PATCH 14/14] Run package validation independently of website builds The website and package tests do not need to share a runner or wait for each other. Run both operating-system matrices independently so package validation no longer extends the site build critical path. Keep Gatsby-dependent checks and documentation validation with the site, retain every required check, and preserve the CI workflow and site artifact consumed by preview deployment. No additional interbuild caches are needed. --- .github/workflows/CI.yml | 53 +++++++++++++++++++++++++++++++++++++--- package.json | 4 ++- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b27bd2b73546..81ab2f87ba44 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -11,7 +11,7 @@ defaults: shell: bash jobs: - tests: + site: strategy: fail-fast: false matrix: @@ -56,8 +56,8 @@ jobs: name: site path: packages/typescriptlang-org/public - # Run all the package's tests - - run: pnpm test:ci + # Validate documentation and types generated by Gatsby + - run: pnpm test:ci:site - name: Save website validation caches uses: ./.github/actions/website-cache/save @@ -81,7 +81,51 @@ jobs: if: ${{ failure() && steps.check-diff.conclusion == 'failure' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: missing.patch + name: missing.patch-site-${{ matrix.os }} + path: missing.patch + + tests: + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "20.x" + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm docs-sync pull microsoft/TypeScript-Website-localizations#main 1 + - run: pnpm bootstrap + - run: pnpm test:ci:packages + + - run: | + git add . + if ! git diff --staged --exit-code --quiet; then + echo "This PR is missing some generated changes. Please update locally or merge the patch artifact." + echo "" + git diff --staged + git diff --staged > missing.patch + exit 1 + fi + name: Check for uncommitted changes + id: check-diff + if: github.event_name == 'pull_request' + + - name: Upload diff artifact + if: ${{ failure() && steps.check-diff.conclusion == 'failure' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: missing.patch-packages-${{ matrix.os }} path: missing.patch changesets: @@ -117,6 +161,7 @@ jobs: runs-on: ubuntu-latest if: ${{ always() }} needs: + - site - tests - changesets diff --git a/package.json b/package.json index 352b35b1ec80..e3cc29de9812 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,9 @@ "clean": "pnpm run --filter=typescriptlang-org gatsby clean", "clean-twoslash": "rm -rf packages/.cache/twoslash", "test": "CI=true pnpm -r run test", - "test:ci": "CI=true pnpm -r --filter=glossary --filter=tsconfig-reference run lint && CI=true pnpm -r --filter=!root --filter=!glossary --filter=!tsconfig-reference --no-sort run test", + "test:ci": "pnpm test:ci:site && pnpm test:ci:packages", + "test:ci:site": "CI=true pnpm -r --filter=glossary --filter=tsconfig-reference run lint && CI=true pnpm --filter=typescriptlang-org test", + "test:ci:packages": "CI=true pnpm -r --filter=!root --filter=!typescriptlang-org --filter=!glossary --filter=!tsconfig-reference --no-sort run test", "update-test-snapshots": "CI=true pnpm run --filter=@typescript/twoslash --filter=@typescript/vfs test -u" }, "repository": "microsoft/TypeScript-Website",