vocs 2.0.7 → 2.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/internal/mdx.d.ts.map +1 -1
  2. package/dist/internal/mdx.js +4 -1
  3. package/dist/internal/mdx.js.map +1 -1
  4. package/dist/internal/shiki-transformers.d.ts.map +1 -1
  5. package/dist/internal/shiki-transformers.js +7 -2
  6. package/dist/internal/shiki-transformers.js.map +1 -1
  7. package/dist/internal/twoslash/inline-cache.d.ts +10 -0
  8. package/dist/internal/twoslash/inline-cache.d.ts.map +1 -1
  9. package/dist/internal/twoslash/inline-cache.js +56 -20
  10. package/dist/internal/twoslash/inline-cache.js.map +1 -1
  11. package/dist/react/internal/CodeToHtml.client.d.ts +27 -0
  12. package/dist/react/internal/CodeToHtml.client.d.ts.map +1 -0
  13. package/dist/react/internal/CodeToHtml.client.js +138 -0
  14. package/dist/react/internal/CodeToHtml.client.js.map +1 -0
  15. package/dist/react/internal/CodeToHtml.d.ts +1 -7
  16. package/dist/react/internal/CodeToHtml.d.ts.map +1 -1
  17. package/dist/react/internal/CodeToHtml.js +1 -56
  18. package/dist/react/internal/CodeToHtml.js.map +1 -1
  19. package/dist/react/internal/TwoslashHover.client.d.ts.map +1 -1
  20. package/dist/react/internal/TwoslashHover.client.js +7 -1
  21. package/dist/react/internal/TwoslashHover.client.js.map +1 -1
  22. package/dist/styles/twoslash.css +32 -0
  23. package/package.json +1 -1
  24. package/src/internal/mdx.ts +4 -1
  25. package/src/internal/shiki-transformers.ts +7 -2
  26. package/src/internal/twoslash/inline-cache.test.ts +80 -0
  27. package/src/internal/twoslash/inline-cache.ts +59 -26
  28. package/src/react/internal/CodeToHtml.client.tsx +176 -0
  29. package/src/react/internal/CodeToHtml.tsx +1 -74
  30. package/src/react/internal/TwoslashHover.client.tsx +8 -1
  31. package/src/styles/twoslash.css +32 -0
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Lazily highlights code on the client.
3
+ *
4
+ * Twoslash hover popups embed a code snippet (the resolved type) for every
5
+ * hoverable token. Highlighting these with Shiki on the server forces RSC to
6
+ * render and serialize a fully highlighted snippet for every token across every
7
+ * page at build time — which dominates build memory and time even though the
8
+ * popups are only ever seen on hover.
9
+ *
10
+ * Instead we render the snippet as plain text in the RSC payload and highlight
11
+ * it on the client. Because the popup only mounts when it opens, the Shiki
12
+ * bundle is dynamically imported and the snippet highlighted on demand.
13
+ */
14
+ export declare function CodeToHtml(props: CodeToHtml.Props): import("react/jsx-runtime").JSX.Element;
15
+ export declare namespace CodeToHtml {
16
+ type Props = {
17
+ code: string;
18
+ lang: string;
19
+ };
20
+ }
21
+ /** Returns the highlighted HTML for a snippet if it has already been computed. */
22
+ export declare function getCached(code: string, lang: string): string | undefined;
23
+ /** Highlights a snippet (memoized), loading the Shiki bundle on first use. */
24
+ export declare function highlight(code: string, lang: string): Promise<string>;
25
+ /** Eagerly loads the Shiki bundle so the first real highlight is fast. */
26
+ export declare function prewarm(): void;
27
+ //# sourceMappingURL=CodeToHtml.client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CodeToHtml.client.d.ts","sourceRoot":"","sources":["../../../src/react/internal/CodeToHtml.client.tsx"],"names":[],"mappings":"AAKA;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,2CA2BjD;AAmCD,yBAAiB,UAAU,CAAC;IAC1B,KAAY,KAAK,GAAG;QAClB,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;CACF;AA0DD,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,sBAEnD;AAED,8EAA8E;AAC9E,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,mBAQzD;AAED,0EAA0E;AAC1E,wBAAgB,OAAO,SAEtB"}
@@ -0,0 +1,138 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { config } from 'virtual:vocs/config';
4
+ import { useEffect, useState } from 'react';
5
+ /**
6
+ * Lazily highlights code on the client.
7
+ *
8
+ * Twoslash hover popups embed a code snippet (the resolved type) for every
9
+ * hoverable token. Highlighting these with Shiki on the server forces RSC to
10
+ * render and serialize a fully highlighted snippet for every token across every
11
+ * page at build time — which dominates build memory and time even though the
12
+ * popups are only ever seen on hover.
13
+ *
14
+ * Instead we render the snippet as plain text in the RSC payload and highlight
15
+ * it on the client. Because the popup only mounts when it opens, the Shiki
16
+ * bundle is dynamically imported and the snippet highlighted on demand.
17
+ */
18
+ export function CodeToHtml(props) {
19
+ const { code, lang } = props;
20
+ // Initialize from the cache so a snippet that was already highlighted (e.g.
21
+ // pre-highlighted by the popover before opening, or shown previously) renders
22
+ // highlighted immediately without a plain-text flash.
23
+ const [html, setHtml] = useState(() => getCached(code, lang) ?? null);
24
+ useEffect(() => {
25
+ if (html)
26
+ return;
27
+ let cancelled = false;
28
+ highlight(code, lang)
29
+ .then((result) => {
30
+ if (!cancelled)
31
+ setHtml(result);
32
+ })
33
+ .catch(() => { });
34
+ return () => {
35
+ cancelled = true;
36
+ };
37
+ }, [code, lang, html]);
38
+ if (html)
39
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: highlighted by Shiki
40
+ return _jsx("div", { dangerouslySetInnerHTML: { __html: html } });
41
+ // While highlighting, render a skeleton that mirrors the snippet's line
42
+ // structure so the popup is shown immediately without a layout shift.
43
+ return _jsx(Skeleton, { code: code });
44
+ }
45
+ function Skeleton(props) {
46
+ const lines = props.code.split('\n');
47
+ return (_jsx("div", { "data-v-code-skeleton": true, "aria-hidden": "true", children: _jsxs("pre", { className: "shiki", "data-v-overflow-fade": true, children: [_jsx("code", { children: lines.map((line, index) => {
48
+ const indent = line.length - line.trimStart().length;
49
+ const length = line.trim().length;
50
+ return (_jsx("span", { className: "line", children: length > 0 && (_jsx("span", { "data-v-skeleton-bar": true, style: {
51
+ marginLeft: `${indent}ch`,
52
+ width: `${Math.min(length, 48)}ch`,
53
+ } })) }, index));
54
+ }) }), _jsx("div", { "data-v-overflow-sentinel": true })] }) }));
55
+ }
56
+ let highlighterPromise;
57
+ function getHighlighter() {
58
+ if (!highlighterPromise)
59
+ highlighterPromise = (async () => {
60
+ const { bundledLanguages, createHighlighter, hastToHtml } = await import('shiki/bundle/web');
61
+ const { codeHighlight } = config;
62
+ const { langAlias = {}, themes } = codeHighlight;
63
+ // Note: `langAlias` is intentionally not passed to the highlighter.
64
+ // Passing a custom `langAlias` registers languages under their alias name
65
+ // when lazily loaded (e.g. `loadLanguage('ts')` registers as `ts` instead
66
+ // of `typescript`), which then makes `codeToHast({ lang })` fail to resolve
67
+ // the grammar. We resolve aliases to their base language ourselves below.
68
+ const highlighter = await createHighlighter({
69
+ themes: Object.values(themes),
70
+ langs: [],
71
+ });
72
+ return {
73
+ async codeToHtml(code, lang) {
74
+ const base = langAlias[lang] ?? lang;
75
+ const resolvedLang = base in bundledLanguages ? base : 'txt';
76
+ if (!highlighter.getLoadedLanguages().includes(resolvedLang))
77
+ await highlighter.loadLanguage(resolvedLang);
78
+ const hast = highlighter.codeToHast(code, {
79
+ defaultColor: 'light-dark()',
80
+ lang: resolvedLang,
81
+ rootStyle: false,
82
+ meta: { 'data-v-overflow-fade': true },
83
+ themes,
84
+ transformers: [transformerShrinkIndent()],
85
+ });
86
+ const pre = hast.children[0];
87
+ if (pre && pre.type === 'element' && pre.tagName === 'pre')
88
+ pre.children.push({
89
+ type: 'element',
90
+ tagName: 'div',
91
+ properties: { 'data-v-overflow-sentinel': true },
92
+ children: [],
93
+ });
94
+ return hastToHtml(hast);
95
+ },
96
+ };
97
+ })();
98
+ return highlighterPromise;
99
+ }
100
+ const cache = new Map();
101
+ function cacheKey(code, lang) {
102
+ return `${lang}\n${code}`;
103
+ }
104
+ /** Returns the highlighted HTML for a snippet if it has already been computed. */
105
+ export function getCached(code, lang) {
106
+ return cache.get(cacheKey(code, lang));
107
+ }
108
+ /** Highlights a snippet (memoized), loading the Shiki bundle on first use. */
109
+ export async function highlight(code, lang) {
110
+ const key = cacheKey(code, lang);
111
+ const cached = cache.get(key);
112
+ if (cached !== undefined)
113
+ return cached;
114
+ const highlighter = await getHighlighter();
115
+ const html = await highlighter.codeToHtml(code, lang);
116
+ cache.set(key, html);
117
+ return html;
118
+ }
119
+ /** Eagerly loads the Shiki bundle so the first real highlight is fast. */
120
+ export function prewarm() {
121
+ void getHighlighter();
122
+ }
123
+ function transformerShrinkIndent() {
124
+ return {
125
+ name: 'indent',
126
+ span(hast) {
127
+ const child = hast.children[0];
128
+ if (!child)
129
+ return;
130
+ if (child.type !== 'text')
131
+ return;
132
+ if (!child.value)
133
+ return;
134
+ hast.children[0] = { type: 'text', value: child.value.replace(/\s\s/g, ' ') };
135
+ },
136
+ };
137
+ }
138
+ //# sourceMappingURL=CodeToHtml.client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CodeToHtml.client.js","sourceRoot":"","sources":["../../../src/react/internal/CodeToHtml.client.tsx"],"names":[],"mappings":"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"}
@@ -1,8 +1,2 @@
1
- export declare function CodeToHtml(props: CodeToHtml.Props): Promise<import("react/jsx-runtime").JSX.Element>;
2
- export declare namespace CodeToHtml {
3
- type Props = {
4
- code: string;
5
- lang: string;
6
- };
7
- }
1
+ export { CodeToHtml } from './CodeToHtml.client.js';
8
2
  //# sourceMappingURL=CodeToHtml.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CodeToHtml.d.ts","sourceRoot":"","sources":["../../../src/react/internal/CodeToHtml.tsx"],"names":[],"mappings":"AAYA,wBAAsB,UAAU,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,oDAyCvD;AAED,yBAAiB,UAAU,CAAC;IAC1B,KAAY,KAAK,GAAG;QAClB,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;CACF"}
1
+ {"version":3,"file":"CodeToHtml.d.ts","sourceRoot":"","sources":["../../../src/react/internal/CodeToHtml.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA"}
@@ -1,57 +1,2 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { config } from 'virtual:vocs/config';
3
- import { langs as virtualLangs } from 'virtual:vocs/langs';
4
- import { bundledLanguages, createHighlighter, hastToHtml, makeSingletonHighlighter, } from 'shiki/bundle/web';
5
- const getHighlighter = makeSingletonHighlighter(createHighlighter);
6
- export async function CodeToHtml(props) {
7
- const { code, lang } = props;
8
- const { codeHighlight } = config;
9
- const { langAlias = {}, themes } = codeHighlight;
10
- const highlighter = await getHighlighter({
11
- themes: import.meta.env.DEV ? ['none'] : Object.values(themes),
12
- langs: import.meta.env.DEV
13
- ? ['txt']
14
- : [...Object.keys(bundledLanguages), ...virtualLangs],
15
- langAlias,
16
- });
17
- const loadedLangs = highlighter.getLoadedLanguages();
18
- const resolvedLang = loadedLangs.includes(lang) ? lang : 'txt';
19
- const hast = highlighter.codeToHast(code, {
20
- defaultColor: 'light-dark()',
21
- lang: import.meta.env.DEV ? 'txt' : resolvedLang,
22
- rootStyle: false,
23
- meta: {
24
- 'data-v-overflow-fade': true,
25
- },
26
- ...(import.meta.env.DEV ? { theme: 'none' } : { themes }),
27
- transformers: [transformerShrinkIndent()],
28
- });
29
- // Add overflow sentinel
30
- const pre = hast.children[0];
31
- if (pre && pre.type === 'element' && pre.tagName === 'pre')
32
- pre.children.push({
33
- type: 'element',
34
- tagName: 'div',
35
- properties: { 'data-v-overflow-sentinel': true },
36
- children: [],
37
- });
38
- const html = hastToHtml(hast);
39
- // biome-ignore lint/security/noDangerouslySetInnerHtml: _
40
- return _jsx("div", { dangerouslySetInnerHTML: { __html: html } });
41
- }
42
- function transformerShrinkIndent() {
43
- return {
44
- name: 'indent',
45
- span(hast) {
46
- const child = hast.children[0];
47
- if (!child)
48
- return;
49
- if (child.type !== 'text')
50
- return;
51
- if (!child.value)
52
- return;
53
- hast.children[0] = { type: 'text', value: child.value.replace(/\s\s/g, ' ') };
54
- },
55
- };
56
- }
1
+ export { CodeToHtml } from './CodeToHtml.client.js';
57
2
  //# sourceMappingURL=CodeToHtml.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CodeToHtml.js","sourceRoot":"","sources":["../../../src/react/internal/CodeToHtml.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CodeToHtml.js","sourceRoot":"","sources":["../../../src/react/internal/CodeToHtml.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"TwoslashHover.client.d.ts","sourceRoot":"","sources":["../../../src/react/internal/TwoslashHover.client.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAGnC,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,2CA+BvD;AAED,yBAAiB,aAAa,CAAC;IAC7B,KAAY,KAAK,GAAG;QAClB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC9B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;QACzB,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;QAC1B,OAAO,EAAE,KAAK,CAAC,SAAS,CAAA;KACzB,CAAA;IAED,SAAgB,eAAe;uBACK,cAAc,GAAG,IAAI;MAwBxD;CACF"}
1
+ {"version":3,"file":"TwoslashHover.client.d.ts","sourceRoot":"","sources":["../../../src/react/internal/TwoslashHover.client.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAInC,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,2CAqCvD;AAED,yBAAiB,aAAa,CAAC;IAC7B,KAAY,KAAK,GAAG;QAClB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC9B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;QACzB,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;QAC1B,OAAO,EAAE,KAAK,CAAC,SAAS,CAAA;KACzB,CAAA;IAED,SAAgB,eAAe;uBACK,cAAc,GAAG,IAAI;MAwBxD;CACF"}
@@ -1,11 +1,17 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Popover } from '@base-ui/react/popover';
4
- import { useCallback } from 'react';
4
+ import { useCallback, useEffect } from 'react';
5
+ import { prewarm } from './CodeToHtml.client.js';
5
6
  export function TwoslashHover(props) {
6
7
  const { className = '', children, trigger } = props;
7
8
  const { ref } = TwoslashHover.useOverflowFade();
8
9
  const open = className?.includes('twoslash-query-persisted');
10
+ // Warm the highlighter so the code inside the popup highlights as quickly as
11
+ // possible (the popup shows a skeleton placeholder until then).
12
+ useEffect(() => {
13
+ prewarm();
14
+ }, []);
9
15
  return (_jsxs(Popover.Root, { ...(open ? { open } : {}), children: [_jsx(Popover.Trigger, { "data-v-twoslash-trigger": true, openOnHover: true, delay: 0, children: _jsx("span", { children: trigger }) }), _jsx(Popover.Portal, { children: _jsx(Popover.Positioner, { align: "start", side: "bottom", sideOffset: 4, collisionAvoidance: { side: 'none' }, children: _jsxs(Popover.Popup, { className: className, initialFocus: false, children: [_jsx(Popover.Arrow, { className: "vocs:data-[side=bottom]:top-[-8px] vocs:data-[side=left]:right-[-13px] vocs:data-[side=left]:rotate-90 vocs:data-[side=right]:left-[-13px] vocs:data-[side=right]:-rotate-90 vocs:data-[side=top]:bottom-[-8px] vocs:data-[side=top]:rotate-180", children: _jsx(ArrowSvg, {}) }), _jsx("div", { "data-v-content": true, ref: ref, children: children })] }) }) })] }));
10
16
  }
11
17
  (function (TwoslashHover) {
@@ -1 +1 @@
1
- {"version":3,"file":"TwoslashHover.client.js","sourceRoot":"","sources":["../../../src/react/internal/TwoslashHover.client.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TwoslashHover.client.js","sourceRoot":"","sources":["../../../src/react/internal/TwoslashHover.client.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAEhD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAEhD,MAAM,UAAU,aAAa,CAAC,KAA0B;IACtD,MAAM,EAAE,SAAS,GAAG,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,KAAK,CAAA;IAEnD,MAAM,EAAE,GAAG,EAAE,GAAG,aAAa,CAAC,eAAe,EAAE,CAAA;IAE/C,MAAM,IAAI,GAAG,SAAS,EAAE,QAAQ,CAAC,0BAA0B,CAAC,CAAA;IAE5D,6EAA6E;IAC7E,gEAAgE;IAChE,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,EAAE,CAAA;IACX,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO,CACL,MAAC,OAAO,CAAC,IAAI,OAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aACtC,KAAC,OAAO,CAAC,OAAO,qCAAyB,WAAW,QAAC,KAAK,EAAE,CAAC,YAC3D,yBAAO,OAAO,GAAQ,GACN,EAClB,KAAC,OAAO,CAAC,MAAM,cACb,KAAC,OAAO,CAAC,UAAU,IACjB,KAAK,EAAC,OAAO,EACb,IAAI,EAAC,QAAQ,EACb,UAAU,EAAE,CAAC,EACb,kBAAkB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,YAEpC,MAAC,OAAO,CAAC,KAAK,IAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,aACtD,KAAC,OAAO,CAAC,KAAK,IAAC,SAAS,EAAC,iPAAiP,YACxQ,KAAC,QAAQ,KAAG,GACE,EAChB,sCAAoB,GAAG,EAAE,GAAG,YACzB,QAAQ,GACL,IACQ,GACG,GACN,IACJ,CAChB,CAAA;AACH,CAAC;AAED,WAAiB,aAAa;IAQ5B,SAAgB,eAAe;QAC7B,MAAM,GAAG,GAAG,WAAW,CAAC,CAAC,OAA8B,EAAE,EAAE;YACzD,IAAI,CAAC,OAAO;gBAAE,OAAM;YACpB,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC,wBAAwB,CAAC,CAAA;YACnE,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;gBAC1B,IAAI,CAAC,CAAC,EAAE,YAAY,WAAW,CAAC;oBAAE,SAAQ;gBAC1C,IAAI,EAAE,CAAC,YAAY,IAAI,EAAE,CAAC,YAAY;oBAAE,SAAQ;gBAEhD,MAAM,QAAQ,GAAG,EAAE,CAAC,aAAa,CAAC,4BAA4B,CAAC,CAAA;gBAC/D,IAAI,CAAC,QAAQ;oBAAE,SAAQ;gBAEvB,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,MAAM,CAAA;gBAEhC,MAAM,QAAQ,GAAG,IAAI,oBAAoB,CACvC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE;oBACV,IAAI,KAAK,EAAE,cAAc;wBAAE,OAAO,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;;wBACpD,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,MAAM,CAAA;gBACvC,CAAC,EACD,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,CAC7B,CAAA;gBACD,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;YAC5B,CAAC;QACH,CAAC,EAAE,EAAE,CAAC,CAAA;QAEN,OAAO,EAAE,GAAG,EAAE,CAAA;IAChB,CAAC;IAzBe,6BAAe,kBAyB9B,CAAA;AACH,CAAC,EAlCgB,aAAa,KAAb,aAAa,QAkC7B;AAED,SAAS,QAAQ,CAAC,KAAkC;IAClD,OAAO,CACL,eAAK,KAAK,EAAC,IAAI,EAAC,MAAM,EAAC,IAAI,EAAC,OAAO,EAAC,WAAW,EAAC,IAAI,EAAC,MAAM,KAAK,KAAK,aACnE,oCAAoB,EACpB,eACE,SAAS,EAAC,6CAA6C,EACvD,CAAC,EAAC,iMAAiM,GACnM,EACF,eACE,SAAS,EAAC,yCAAyC,EACnD,CAAC,EAAC,sTAAsT,GACxT,IACE,CACP,CAAA;AACH,CAAC"}
@@ -63,6 +63,38 @@
63
63
  }
64
64
  }
65
65
 
66
+ [data-v-code-skeleton] [data-v-skeleton-bar] {
67
+ display: inline-block;
68
+ height: 0.72em;
69
+ vertical-align: middle;
70
+ border-radius: 0.1875rem;
71
+ background-color: var(--vocs-background-color-surfaceTint);
72
+ background-image: linear-gradient(
73
+ 90deg,
74
+ transparent 0%,
75
+ color-mix(in srgb, var(--vocs-color-gray10) 22%, transparent) 50%,
76
+ transparent 100%
77
+ );
78
+ background-repeat: no-repeat;
79
+ background-size: 200% 100%;
80
+ animation: vocs-skeleton-shimmer 1.25s ease-in-out infinite;
81
+ }
82
+
83
+ @keyframes vocs-skeleton-shimmer {
84
+ from {
85
+ background-position: 180% 0;
86
+ }
87
+ to {
88
+ background-position: -80% 0;
89
+ }
90
+ }
91
+
92
+ @media (prefers-reduced-motion: reduce) {
93
+ [data-v-code-skeleton] [data-v-skeleton-bar] {
94
+ animation: none;
95
+ }
96
+ }
97
+
66
98
  .twoslash-popup-docs {
67
99
  @apply vocs:max-h-40 vocs:overflow-y-scroll;
68
100
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vocs",
3
3
  "type": "module",
4
- "version": "2.0.7",
4
+ "version": "2.0.10",
5
5
  "main": "./dist/index.js",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -1118,7 +1118,10 @@ function getVirtualFiles(codeNodes: MdAst.Code[]): Map<string, string> {
1118
1118
  for (const node of codeNodes) {
1119
1119
  const fileName = getCodeFileName(node)
1120
1120
  if (!fileName) continue
1121
- virtualFiles.set(fileName, node.value)
1121
+ // Strip any inline twoslash cache comment so it isn't inlined ahead of a
1122
+ // host block's code when this virtual file is imported/included. The
1123
+ // original node keeps its comment for its own rendering.
1124
+ virtualFiles.set(fileName, InlineCache.stripInlineCacheComments(node.value))
1122
1125
  }
1123
1126
  return virtualFiles
1124
1127
  }
@@ -307,8 +307,13 @@ export function twoslash(options: twoslash.Options): ShikiTransformer {
307
307
  executeOptions?: Parameters<TwoslashInstance>[2],
308
308
  meta?: Parameters<NonNullable<typeof typesCache.preprocess>>[3],
309
309
  ) {
310
- const preprocessed = typesCache.preprocess?.(code, lang, executeOptions, meta)
311
- return Renderer.normalizeCustomTagBlocks(preprocessed ?? code)
310
+ // Normalize before the inner preprocess so the cache key hashed on
311
+ // read matches the code hashed on write (which runs against the
312
+ // normalized output). Without this, snippets containing consecutive
313
+ // custom tag lines (`@log`/`@error`/`@warn`/`@annotate`) would never
314
+ // hit the cache.
315
+ const normalized = Renderer.normalizeCustomTagBlocks(code)
316
+ return typesCache.preprocess?.(normalized, lang, executeOptions, meta) ?? normalized
312
317
  },
313
318
  }
314
319
  : undefined
@@ -6,6 +6,7 @@ import {
6
6
  createInlineTypesCache,
7
7
  extractSourceMapComment,
8
8
  injectSourceMapComment,
9
+ stripInlineCacheComments,
9
10
  } from './inline-cache.js'
10
11
 
11
12
  describe('source map codec', () => {
@@ -26,6 +27,21 @@ describe('source map codec', () => {
26
27
  })
27
28
  })
28
29
 
30
+ describe('stripInlineCacheComments', () => {
31
+ it('removes all cache comments', () => {
32
+ const code = [
33
+ '// @twoslash-cache: {"v":1,"hash":"abc","data":"x"}',
34
+ 'export const client = 1',
35
+ ].join('\n')
36
+ expect(stripInlineCacheComments(code)).toBe('export const client = 1')
37
+ })
38
+
39
+ it('leaves code without cache comments untouched', () => {
40
+ const code = 'export const client = 1\nconst a = 2'
41
+ expect(stripInlineCacheComments(code)).toBe(code)
42
+ })
43
+ })
44
+
29
45
  describe('inline types cache', () => {
30
46
  const tmpFiles: string[] = []
31
47
 
@@ -87,6 +103,70 @@ describe('inline types cache', () => {
87
103
  }
88
104
  })
89
105
 
106
+ it('replaces an existing cache comment in place instead of appending a duplicate', () => {
107
+ const code = 'const a: string = "x"'
108
+ const { file, from, to } = createMarkdown(code)
109
+ const data = { nodes: [], code: 'compiled-output' }
110
+
111
+ // --- Pass 1: cold write ---
112
+ {
113
+ const { typesCache, patcher } = createInlineTypesCache()
114
+ const meta = { sourceMap: { path: file, from, to } } as never
115
+ typesCache.preprocess?.(code, 'ts', {}, meta)
116
+ typesCache.write(code, data as never, 'ts', {}, meta)
117
+ patcher.patch(file)
118
+ }
119
+
120
+ const cacheLines = () =>
121
+ fs
122
+ .readFileSync(file, 'utf-8')
123
+ .split('\n')
124
+ .filter((l) => l.includes('// @twoslash-cache:'))
125
+ expect(cacheLines()).toHaveLength(1)
126
+
127
+ // --- Pass 2: a writer with stale in-memory code (no cache comment, so the
128
+ // existing comment can't be located via `search`) must still replace the
129
+ // on-disk comment rather than append a second one. ---
130
+ {
131
+ const content = fs.readFileSync(file, 'utf-8')
132
+ const currentTo = content.lastIndexOf('\n```')
133
+ const { typesCache, patcher } = createInlineTypesCache()
134
+ const meta = { sourceMap: { path: file, from, to: currentTo } } as never
135
+ // Pass the original (comment-free) code to simulate a stale read.
136
+ typesCache.preprocess?.(code, 'ts', {}, meta)
137
+ typesCache.write(code, data as never, 'ts', {}, meta)
138
+ patcher.patch(file)
139
+ }
140
+
141
+ expect(cacheLines()).toHaveLength(1)
142
+ })
143
+
144
+ it('hits regardless of twoslash options (portable across environments)', () => {
145
+ const code = 'const a: string = "x"'
146
+ const { file, from, to } = createMarkdown(code)
147
+ const data = { nodes: [], code: 'compiled-output' }
148
+
149
+ // Seed with one set of options (e.g. machine A, with `paths`).
150
+ {
151
+ const { typesCache, patcher } = createInlineTypesCache()
152
+ const meta = { sourceMap: { path: file, from, to } } as never
153
+ const options = { compilerOptions: { paths: { foo: ['/abs/a'] } } }
154
+ typesCache.preprocess?.(code, 'ts', options as never, meta)
155
+ typesCache.write(code, data as never, 'ts', options as never, meta)
156
+ patcher.patch(file)
157
+ }
158
+
159
+ // Read with different options (e.g. machine B / Vercel, without `paths`).
160
+ {
161
+ const { typesCache } = createInlineTypesCache()
162
+ const body = readBody(file, from)
163
+ const meta = { sourceMap: { path: file, from, to } } as never
164
+ const otherOptions = { compilerOptions: {} }
165
+ typesCache.preprocess?.(body, 'ts', otherOptions as never, meta)
166
+ expect(typesCache.read(body, 'ts', otherOptions as never, meta)).toEqual(data)
167
+ }
168
+ })
169
+
90
170
  it('invalidates the cache when the hash no longer matches', () => {
91
171
  const code = 'const a: string = "x"'
92
172
  const { file, from, to } = createMarkdown(code)
@@ -1,7 +1,6 @@
1
1
  import * as crypto from 'node:crypto'
2
2
  import type { TwoslashShikiReturn, TwoslashTypesCache } from '@shikijs/twoslash'
3
3
  import LZString from 'lz-string'
4
- import { getObjectHash, type TwoslashExecuteOptions } from 'twoslash/core'
5
4
  import { FilePatcher } from './file-patcher.js'
6
5
 
7
6
  /**
@@ -73,8 +72,33 @@ type CachePayload = {
73
72
  data: string
74
73
  }
75
74
 
75
+ /**
76
+ * Cache payload format version. Kept at `1` even though the cache key changed
77
+ * from `optionsHash:lang:code` to `lang:code` (see `cacheHash`): stale
78
+ * payloads from the old key naturally fail the hash check on `preprocess` and
79
+ * are transparently re-seeded on the next write, so a version bump (which
80
+ * would force a one-shot invalidation of every committed cache comment) is
81
+ * unnecessary.
82
+ */
83
+ const CACHE_VERSION = 1
84
+
76
85
  const CODE_INLINE_CACHE_KEY = '@twoslash-cache'
77
86
  const CODE_INLINE_CACHE_REGEX = new RegExp(`// ${CODE_INLINE_CACHE_KEY}: (.*)(?:\n|$)`, 'g')
87
+ /** Matches a cache comment anchored to the start of a code block body. */
88
+ const CODE_INLINE_CACHE_LINE_REGEX = new RegExp(`^// ${CODE_INLINE_CACHE_KEY}: .*(?:\n|$)`)
89
+
90
+ /**
91
+ * Remove all `// @twoslash-cache: ...` comments from a code string.
92
+ *
93
+ * Used when a virtual file's content is injected into another twoslash block
94
+ * (via `[!include]` or `import` resolution). Without stripping, the included
95
+ * file's own cache comment would be inlined ahead of the host block's code and
96
+ * picked up as the host's cache, causing a permanent hash mismatch (and a
97
+ * spurious cache comment to be re-appended on every build).
98
+ */
99
+ export function stripInlineCacheComments(code: string): string {
100
+ return code.replace(CODE_INLINE_CACHE_REGEX, '')
101
+ }
78
102
 
79
103
  export function createInlineTypesCache(
80
104
  options: { remove?: boolean | undefined; ignoreCache?: boolean | undefined } = {},
@@ -82,32 +106,25 @@ export function createInlineTypesCache(
82
106
  const { remove, ignoreCache } = options
83
107
  const patcher = new FilePatcher()
84
108
 
85
- const optionsHashCache = new WeakMap<TwoslashExecuteOptions, string>()
86
- function getOptionsHash(options: TwoslashExecuteOptions = {}): string {
87
- let hash = optionsHashCache.get(options)
88
- if (!hash) {
89
- hash = getObjectHash(options)
90
- optionsHashCache.set(options, hash)
91
- }
92
- return hash
93
- }
94
-
95
- function cacheHash(code: string, lang?: string, options?: TwoslashExecuteOptions): string {
109
+ // Key the cache on the (already fully-composed) snippet code and language
110
+ // only — deliberately NOT on the twoslash options. The whole point of the
111
+ // inline cache is to travel with the repo across machines and CI, but
112
+ // `twoslashOptions` frequently contain environment-specific or volatile
113
+ // values (absolute paths, config that branches on whether built `.d.ts`
114
+ // files exist, etc). Hashing them in makes the committed cache miss on any
115
+ // machine other than the one that seeded it. This matches the filesystem
116
+ // types cache, which also keys on code alone.
117
+ function cacheHash(code: string, lang?: string): string {
96
118
  return crypto
97
119
  .createHash('sha256')
98
- .update(`${getOptionsHash(options)}:${lang ?? ''}:${code}`)
120
+ .update(`${lang ?? ''}:${code}`)
99
121
  .digest('hex')
100
122
  }
101
123
 
102
- function stringifyCachePayload(
103
- data: TwoslashShikiReturn,
104
- code: string,
105
- lang?: string,
106
- options?: TwoslashExecuteOptions,
107
- ): string {
124
+ function stringifyCachePayload(data: TwoslashShikiReturn, code: string, lang?: string): string {
108
125
  const payload: CachePayload = {
109
- v: 1,
110
- hash: cacheHash(code, lang, options),
126
+ v: CACHE_VERSION,
127
+ hash: cacheHash(code, lang),
111
128
  data: LZString.compressToBase64(JSON.stringify(data)),
112
129
  }
113
130
  return JSON.stringify(payload)
@@ -120,7 +137,7 @@ export function createInlineTypesCache(
120
137
  if (!cache) return null
121
138
  try {
122
139
  const payload = JSON.parse(cache) as CachePayload
123
- if (payload.v === 1) {
140
+ if (payload.v === CACHE_VERSION) {
124
141
  return {
125
142
  payload,
126
143
  twoslash: () => {
@@ -148,12 +165,28 @@ export function createInlineTypesCache(
148
165
  const range: { from: number; to?: number } = { from: source.from }
149
166
  let linebreak = true
150
167
 
168
+ let located = false
151
169
  if (search) {
152
170
  const cachePos = file.content.indexOf(search, source.from)
153
171
  if (cachePos !== -1 && cachePos < source.to) {
154
172
  range.from = cachePos
155
173
  range.to = cachePos + search.length
156
174
  linebreak = search.endsWith('\n')
175
+ located = true
176
+ }
177
+ }
178
+
179
+ // Fallback: if the existing cache comment wasn't located via `search` (e.g.
180
+ // a concurrent build environment already wrote one, or the in-memory code
181
+ // was stale), detect a cache comment at the block body start and replace it
182
+ // in place rather than appending a duplicate.
183
+ if (!located) {
184
+ const body = file.content.slice(source.from, source.to)
185
+ const match = body.match(CODE_INLINE_CACHE_LINE_REGEX)
186
+ if (match) {
187
+ range.from = source.from
188
+ range.to = source.from + match[0].length
189
+ linebreak = match[0].endsWith('\n')
157
190
  }
158
191
  }
159
192
 
@@ -169,7 +202,7 @@ export function createInlineTypesCache(
169
202
  }
170
203
 
171
204
  const typesCache: TwoslashTypesCache = {
172
- preprocess(code, lang, options, meta) {
205
+ preprocess(code, lang, _options, meta) {
173
206
  if (!meta) return
174
207
 
175
208
  let rawCache = ''
@@ -187,7 +220,7 @@ export function createInlineTypesCache(
187
220
  const shouldLoadCache = !ignoreCache && !remove
188
221
  if (shouldLoadCache) {
189
222
  const cache = resolveCachePayload(cacheString)
190
- if (cache?.payload.hash === cacheHash(code, lang, options)) {
223
+ if (cache?.payload.hash === cacheHash(code, lang)) {
191
224
  const twoslash = cache.twoslash()
192
225
  if (twoslash) meta.__cache = twoslash
193
226
  }
@@ -203,13 +236,13 @@ export function createInlineTypesCache(
203
236
  read(_code, _lang, _options, meta) {
204
237
  return meta?.__cache ?? null
205
238
  },
206
- write(code, data, lang, options, meta) {
239
+ write(code, data, lang, _options, meta) {
207
240
  if (remove) {
208
241
  meta?.__patch?.('')
209
242
  return
210
243
  }
211
244
  const twoslashShiki = simplifyTwoslashReturn(data)
212
- const cacheStr = `// ${CODE_INLINE_CACHE_KEY}: ${stringifyCachePayload(twoslashShiki, code, lang, options)}`
245
+ const cacheStr = `// ${CODE_INLINE_CACHE_KEY}: ${stringifyCachePayload(twoslashShiki, code, lang)}`
213
246
  meta?.__patch?.(cacheStr)
214
247
  },
215
248
  }