@vmz/plugin-shiki 0.3.12 → 0.3.13

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 CHANGED
@@ -31,12 +31,13 @@ bundled dependency, so install it alongside the plugin:
31
31
  pnpm add @vmz/plugin-shiki shiki
32
32
  ```
33
33
 
34
- The default `vmz-textmate/shiki` path also needs `shiki/core`; installing `@shikijs/*` transitively through
35
- `vmz-textmate` does not replace the application-level `shiki` install.
34
+ The runtime loads plain `shiki` by default (a literal dynamic import that client bundles can vendor). A TextMate
35
+ adapter adds custom grammars on top; installing `@shikijs/*` transitively through an adapter does not replace the
36
+ application-level `shiki` install.
36
37
 
37
38
  ## Third-party TextMate grammar (VMZ-2)
38
39
 
39
- By default the runtime loads `vmz-textmate/shiki`. For other languages (e.g. VOS), pass a peer adapter:
40
+ Custom grammars (VMZ, VOS, …) come from an explicit adapter module id that the host must resolve:
40
41
 
41
42
  ```ts
42
43
  import shiki from '@vmz/plugin-shiki';
@@ -47,6 +48,9 @@ export default defineConfig({
47
48
  });
48
49
  ```
49
50
 
51
+ The adapter specifier is host-resolved (serve-host resolve hooks on Node, an import map or vendor copy in the
52
+ browser) and never vendored by the packer; when it fails to load, the runtime falls back to plain `shiki`.
53
+
50
54
  The plugin writes `dist/_vmz/plugin-shiki.config.json`; the runtime reads it in SSR (`VMZ_DIST`) and in the browser (`fetch`).
51
55
 
52
56
  ## VMZ boundary
package/dist/runtime.d.ts CHANGED
@@ -4,9 +4,14 @@
4
4
  */
5
5
  import type { Highlighter } from 'shiki';
6
6
  export type ShikiRuntimeConfig = {
7
- /** Shiki + TextMate adapter module (default `vmz-textmate/shiki`). */
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 textmate highlighter factory. */
14
+ /** Default themes passed to the highlighter factory. */
10
15
  themes?: string[];
11
16
  };
12
17
  type CodeHighlighter = Pick<Highlighter, 'codeToHtml'>;
@@ -21,7 +26,14 @@ export declare function configureShiki(opts: ShikiRuntimeConfig): void;
21
26
  export declare function prewarmShiki(opts?: {
22
27
  themes?: string[];
23
28
  }): Promise<CodeHighlighter>;
24
- export declare function highlight(code: string, lang?: string, theme?: string): Promise<string>;
29
+ export declare function highlight(code: string, lang?: string, theme?: string | null): Promise<string>;
25
30
  /** Sync highlight when prewarmed; otherwise escaped `<pre><code>`. */
26
- export declare function highlightSync(code: string, lang?: string, theme?: string): string;
31
+ export declare function highlightSync(code: string, lang?: string, theme?: string | null): string;
32
+ /**
33
+ * Inline-structure sync highlight for embedders that own the `<pre>` chrome
34
+ * (markdown-it fence callback in the document pipeline). Returns the token
35
+ * spans only — no `<pre>` wrapper, no inline background. Empty string when no
36
+ * highlighter is prewarmed so the embedder can fall back to escaped code.
37
+ */
38
+ export declare function highlightInline(code: string, lang?: string, theme?: string | null): string;
27
39
  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 DEFAULT_TEXTMATE = 'vmz-textmate/shiki';
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 || DEFAULT_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
- const mod = (spec === DEFAULT_TEXTMATE ? await import('vmz-textmate/shiki') : await import(/* webpackIgnore: true */ spec));
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 factory({ themes, langs: [] });
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 {
@@ -102,8 +124,11 @@ async function loadTextmateHighlighter(themes) {
102
124
  return null;
103
125
  }
104
126
  async function loadGenericHighlighter(themes) {
105
- const genericPackage = 'shiki';
106
- const { createHighlighter } = await import(genericPackage);
127
+ // Literal specifier only: a computed dynamic import (variable argument) is
128
+ // invisible to the packer, so the browser chunk would keep a bare specifier
129
+ // and fail with "Failed to resolve module specifier 'shiki'". Literal
130
+ // imports let vmz-pack vendor the highlighter into an async client chunk.
131
+ const { createHighlighter } = await import('shiki');
107
132
  return createHighlighter({
108
133
  themes,
109
134
  langs: ['javascript', 'typescript', 'tsx', 'jsx', 'json', 'html', 'css', 'markdown', 'bash', 'text'],
@@ -116,19 +141,29 @@ export async function prewarmShiki(opts = {}) {
116
141
  return pending;
117
142
  pending = (async () => {
118
143
  await resolveRuntimeConfig();
119
- const themes = opts.themes?.length ? opts.themes : config.themes?.length ? config.themes : ['vitesse-dark'];
144
+ const requested = opts.themes?.length ? opts.themes : config.themes?.length ? config.themes : [DEFAULT_THEME];
145
+ const themes = requested.map((theme) => normalizeTheme(theme));
120
146
  const fromTextmate = await loadTextmateHighlighter(themes);
121
147
  cached = fromTextmate ?? (await loadGenericHighlighter(themes));
122
148
  return cached;
123
149
  })();
124
150
  return pending;
125
151
  }
126
- export async function highlight(code, lang = 'text', theme = 'vitesse-dark') {
127
- const highlighter = await prewarmShiki({ themes: [theme] });
152
+ export async function highlight(code, lang = 'text', theme = DEFAULT_THEME) {
153
+ const safeTheme = normalizeTheme(theme);
154
+ const safeLang = normalizeLang(lang);
155
+ let highlighter;
156
+ try {
157
+ highlighter = await prewarmShiki({ themes: [safeTheme] });
158
+ }
159
+ catch {
160
+ // Highlighter bootstrap failure must not take the page down.
161
+ return fallbackPre(code);
162
+ }
128
163
  try {
129
164
  return highlighter.codeToHtml(code ?? '', {
130
- lang: lang || 'text',
131
- theme,
165
+ lang: safeLang,
166
+ theme: safeTheme,
132
167
  transformers: [stripPreInlineStyle],
133
168
  });
134
169
  }
@@ -137,13 +172,13 @@ export async function highlight(code, lang = 'text', theme = 'vitesse-dark') {
137
172
  }
138
173
  }
139
174
  /** Sync highlight when prewarmed; otherwise escaped `<pre><code>`. */
140
- export function highlightSync(code, lang = 'text', theme = 'vitesse-dark') {
175
+ export function highlightSync(code, lang = 'text', theme = DEFAULT_THEME) {
141
176
  if (!cached)
142
177
  return fallbackPre(code);
143
178
  try {
144
179
  return cached.codeToHtml(code ?? '', {
145
- lang: lang || 'text',
146
- theme,
180
+ lang: normalizeLang(lang),
181
+ theme: normalizeTheme(theme),
147
182
  transformers: [stripPreInlineStyle],
148
183
  });
149
184
  }
@@ -151,6 +186,27 @@ export function highlightSync(code, lang = 'text', theme = 'vitesse-dark') {
151
186
  return fallbackPre(code);
152
187
  }
153
188
  }
189
+ /**
190
+ * Inline-structure sync highlight for embedders that own the `<pre>` chrome
191
+ * (markdown-it fence callback in the document pipeline). Returns the token
192
+ * spans only — no `<pre>` wrapper, no inline background. Empty string when no
193
+ * highlighter is prewarmed so the embedder can fall back to escaped code.
194
+ */
195
+ export function highlightInline(code, lang = 'text', theme = DEFAULT_THEME) {
196
+ if (!cached)
197
+ return '';
198
+ try {
199
+ return cached.codeToHtml(code ?? '', {
200
+ lang: normalizeLang(lang),
201
+ theme: normalizeTheme(theme),
202
+ structure: 'inline',
203
+ transformers: [stripPreInlineStyle],
204
+ });
205
+ }
206
+ catch {
207
+ return '';
208
+ }
209
+ }
154
210
  function fallbackPre(code) {
155
211
  const escaped = String(code ?? '')
156
212
  .replace(/&/g, '&amp;')
@@ -1,7 +1,8 @@
1
1
  export type ShikiPluginOptions = {
2
2
  /**
3
- * TextMate Shiki adapter module id.
4
- * Default `vmz-textmate/shiki`; VOS uses `@game-gpt/vos-textmate/shiki`.
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. */
@@ -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 ?? DEFAULT_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 ?? DEFAULT_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,13 @@
1
1
  {
2
2
  "dependencies": {
3
- "@vmz/plugin": "0.3.12"
3
+ "@vmz/plugin": "0.3.13"
4
4
  },
5
5
  "description": "VMZ Shiki adapter - <Shiki> + code engine registration",
6
6
  "devDependencies": {
7
7
  "@types/node": "^22.15.0",
8
8
  "shiki": "^3.12.2",
9
9
  "typescript": "^5.8.3",
10
- "vmz-textmate": "0.3.12"
10
+ "vmz-textmate": "0.3.13"
11
11
  },
12
12
  "engines": {
13
13
  "node": ">=24.12.0"
@@ -37,16 +37,7 @@
37
37
  "main": "./dist/vmz.plugin.js",
38
38
  "name": "@vmz/plugin-shiki",
39
39
  "peerDependencies": {
40
- "shiki": ">=3",
41
- "vmz-textmate": "*"
42
- },
43
- "peerDependenciesMeta": {
44
- "shiki": {
45
- "optional": false
46
- },
47
- "vmz-textmate": {
48
- "optional": true
49
- }
40
+ "shiki": ">=3"
50
41
  },
51
42
  "publishConfig": {
52
43
  "access": "public"
@@ -61,5 +52,5 @@
61
52
  },
62
53
  "type": "module",
63
54
  "types": "./dist/vmz.plugin.d.ts",
64
- "version": "0.3.12"
55
+ "version": "0.3.13"
65
56
  }