diff --git a/.github/actions/website-cache/action.yml b/.github/actions/website-cache/action.yml new file mode 100644 index 000000000000..040d1d460f45 --- /dev/null +++ b/.github/actions/website-cache/action.yml @@ -0,0 +1,25 @@ +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: + - name: Get Node version + id: node + shell: bash + run: echo "version=$(node --version)" >> "$GITHUB_OUTPUT" + + - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + id: restore + with: + path: | + node_modules/.cache/twoslash + packages/typescriptlang-org/.cache/webpack + key: website-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.node.outputs.version }}-${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: | + website-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.node.outputs.version }}- 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/.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 bff849e3510a..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: @@ -24,22 +24,31 @@ 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 # Build the packages - run: pnpm bootstrap - - run: pnpm build + + - 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' @@ -47,8 +56,57 @@ jobs: name: site path: packages/typescriptlang-org/public - # Run all the package's tests - - run: pnpm test + # Validate documentation and types generated by Gatsby + - run: pnpm test:ci:site + + - 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 + 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-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 . @@ -67,7 +125,7 @@ jobs: if: ${{ failure() && steps.check-diff.conclusion == 'failure' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: missing.patch + name: missing.patch-packages-${{ matrix.os }} path: missing.patch changesets: @@ -103,6 +161,7 @@ jobs: runs-on: ubuntu-latest if: ${{ always() }} needs: + - site - tests - changesets diff --git a/.github/workflows/deploy-prod-static.yml b/.github/workflows/deploy-prod-static.yml index f70ec81bb839..b962fc44c5a8 100644 --- a/.github/workflows/deploy-prod-static.yml +++ b/.github/workflows/deploy-prod-static.yml @@ -31,25 +31,36 @@ 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 - # 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 - pnpm build + - name: Build packages + run: pnpm bootstrap - - name: Makes the site - run: | - pnpm build-site - cp -r packages/typescriptlang-org/public site + - name: Restore website build caches + id: website-cache + uses: ./.github/actions/website-cache + + - name: Set up playground cache busting + run: pnpm run --filter=typescriptlang-org setup-playground-cache-bust + + - 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 @@ -57,7 +68,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/.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/package.json b/package.json index 200cf3cd8336..e3cc29de9812 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", @@ -67,7 +68,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", @@ -76,6 +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": "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", 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", 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..b90024399937 --- /dev/null +++ b/packages/typescriptlang-org/lib/bootup/shiki.test.js @@ -0,0 +1,465 @@ +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) +}) + +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 utimesSync = fs.utimesSync + 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) + }) + jest.spyOn(fs, "utimesSync").mockImplementation((path, ...args) => { + if (!isCache(path)) return utimesSync(path, ...args) + }) + runTwoSlash = jest + .spyOn(shikiTwoslash, "runTwoSlash") + .mockImplementation((code, lang, { tsModule, ...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) + expect(fs.utimesSync).toHaveBeenCalledTimes(1) + expect(fs.utimesSync).toHaveBeenCalledWith( + expect.any(String), + expect.any(Date), + expect.any(Date) + ) + }) + + it("distinguishes TypeScript from JavaScript", () => { + runTwoSlashOnNode("const value = 1", fence("ts"), {}) + runTwoSlashOnNode("const value = 1", fence("js"), {}) + 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"), { + 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) + }) + + 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" + ) + }) + + 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", () => { + 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/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/patches/remark-shiki-twoslash@3.1.3.patch b/patches/remark-shiki-twoslash@3.1.3.patch new file mode 100644 index 000000000000..2b3d5adcc5e6 --- /dev/null +++ b/patches/remark-shiki-twoslash@3.1.3.patch @@ -0,0 +1,290 @@ +diff --git a/dist/remark-shiki-twoslash.cjs.development.js b/dist/remark-shiki-twoslash.cjs.development.js +index efaed7711553c44c31abcaf53abab08b8780b248..782e3362bfd5be74d087c01571ce7e858697dec7 100644 +--- a/dist/remark-shiki-twoslash.cjs.development.js ++++ b/dist/remark-shiki-twoslash.cjs.development.js +@@ -808,57 +808,7 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { + return shikiTwoslash.runTwoSlash(code, lang, settings); + } + +- 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 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")); +- } 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..2df325d745cc341393c3c38c72ad29342f2631b7 100644 +--- a/dist/remark-shiki-twoslash.cjs.production.min.js ++++ b/dist/remark-shiki-twoslash.cjs.production.min.js +@@ -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);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..cabf08a27259ac62316f3459c95ecd5bf9ae0082 100644 +--- a/dist/remark-shiki-twoslash.esm.js ++++ b/dist/remark-shiki-twoslash.esm.js +@@ -802,57 +802,7 @@ var cachedTwoslashCall = function cachedTwoslashCall(code, lang, settings) { + return runTwoSlash(code, lang, settings); + } + +- 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 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")); +- } 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 0c8438854935..fa0560b58e97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,11 @@ overrides: patchedDependencies: gatsby-remark-shiki-twoslash@3.0.38: - hash: 5584d3c994349e1a6796ab8e22811fc912fad257e9ad818930836c458ab23640 + hash: 3aa0dc2d9c0fd3921986acbadfef7a10df948fb30d02590f86cecab9b91800bf path: patches/gatsby-remark-shiki-twoslash@3.0.38.patch + remark-shiki-twoslash@3.1.3: + hash: 5bd6570cdb97b323924ced9d2e68aae4b5f7bea40b13ee9a3ca712e774883c30 + 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=5bd6570cdb97b323924ced9d2e68aae4b5f7bea40b13ee9a3ca712e774883c30)(typescript@6.0.2) packages/ata: dependencies: @@ -395,7 +398,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,9 +16341,9 @@ 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) + remark-shiki-twoslash: 3.1.3(patch_hash=5bd6570cdb97b323924ced9d2e68aae4b5f7bea40b13ee9a3ca712e774883c30)(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=5bd6570cdb97b323924ced9d2e68aae4b5f7bea40b13ee9a3ca712e774883c30)(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=5bd6570cdb97b323924ced9d2e68aae4b5f7bea40b13ee9a3ca712e774883c30)(typescript@6.0.2) unist-util-visit: 3.1.0 transitivePeerDependencies: - supports-color