Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/.vitepress/twoslash-renderer.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Temporary regression coverage for the downstream patch from
// https://github.com/shikijs/shiki/pull/1349
// Remove this file and its mise task after Fedify upgrades to a release
// containing the fix and removes the downstream patch.

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { transformerTwoslash } from "@shikijs/vitepress-twoslash";
import { createMarkdownRenderer, disposeMdItInstance } from "vitepress";

const source =
'/** **Documentation** with "quotes", &, and 한글. */\nconst value = 1;\nconsole.log(value);';

describe("installed Twoslash popup optimization", () => {
it("should optimize ordinary fences through VitePress's own highlighter", async () => {
const html = await renderFence("ts");
assert.match(html, /v-html=/);
assert.ok(!html.includes("<strong>Documentation</strong>"));
assert.ok(html.includes("Documentation"));
});

it("should retain popup descendants when explicitly disabled", async () => {
const html = await renderFence("ts", false);
assert.ok(!html.includes("v-html="));
assert.ok(html.includes("<strong>Documentation</strong>"));
});

it("should retain the original output for interpolated fences", async () => {
const html = await renderFence("ts-vue");
assert.ok(!html.includes("v-html="));
assert.equal(html, await renderFence("ts-vue", false));
});
});

// Helpers
async function renderFence(
lang: string,
optimizePopups = true,
): Promise<string> {
const markdown = await createMarkdownRenderer(process.cwd(), {
theme: "github-dark",
languages: ["ts", "js"],
codeTransformers: [transformerTwoslash({ optimizePopups })],
});
try {
return await markdown.renderAsync(
`\`\`\`${lang} twoslash\n${source}\n\`\`\``,
);
} finally {
disposeMdItInstance();
}
}
19 changes: 19 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,22 @@ mise run docs # Start development server

Always run `mise run docs:build` before committing to catch Twoslash type
errors.


Temporary popup optimization
----------------------------

The root *package.json* registers a pnpm patch for
`@shikijs/vitepress-twoslash@4.6.0`, ported from
[shikijs/shiki#1349] at commit
`a5afc63679578b5d197665a8286ada283e76347b`. It compiles static popup contents
as HTML strings to reduce the Vue compiler's memory use. Interpolated fences
and custom renderers retain their original output. The package is pinned to
the exact patched version so an update cannot silently drop the workaround.

Run `mise run test:docs-twoslash` to check the installed patch through
VitePress's highlighter. Remove the patch, its registration, and the temporary
regression test and its task and type-check entries only after upgrading to a
release containing the fix and verifying `mise run docs:build`.

[shikijs/shiki#1349]: https://github.com/shikijs/shiki/pull/1349
4 changes: 2 additions & 2 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@opentelemetry/sdk-node": "catalog:",
"@opentelemetry/sdk-trace-base": "catalog:",
"@sentry/node": "^8.47.0",
"@shikijs/vitepress-twoslash": "^4.3.1",
"@shikijs/vitepress-twoslash": "4.6.0",
"@teidesu/deno-types": "^2.1.4",
"@types/amqplib": "catalog:",
"@types/better-sqlite3": "catalog:",
Expand Down Expand Up @@ -79,7 +79,7 @@
"stringify-entities": "^4.0.4",
"temporal-polyfill": "catalog:",
"typescript": "catalog:",
"vitepress": "^2.0.0-alpha.18",
"vitepress": "^2.0.0-alpha.21",
"vitepress-plugin-group-icons": "^1.7.5",
"vitepress-plugin-llms": "^1.13.1",
"vitepress-plugin-mermaid": "^2.0.17",
Expand Down
5 changes: 5 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ depends = [
"check:fixture-usage",
"check:node-modules",
"test:docs-contributors",
"test:docs-twoslash",
"test:codegen",
"test:changelog-pr-refs",
"test:tooling",
Expand Down Expand Up @@ -517,6 +518,10 @@ run = "pnpm -C docs build"
description = "Test the team page contributor loading and filtering"
run = "node --experimental-strip-types --test docs/.vitepress/contributors.test.mts"

[tasks."test:docs-twoslash"]
description = "Test the installed Twoslash popup optimization through VitePress"
run = "node --experimental-strip-types --test docs/.vitepress/twoslash-renderer.test.mts"

[tasks."docs:preview"]
description = "Preview the built documentation"
run = "pnpm -C docs preview"
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"esbuild"
],
"patchedDependencies": {
"@netlify/blobs@11.0.3": "patches/@netlify__blobs@11.0.3.patch"
"@netlify/blobs@11.0.3": "patches/@netlify__blobs@11.0.3.patch",
"@shikijs/vitepress-twoslash@4.6.0": "patches/@shikijs__vitepress-twoslash@4.6.0.patch"
}
},
"type": "module"
Expand Down
128 changes: 128 additions & 0 deletions patches/@shikijs__vitepress-twoslash@4.6.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index dbc928b18c143e3e281c95108568894cc0ee1cb9..27ef389ad94cf09948c65fb156218dbd9cd883d3 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1,10 +1,26 @@
import { createTransformerFactory } from "@shikijs/twoslash/core";
import { removeTwoslashNotations } from "twoslash";
import { createTwoslasher } from "twoslash-vue";
+import { hastToHtml } from "shiki/core";
import { defaultHoverInfoProcessor, rendererRich } from "@shikijs/twoslash";
import { fromMarkdown } from "mdast-util-from-markdown";
import { gfmFromMarkdown } from "mdast-util-gfm";
import { defaultHandlers, toHast } from "mdast-util-to-hast";
+// src/popup-optimization.ts
+const popups = /* @__PURE__ */ new WeakSet();
+function registerPopup(node) {
+ popups.add(node);
+}
+function optimizePopupContent(node) {
+ if (node.type === "element" && popups.has(node)) {
+ node.properties["v-html"] = JSON.stringify(hastToHtml(node.children)).replaceAll("<", "\\u003c");
+ delete node.properties["v-pre"];
+ node.children = [];
+ return;
+ }
+ for (const child of node.children ?? []) if (child.type === "element") optimizePopupContent(child);
+ if (node.type === "element" && node.content) optimizePopupContent(node.content);
+}
// src/renderer-floating-vue.ts
const RE_JSDOC_LINK = /\{@link ([^}]*)\}/g;
const RE_PARAM_NAME = /^([\w$-]+)/;
@@ -80,6 +96,7 @@ function rendererFloatingVue(options = {}) {
}
function vPre(el) {
if (el.type === "element") {
+ registerPopup(el);
el.properties = el.properties || {};
el.properties["v-pre"] = "";
}
@@ -134,6 +151,12 @@ function compose(parts) {
const RE_NEWLINE = /\n/g;
const RE_TWOSLASH = /\btwoslash\b/;
const RE_LEFT_BRACE = /\{/g;
+const knownPreHooks = /* @__PURE__ */ new Set([
+ "vitepress:add-class",
+ "vitepress:add-dir",
+ "vitepress:v-pre"
+]);
+
/**
* Create a Shiki transformer for VitePress to enable twoslash integration
*
@@ -166,21 +189,53 @@ function transformerTwoslash(options = {}) {
explicitTrigger
});
const trigger = explicitTrigger instanceof RegExp ? explicitTrigger : RE_TWOSLASH;
- return {
+ const retainedVPre = /* @__PURE__ */ new WeakSet();
+ function canOptimize(transformers, own) {
+ if (options.optimizePopups === false || options.renderer != null || !transformers) return false;
+ const ownIndex = transformers.indexOf(own);
+ const vPreHooks = transformers.filter((i) => i.name === "vitepress:v-pre");
+ if (ownIndex < 0 || vPreHooks.length !== 1 || transformers.indexOf(vPreHooks[0]) >= ownIndex) return false;
+ return transformers.every((item, index) => !item.pre || item.enforce == null && (index >= ownIndex || knownPreHooks.has(item.name || "")));
+ }
+ const transformer = {
...twoslash,
name: "@shikijs/vitepress-twoslash",
preprocess(code, options) {
+ retainedVPre.delete(this.meta);
const cleanup = options.transformers?.find((i) => i.name === "vitepress:clean-up");
if (cleanup) options.transformers?.splice(options.transformers.indexOf(cleanup), 1);
if (!explicitTrigger || options.meta?.__raw?.match(trigger)) {
const vPre = options.transformers?.find((i) => i.name === "vitepress:v-pre");
- if (vPre) options.transformers?.splice(options.transformers.indexOf(vPre), 1);
+ if (vPre) {
+ if (options.structure !== "inline" && canOptimize(options.transformers, transformer)) retainedVPre.add(this.meta);
+ else options.transformers?.splice(options.transformers.indexOf(vPre), 1);
+ }
}
return twoslash.preprocess.call(this, code, options);
},
+ pre(node) {
+ node = twoslash.pre?.call(this, node) || node;
+ if (retainedVPre.has(this.meta)) {
+ const isStatic = "v-pre" in node.properties;
+ delete node.properties["v-pre"];
+ if (isStatic && this.meta.twoslash && canOptimize(this.options.transformers, transformer)) optimizePopupContent(node);
+ }
+ return node;
+ },
+ root(node) {
+ if (retainedVPre.has(this.meta)) {
+ const transformers = this.options.transformers || [];
+ if (transformers.findIndex((i) => i.name === "vitepress:v-pre") > transformers.indexOf(transformer)) {
+ for (const child of node.children) if (child.type === "element" && child.tagName === "pre") delete child.properties["v-pre"];
+ }
+ retainedVPre.delete(this.meta);
+ }
+ return twoslash.root?.call(this, node) || node;
+ },
postprocess(html) {
if (this.meta.twoslash) return html.replace(RE_LEFT_BRACE, "&#123;");
}
};
+ return transformer;
}
export { defaultHoverInfoProcessor, rendererFloatingVue, transformerTwoslash };
diff --git a/dist/types-BNt51DSh.d.mts b/dist/types-BNt51DSh.d.mts
index 80f853003724c49b6a16783d374739b90457a005..b67ba4de7f8ecca78868b8fdde13fe1da10dd571 100644
--- a/dist/types-BNt51DSh.d.mts
+++ b/dist/types-BNt51DSh.d.mts
@@ -20,6 +20,15 @@ interface TransformerTwoslashVueOptions extends TransformerTwoslashOptions {
twoslashOptions?: TransformerTwoslashOptions['twoslashOptions'] & VueSpecificOptions;
}
interface VitePressPluginTwoslashOptions extends TransformerTwoslashVueOptions, TwoslashFloatingVueRendererOptions {
+ /**
+ * Compile default-renderer popups as static HTML in ordinary VitePress fences.
+ * Interpolated fences, custom renderers and unrecognized hook orders retain
+ * their full HAST. Disable this to inspect popup descendants in later hooks
+ * or apply scoped SFC styles to them.
+ * @default true
+ */
+ optimizePopups?: boolean;
+
/**
* Requires adding `twoslash` to the code block explicitly to run twoslash
* @default true
Loading
Loading