@vmz/plugin-shiki 0.3.12 → 0.3.14
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/README.md +14 -6
- package/dist/runtime.d.ts +30 -6
- package/dist/runtime.js +120 -17
- package/dist/vmz.plugin.d.ts +3 -2
- package/dist/vmz.plugin.js +9 -6
- package/package.json +8 -13
package/README.md
CHANGED
|
@@ -24,19 +24,23 @@ It is especially useful for:
|
|
|
24
24
|
|
|
25
25
|
## Install
|
|
26
26
|
|
|
27
|
-
`@vmz/plugin-shiki` resolves Shiki from the application. `shiki` (`>=3`)
|
|
28
|
-
bundled
|
|
27
|
+
`@vmz/plugin-shiki` resolves Shiki from the application. `shiki`, `@shikijs/langs` and `@shikijs/themes` (`>=3`) are
|
|
28
|
+
required peer dependencies, not bundled dependencies, so install them alongside the plugin:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
pnpm add @vmz/plugin-shiki shiki
|
|
31
|
+
pnpm add @vmz/plugin-shiki shiki @shikijs/langs @shikijs/themes
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
The
|
|
35
|
-
|
|
34
|
+
The runtime loads `shiki/core` plus explicitly imported grammar and theme modules, so a client bundle only carries the
|
|
35
|
+
grammars this plugin can actually resolve instead of every `bundle-full` language. Supported grammars:
|
|
36
|
+
`javascript`, `typescript`, `tsx`, `jsx`, `json`, `html`, `css`, `markdown`, `bash` (plus `text`/`plaintext`, which
|
|
37
|
+
Shiki serves without a grammar module). Supported themes: `vitesse-dark` (default) and `vitesse-light`. A theme outside
|
|
38
|
+
that set is rejected with an explicit error instead of being silently swapped. A TextMate adapter adds custom grammars
|
|
39
|
+
on top; it does not replace the application-level `shiki` install.
|
|
36
40
|
|
|
37
41
|
## Third-party TextMate grammar (VMZ-2)
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
Custom grammars (VMZ, VOS, …) come from an explicit adapter module id that the host must resolve:
|
|
40
44
|
|
|
41
45
|
```ts
|
|
42
46
|
import shiki from '@vmz/plugin-shiki';
|
|
@@ -47,6 +51,10 @@ export default defineConfig({
|
|
|
47
51
|
});
|
|
48
52
|
```
|
|
49
53
|
|
|
54
|
+
The adapter specifier is host-resolved (serve-host resolve hooks on Node, an import map or vendor copy in the
|
|
55
|
+
browser) and never vendored by the packer; when it fails to load, the runtime falls back to the built-in grammar set
|
|
56
|
+
above.
|
|
57
|
+
|
|
50
58
|
The plugin writes `dist/_vmz/plugin-shiki.config.json`; the runtime reads it in SSR (`VMZ_DIST`) and in the browser (`fetch`).
|
|
51
59
|
|
|
52
60
|
## VMZ boundary
|
package/dist/runtime.d.ts
CHANGED
|
@@ -2,14 +2,31 @@
|
|
|
2
2
|
* Shiki highlight helper — async with optional sync cache after prewarm.
|
|
3
3
|
* TextMate grammar via configurable peer (default `vmz-textmate/shiki`).
|
|
4
4
|
*/
|
|
5
|
-
import type {
|
|
5
|
+
import type { ShikiTransformer } from 'shiki';
|
|
6
6
|
export type ShikiRuntimeConfig = {
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* TextMate Shiki adapter module id (e.g. `vmz-textmate/shiki`). Explicit
|
|
9
|
+
* opt-in: the adapter package is host-visible (not an npm dependency of
|
|
10
|
+
* this plugin), so the host must guarantee resolvability — serve-host
|
|
11
|
+
* resolve hooks on Node, an import map or vendor copy in the browser.
|
|
12
|
+
*/
|
|
8
13
|
textmate?: string;
|
|
9
|
-
/** Default themes passed to the
|
|
14
|
+
/** Default themes passed to the highlighter factory. */
|
|
10
15
|
themes?: string[];
|
|
11
16
|
};
|
|
12
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The highlight surface every adapter must provide. Kept structural instead of
|
|
19
|
+
* `Pick<Highlighter, 'codeToHtml'>`: the fine-grained entry returns a
|
|
20
|
+
* `HighlighterCore`, and both satisfy this shape.
|
|
21
|
+
*/
|
|
22
|
+
type CodeHighlighter = {
|
|
23
|
+
codeToHtml(code: string, options: {
|
|
24
|
+
lang: string;
|
|
25
|
+
theme: string;
|
|
26
|
+
transformers?: ShikiTransformer[];
|
|
27
|
+
structure?: 'classic' | 'inline';
|
|
28
|
+
}): string;
|
|
29
|
+
};
|
|
13
30
|
/** @internal test hook */
|
|
14
31
|
export declare function getShikiRuntimeConfig(): Readonly<ShikiRuntimeConfig>;
|
|
15
32
|
/** Reset module state (tests). */
|
|
@@ -21,7 +38,14 @@ export declare function configureShiki(opts: ShikiRuntimeConfig): void;
|
|
|
21
38
|
export declare function prewarmShiki(opts?: {
|
|
22
39
|
themes?: string[];
|
|
23
40
|
}): Promise<CodeHighlighter>;
|
|
24
|
-
export declare function highlight(code: string, lang?: string, theme?: string): Promise<string>;
|
|
41
|
+
export declare function highlight(code: string, lang?: string, theme?: string | null): Promise<string>;
|
|
25
42
|
/** Sync highlight when prewarmed; otherwise escaped `<pre><code>`. */
|
|
26
|
-
export declare function highlightSync(code: string, lang?: string, theme?: string): string;
|
|
43
|
+
export declare function highlightSync(code: string, lang?: string, theme?: string | null): string;
|
|
44
|
+
/**
|
|
45
|
+
* Inline-structure sync highlight for embedders that own the `<pre>` chrome
|
|
46
|
+
* (markdown-it fence callback in the document pipeline). Returns the token
|
|
47
|
+
* spans only — no `<pre>` wrapper, no inline background. Empty string when no
|
|
48
|
+
* highlighter is prewarmed so the embedder can fall back to escaped code.
|
|
49
|
+
*/
|
|
50
|
+
export declare function highlightInline(code: string, lang?: string, theme?: string | null): string;
|
|
27
51
|
export {};
|
package/dist/runtime.js
CHANGED
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
* Shiki highlight helper — async with optional sync cache after prewarm.
|
|
3
3
|
* TextMate grammar via configurable peer (default `vmz-textmate/shiki`).
|
|
4
4
|
*/
|
|
5
|
-
const
|
|
5
|
+
const DEFAULT_THEME = 'vitesse-dark';
|
|
6
|
+
const DEFAULT_LANG = 'text';
|
|
7
|
+
/**
|
|
8
|
+
* Component props are nullable (`theme: string | null = null`) and a bound
|
|
9
|
+
* `null` overrides the prop default in the compiled component, so runtime
|
|
10
|
+
* entry points normalize before touching Shiki: `createHighlighter` rejects
|
|
11
|
+
* falsy theme/lang getters deep inside `@shikijs/core` (`normalizeGetter`
|
|
12
|
+
* dereferences the resolved module), which used to surface as a page-level
|
|
13
|
+
* SSR failure.
|
|
14
|
+
*/
|
|
15
|
+
function normalizeTheme(theme) {
|
|
16
|
+
return typeof theme === 'string' && theme.trim() ? theme : DEFAULT_THEME;
|
|
17
|
+
}
|
|
18
|
+
function normalizeLang(lang) {
|
|
19
|
+
return typeof lang === 'string' && lang.trim() ? lang : DEFAULT_LANG;
|
|
20
|
+
}
|
|
6
21
|
/**
|
|
7
22
|
* Shiki writes `background-color` and `color` inline on `<pre>`, which makes
|
|
8
23
|
* the site stylesheet need `!important` to own code surfaces. Strip that
|
|
@@ -85,15 +100,22 @@ async function resolveRuntimeConfig() {
|
|
|
85
100
|
configResolved = true;
|
|
86
101
|
}
|
|
87
102
|
function textmateSpec() {
|
|
88
|
-
return config.textmate ||
|
|
103
|
+
return config.textmate || null;
|
|
89
104
|
}
|
|
90
105
|
async function loadTextmateHighlighter(themes) {
|
|
91
106
|
const spec = textmateSpec();
|
|
107
|
+
if (!spec)
|
|
108
|
+
return null;
|
|
92
109
|
try {
|
|
93
|
-
|
|
110
|
+
// The textmate adapter is an explicit opt-in module id resolved by the
|
|
111
|
+
// host (serve-host resolve hooks, import map), never vendored by pack —
|
|
112
|
+
// the generic path below is the pack-visible default.
|
|
113
|
+
const mod = (await import(/* webpackIgnore: true */ spec));
|
|
94
114
|
const factory = mod.createVmzHighlighter ?? mod.createHighlighter ?? mod.default?.createVmzHighlighter;
|
|
95
115
|
if (typeof factory === 'function') {
|
|
96
|
-
return
|
|
116
|
+
// `return await` (not `return`): a rejected factory promise must be
|
|
117
|
+
// caught here so the generic Shiki path below still runs.
|
|
118
|
+
return await factory({ themes, langs: [] });
|
|
97
119
|
}
|
|
98
120
|
}
|
|
99
121
|
catch {
|
|
@@ -101,12 +123,62 @@ async function loadTextmateHighlighter(themes) {
|
|
|
101
123
|
}
|
|
102
124
|
return null;
|
|
103
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Grammars the generic (non-TextMate) path can resolve. `shiki`'s root entry
|
|
128
|
+
* reaches `bundle-full`, which registers every bundled grammar and theme as its
|
|
129
|
+
* own dynamic import — 272 async chunks of unused `@shikijs/langs` grammars in
|
|
130
|
+
* the delivered pack. The fine-grained entry below keeps the reachable grammar
|
|
131
|
+
* set equal to what the runtime can actually resolve.
|
|
132
|
+
*
|
|
133
|
+
* `text` / `plaintext` / `txt` / `plain` need no module: Shiki treats them as
|
|
134
|
+
* plain text (`isPlainLang`).
|
|
135
|
+
*/
|
|
136
|
+
const LANG_MODULES = {
|
|
137
|
+
javascript: () => import('@shikijs/langs/javascript'),
|
|
138
|
+
typescript: () => import('@shikijs/langs/typescript'),
|
|
139
|
+
tsx: () => import('@shikijs/langs/tsx'),
|
|
140
|
+
jsx: () => import('@shikijs/langs/jsx'),
|
|
141
|
+
json: () => import('@shikijs/langs/json'),
|
|
142
|
+
html: () => import('@shikijs/langs/html'),
|
|
143
|
+
css: () => import('@shikijs/langs/css'),
|
|
144
|
+
markdown: () => import('@shikijs/langs/markdown'),
|
|
145
|
+
bash: () => import('@shikijs/langs/bash'),
|
|
146
|
+
};
|
|
147
|
+
/** Themes the generic path can resolve (`vitesse-dark` is the runtime default). */
|
|
148
|
+
const THEME_MODULES = {
|
|
149
|
+
'vitesse-dark': () => import('@shikijs/themes/vitesse-dark'),
|
|
150
|
+
'vitesse-light': () => import('@shikijs/themes/vitesse-light'),
|
|
151
|
+
};
|
|
104
152
|
async function loadGenericHighlighter(themes) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
153
|
+
// Literal specifiers only: a computed dynamic import (variable argument) is
|
|
154
|
+
// invisible to the packer, so the browser chunk would keep a bare specifier
|
|
155
|
+
// and fail with "Failed to resolve module specifier 'shiki'". Literal
|
|
156
|
+
// imports let vmz-pack vendor the highlighter into an async client chunk.
|
|
157
|
+
for (const theme of themes) {
|
|
158
|
+
if (!THEME_MODULES[theme]) {
|
|
159
|
+
// Explicit rejection, no silent substitution: a theme outside the
|
|
160
|
+
// shipped set cannot be highlighted, and pretending otherwise would
|
|
161
|
+
// emit the wrong colours.
|
|
162
|
+
throw new Error(`@vmz/plugin-shiki: unsupported theme ${JSON.stringify(theme)} ` +
|
|
163
|
+
`(supported: ${Object.keys(THEME_MODULES).join(', ')})`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Every supported theme is registered at once: the highlighter is created
|
|
167
|
+
// once per process and per-call `theme` switches must not hit an
|
|
168
|
+
// unregistered theme. The requested list only decides what must be
|
|
169
|
+
// resolvable (validation above), not what is registered.
|
|
170
|
+
const [{ createHighlighterCore }, { createJavaScriptRegexEngine }, themeModules, langModules] = await Promise.all([
|
|
171
|
+
import('shiki/core'),
|
|
172
|
+
import('shiki/engine/javascript'),
|
|
173
|
+
Promise.all(Object.values(THEME_MODULES).map((load) => load())),
|
|
174
|
+
Promise.all(Object.values(LANG_MODULES).map((load) => load())),
|
|
175
|
+
]);
|
|
176
|
+
return createHighlighterCore({
|
|
177
|
+
themes: themeModules.map((module) => module.default),
|
|
178
|
+
langs: langModules.map((module) => module.default),
|
|
179
|
+
// Grammar fidelity comes from the same JS RegExp engine `vmz-textmate`
|
|
180
|
+
// ships: it needs no 608 KiB inlined Oniguruma wasm beside the chunk.
|
|
181
|
+
engine: createJavaScriptRegexEngine(),
|
|
110
182
|
});
|
|
111
183
|
}
|
|
112
184
|
export async function prewarmShiki(opts = {}) {
|
|
@@ -116,19 +188,29 @@ export async function prewarmShiki(opts = {}) {
|
|
|
116
188
|
return pending;
|
|
117
189
|
pending = (async () => {
|
|
118
190
|
await resolveRuntimeConfig();
|
|
119
|
-
const
|
|
191
|
+
const requested = opts.themes?.length ? opts.themes : config.themes?.length ? config.themes : [DEFAULT_THEME];
|
|
192
|
+
const themes = requested.map((theme) => normalizeTheme(theme));
|
|
120
193
|
const fromTextmate = await loadTextmateHighlighter(themes);
|
|
121
194
|
cached = fromTextmate ?? (await loadGenericHighlighter(themes));
|
|
122
195
|
return cached;
|
|
123
196
|
})();
|
|
124
197
|
return pending;
|
|
125
198
|
}
|
|
126
|
-
export async function highlight(code, lang = 'text', theme =
|
|
127
|
-
const
|
|
199
|
+
export async function highlight(code, lang = 'text', theme = DEFAULT_THEME) {
|
|
200
|
+
const safeTheme = normalizeTheme(theme);
|
|
201
|
+
const safeLang = normalizeLang(lang);
|
|
202
|
+
let highlighter;
|
|
203
|
+
try {
|
|
204
|
+
highlighter = await prewarmShiki({ themes: [safeTheme] });
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Highlighter bootstrap failure must not take the page down.
|
|
208
|
+
return fallbackPre(code);
|
|
209
|
+
}
|
|
128
210
|
try {
|
|
129
211
|
return highlighter.codeToHtml(code ?? '', {
|
|
130
|
-
lang:
|
|
131
|
-
theme,
|
|
212
|
+
lang: safeLang,
|
|
213
|
+
theme: safeTheme,
|
|
132
214
|
transformers: [stripPreInlineStyle],
|
|
133
215
|
});
|
|
134
216
|
}
|
|
@@ -137,13 +219,13 @@ export async function highlight(code, lang = 'text', theme = 'vitesse-dark') {
|
|
|
137
219
|
}
|
|
138
220
|
}
|
|
139
221
|
/** Sync highlight when prewarmed; otherwise escaped `<pre><code>`. */
|
|
140
|
-
export function highlightSync(code, lang = 'text', theme =
|
|
222
|
+
export function highlightSync(code, lang = 'text', theme = DEFAULT_THEME) {
|
|
141
223
|
if (!cached)
|
|
142
224
|
return fallbackPre(code);
|
|
143
225
|
try {
|
|
144
226
|
return cached.codeToHtml(code ?? '', {
|
|
145
|
-
lang: lang
|
|
146
|
-
theme,
|
|
227
|
+
lang: normalizeLang(lang),
|
|
228
|
+
theme: normalizeTheme(theme),
|
|
147
229
|
transformers: [stripPreInlineStyle],
|
|
148
230
|
});
|
|
149
231
|
}
|
|
@@ -151,6 +233,27 @@ export function highlightSync(code, lang = 'text', theme = 'vitesse-dark') {
|
|
|
151
233
|
return fallbackPre(code);
|
|
152
234
|
}
|
|
153
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Inline-structure sync highlight for embedders that own the `<pre>` chrome
|
|
238
|
+
* (markdown-it fence callback in the document pipeline). Returns the token
|
|
239
|
+
* spans only — no `<pre>` wrapper, no inline background. Empty string when no
|
|
240
|
+
* highlighter is prewarmed so the embedder can fall back to escaped code.
|
|
241
|
+
*/
|
|
242
|
+
export function highlightInline(code, lang = 'text', theme = DEFAULT_THEME) {
|
|
243
|
+
if (!cached)
|
|
244
|
+
return '';
|
|
245
|
+
try {
|
|
246
|
+
return cached.codeToHtml(code ?? '', {
|
|
247
|
+
lang: normalizeLang(lang),
|
|
248
|
+
theme: normalizeTheme(theme),
|
|
249
|
+
structure: 'inline',
|
|
250
|
+
transformers: [stripPreInlineStyle],
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return '';
|
|
255
|
+
}
|
|
256
|
+
}
|
|
154
257
|
function fallbackPre(code) {
|
|
155
258
|
const escaped = String(code ?? '')
|
|
156
259
|
.replace(/&/g, '&')
|
package/dist/vmz.plugin.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export type ShikiPluginOptions = {
|
|
2
2
|
/**
|
|
3
|
-
* TextMate Shiki adapter module id.
|
|
4
|
-
*
|
|
3
|
+
* TextMate Shiki adapter module id (e.g. `vmz-textmate/shiki`). Explicit
|
|
4
|
+
* opt-in: the host must resolve this specifier (it is not an npm
|
|
5
|
+
* dependency of the plugin). Omit it to use the plain Shiki path.
|
|
5
6
|
*/
|
|
6
7
|
textmate?: string;
|
|
7
8
|
/** Default Shiki themes for prewarm. */
|
package/dist/vmz.plugin.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { definePlugin, loadPluginSource } from '@vmz/plugin';
|
|
4
|
-
import { configureShiki } from './runtime.js';
|
|
4
|
+
import { configureShiki, prewarmShiki } from './runtime.js';
|
|
5
5
|
const source = loadPluginSource(import.meta.url, '../components/Shiki.vmz');
|
|
6
|
-
const DEFAULT_TEXTMATE = 'vmz-textmate/shiki';
|
|
7
6
|
function writeRuntimeSidecar(outDir, opts) {
|
|
8
|
-
const textmate = opts.textmate ?? DEFAULT_TEXTMATE;
|
|
9
7
|
const payload = {
|
|
10
|
-
textmate,
|
|
8
|
+
...(opts.textmate ? { textmate: opts.textmate } : {}),
|
|
11
9
|
...(opts.themes?.length ? { themes: opts.themes } : {}),
|
|
12
10
|
};
|
|
13
11
|
const dir = path.join(outDir, '_vmz');
|
|
@@ -28,9 +26,14 @@ function writeRuntimeSidecar(outDir, opts) {
|
|
|
28
26
|
*/
|
|
29
27
|
export function shiki(options = {}) {
|
|
30
28
|
configureShiki({
|
|
31
|
-
textmate: options.textmate
|
|
29
|
+
textmate: options.textmate,
|
|
32
30
|
themes: options.themes,
|
|
33
31
|
});
|
|
32
|
+
// Warm the highlighter at config-load time (the same process later runs the
|
|
33
|
+
// document pipeline and page SSR), so `highlightSync` hits the cache during
|
|
34
|
+
// SSR and the first paint already carries the final tokenized markup — no
|
|
35
|
+
// unhighlighted fallback flash. Bootstrap failure degrades to plain fences.
|
|
36
|
+
void prewarmShiki({ themes: options.themes }).catch(() => { });
|
|
34
37
|
return definePlugin({
|
|
35
38
|
name: '@vmz/plugin-shiki',
|
|
36
39
|
version: '0.1.0',
|
|
@@ -42,7 +45,7 @@ export function shiki(options = {}) {
|
|
|
42
45
|
writeRuntimeSidecar(ctx.outDir, options);
|
|
43
46
|
return {
|
|
44
47
|
stage: 'workspace_resolve',
|
|
45
|
-
cacheKey: `@vmz/plugin-shiki:Shiki.vmz:${source.contentHash.slice(0, 12)}:${options.textmate ??
|
|
48
|
+
cacheKey: `@vmz/plugin-shiki:Shiki.vmz:${source.contentHash.slice(0, 12)}:${options.textmate ?? 'generic'}`,
|
|
46
49
|
items: [
|
|
47
50
|
{
|
|
48
51
|
id: 'component-shiki',
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
|
-
"@vmz/plugin": "0.3.
|
|
3
|
+
"@vmz/plugin": "0.3.14"
|
|
4
4
|
},
|
|
5
5
|
"description": "VMZ Shiki adapter - <Shiki> + code engine registration",
|
|
6
6
|
"devDependencies": {
|
|
7
|
+
"@shikijs/langs": "^3.23.0",
|
|
8
|
+
"@shikijs/themes": "^3.23.0",
|
|
7
9
|
"@types/node": "^22.15.0",
|
|
8
10
|
"shiki": "^3.12.2",
|
|
9
11
|
"typescript": "^5.8.3",
|
|
10
|
-
"vmz-textmate": "0.3.
|
|
12
|
+
"vmz-textmate": "0.3.14"
|
|
11
13
|
},
|
|
12
14
|
"engines": {
|
|
13
15
|
"node": ">=24.12.0"
|
|
@@ -37,16 +39,9 @@
|
|
|
37
39
|
"main": "./dist/vmz.plugin.js",
|
|
38
40
|
"name": "@vmz/plugin-shiki",
|
|
39
41
|
"peerDependencies": {
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
"peerDependenciesMeta": {
|
|
44
|
-
"shiki": {
|
|
45
|
-
"optional": false
|
|
46
|
-
},
|
|
47
|
-
"vmz-textmate": {
|
|
48
|
-
"optional": true
|
|
49
|
-
}
|
|
42
|
+
"@shikijs/langs": ">=3",
|
|
43
|
+
"@shikijs/themes": ">=3",
|
|
44
|
+
"shiki": ">=3"
|
|
50
45
|
},
|
|
51
46
|
"publishConfig": {
|
|
52
47
|
"access": "public"
|
|
@@ -61,5 +56,5 @@
|
|
|
61
56
|
},
|
|
62
57
|
"type": "module",
|
|
63
58
|
"types": "./dist/vmz.plugin.d.ts",
|
|
64
|
-
"version": "0.3.
|
|
59
|
+
"version": "0.3.14"
|
|
65
60
|
}
|