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.
- package/dist/internal/mdx.d.ts.map +1 -1
- package/dist/internal/mdx.js +4 -1
- package/dist/internal/mdx.js.map +1 -1
- package/dist/internal/shiki-transformers.d.ts.map +1 -1
- package/dist/internal/shiki-transformers.js +7 -2
- package/dist/internal/shiki-transformers.js.map +1 -1
- package/dist/internal/twoslash/inline-cache.d.ts +10 -0
- package/dist/internal/twoslash/inline-cache.d.ts.map +1 -1
- package/dist/internal/twoslash/inline-cache.js +56 -20
- package/dist/internal/twoslash/inline-cache.js.map +1 -1
- package/dist/react/internal/CodeToHtml.client.d.ts +27 -0
- package/dist/react/internal/CodeToHtml.client.d.ts.map +1 -0
- package/dist/react/internal/CodeToHtml.client.js +138 -0
- package/dist/react/internal/CodeToHtml.client.js.map +1 -0
- package/dist/react/internal/CodeToHtml.d.ts +1 -7
- package/dist/react/internal/CodeToHtml.d.ts.map +1 -1
- package/dist/react/internal/CodeToHtml.js +1 -56
- package/dist/react/internal/CodeToHtml.js.map +1 -1
- package/dist/react/internal/TwoslashHover.client.d.ts.map +1 -1
- package/dist/react/internal/TwoslashHover.client.js +7 -1
- package/dist/react/internal/TwoslashHover.client.js.map +1 -1
- package/dist/styles/twoslash.css +32 -0
- package/package.json +1 -1
- package/src/internal/mdx.ts +4 -1
- package/src/internal/shiki-transformers.ts +7 -2
- package/src/internal/twoslash/inline-cache.test.ts +80 -0
- package/src/internal/twoslash/inline-cache.ts +59 -26
- package/src/react/internal/CodeToHtml.client.tsx +176 -0
- package/src/react/internal/CodeToHtml.tsx +1 -74
- package/src/react/internal/TwoslashHover.client.tsx +8 -1
- 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
|
|
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":"
|
|
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
|
-
|
|
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":"
|
|
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;
|
|
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":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAEhD,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"TwoslashHover.client.js","sourceRoot":"","sources":["../../../src/react/internal/TwoslashHover.client.tsx"],"names":[],"mappings":"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"}
|
package/dist/styles/twoslash.css
CHANGED
|
@@ -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
package/src/internal/mdx.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(`${
|
|
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:
|
|
110
|
-
hash: cacheHash(code, lang
|
|
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 ===
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
245
|
+
const cacheStr = `// ${CODE_INLINE_CACHE_KEY}: ${stringifyCachePayload(twoslashShiki, code, lang)}`
|
|
213
246
|
meta?.__patch?.(cacheStr)
|
|
214
247
|
},
|
|
215
248
|
}
|