Skip to content

Commit 52046ff

Browse files
authoredJan 14, 2025··
feat(codegen): new pacakge (#889)
1 parent fa54de0 commit 52046ff

15 files changed

+607
-8
lines changed
 

‎docs/packages/codegen.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# shiki-codegen
2+
3+
<Badges name="shiki-codegen" />
4+
5+
Codegen for Shiki to create optimized bundles dedicated for your usage.
6+
7+
## Usage
8+
9+
### CLI
10+
11+
```bash
12+
npx shiki-codegen \
13+
--langs typescript,javascript,vue \
14+
--themes light-plus,dark-plus \
15+
--engine javascript \
16+
./shiki.bundle.ts
17+
```
18+
19+
File `shiki.bundle.ts` will be created with the code that you can use in your project.
20+
21+
Then you can use it in your project:
22+
23+
```ts
24+
import { codeToHtml } from './shiki.bundle'
25+
26+
const html = await codeToHtml(code, { lang: 'typescript', theme: 'light-plus' })
27+
```
28+
29+
### Programmatic
30+
31+
```ts
32+
import { codegen } from 'shiki-codegen'
33+
34+
const { code } = await codegen({
35+
langs: ['typescript', 'javascript', 'vue'],
36+
themes: ['light-plus', 'dark-plus'],
37+
engine: 'javascript',
38+
typescript: true
39+
})
40+
41+
// Write the code to a file
42+
```

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"@vitest/coverage-v8": "catalog:",
4343
"ansi-sequence-parser": "catalog:",
4444
"bumpp": "catalog:",
45-
"diff-match-patch-es": "^1.0.1",
45+
"diff-match-patch-es": "catalog:",
4646
"eslint": "catalog:",
4747
"eslint-plugin-format": "catalog:",
4848
"esno": "catalog:",

‎packages/codegen/README.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# @shikijs/codegen
2+
3+
Codegen for fine-grained Shiki bundles.
4+
5+
[Documentation](https://shiki.style/packages/codegen)
6+
7+
## License
8+
9+
MIT

‎packages/codegen/bin.mjs

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env node
2+
import('./dist/cli.mjs')

‎packages/codegen/build.config.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { defineBuildConfig } from 'unbuild'
2+
3+
export default defineBuildConfig({
4+
entries: [
5+
'src/index.ts',
6+
'src/cli.ts',
7+
],
8+
declaration: true,
9+
rollup: {
10+
emitCJS: false,
11+
dts: {
12+
compilerOptions: {
13+
paths: {},
14+
},
15+
},
16+
},
17+
externals: [
18+
'hast',
19+
],
20+
})

‎packages/codegen/package.json

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "shiki-codegen",
3+
"type": "module",
4+
"version": "1.26.2",
5+
"description": "Codegen for fine-grained Shiki bundles.",
6+
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
7+
"license": "MIT",
8+
"homepage": "https://github.com/shikijs/shiki#readme",
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/shikijs/shiki.git",
12+
"directory": "packages/codegen"
13+
},
14+
"bugs": "https://github.com/shikijs/shiki/issues",
15+
"keywords": [
16+
"shiki",
17+
"codegen"
18+
],
19+
"sideEffects": false,
20+
"exports": {
21+
".": {
22+
"types": "./dist/index.d.mts",
23+
"default": "./dist/index.mjs"
24+
},
25+
"./cli": {
26+
"types": "./dist/cli.d.mts",
27+
"default": "./dist/cli.mjs"
28+
}
29+
},
30+
"main": "./dist/index.mjs",
31+
"module": "./dist/index.mjs",
32+
"types": "./dist/index.d.mts",
33+
"bin": {
34+
"shiki-codegen": "bin.mjs"
35+
},
36+
"files": [
37+
"bin.mjs",
38+
"dist"
39+
],
40+
"scripts": {
41+
"build": "unbuild",
42+
"dev": "unbuild --stub",
43+
"prepublishOnly": "nr build",
44+
"test": "vitest"
45+
},
46+
"dependencies": {
47+
"cac": "catalog:",
48+
"prettier": "catalog:",
49+
"shiki": "workspace:*"
50+
}
51+
}

‎packages/codegen/src/cli.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import fs from 'node:fs/promises'
2+
import { dirname, resolve } from 'node:path'
3+
import { cac } from 'cac'
4+
import { codegen } from '.'
5+
6+
const cli = cac('shiki-codegen')
7+
8+
cli
9+
.command('[path]', 'Generate shiki bundle module')
10+
.option('--themes <themes>', 'Themes to include')
11+
.option('--langs <langs>', 'Languages to include')
12+
.option('--engine <engine>', 'Engine to use', { default: 'javascript' })
13+
.option('--precompiled', 'Use precompiled languages', { default: false })
14+
.option('--shorthands', 'Generate shorthands', { default: true })
15+
.option('--format', 'Use prettier to format', { default: true })
16+
.action(async (path, options) => {
17+
const output: string = resolve(path || 'shiki.bundle.ts')
18+
19+
const langs = ((Array.isArray(options.langs) ? options.langs.join(',') : options.langs || '') as string)
20+
.split(',')
21+
.map(lang => lang.trim())
22+
.filter(Boolean)
23+
const themes = ((Array.isArray(options.themes) ? options.themes.join(',') : options.themes || '') as string)
24+
.split(',')
25+
.map(theme => theme.trim())
26+
.filter(Boolean)
27+
28+
const isTypeScript = !!output.match(/\.[cm]?ts$/i)
29+
30+
if (!themes.length) {
31+
throw new Error('No themes specified, use --themes=theme-name to specify themes')
32+
}
33+
if (!langs.length) {
34+
throw new Error('No langs specified, use --langs=lang-name to specify langs')
35+
}
36+
37+
const { code } = await codegen({
38+
langs: langs as any[],
39+
themes: themes as any[],
40+
engine: options.engine,
41+
precompiled: options.precompiled,
42+
shorthands: options.shorthands,
43+
format: options.format,
44+
typescript: isTypeScript,
45+
})
46+
47+
await fs.mkdir(dirname(output), { recursive: true })
48+
await fs.writeFile(output, code, 'utf-8')
49+
console.log(`Generated bundle to ${output}`)
50+
})
51+
52+
cli.help()
53+
cli.parse()

‎packages/codegen/src/index.ts

+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import type { Options as PrettierOptions } from 'prettier'
2+
import type { BundledLanguage, BundledTheme } from 'shiki'
3+
import { bundledLanguagesInfo, bundledThemesInfo } from 'shiki/bundle/full'
4+
5+
export interface ShikiCodegenOptions {
6+
/**
7+
* The header to add to the generated code.
8+
*
9+
* @default '/* Generate by @shikijs/codegen *\/'
10+
*/
11+
header?: string
12+
13+
/**
14+
* The languages to bundle.
15+
*/
16+
langs: readonly BundledLanguage[]
17+
18+
/**
19+
* The themes to bundle.
20+
*/
21+
themes: readonly BundledTheme[]
22+
23+
/**
24+
* The engine to use for syntax highlighting.
25+
*/
26+
engine: 'oniguruma' | 'javascript' | 'javascript-raw'
27+
28+
/**
29+
* Use precompiled grammars.
30+
* Only available when `engine` is set to `javascript` or `javascript-raw`.
31+
*/
32+
precompiled?: boolean
33+
34+
/**
35+
* Whether to generate TypeScript code.
36+
*
37+
* @default true
38+
*/
39+
typescript?: boolean
40+
41+
/**
42+
* Generate shorthands for the highlighter.
43+
*
44+
* @default true
45+
*/
46+
shorthands?: boolean
47+
48+
/**
49+
* Use Prettier to format the generated code.
50+
*/
51+
format?: boolean | PrettierOptions
52+
}
53+
54+
export interface ShikiCodegenResult {
55+
code: string
56+
}
57+
58+
export async function codegen(options: ShikiCodegenOptions): Promise<ShikiCodegenResult> {
59+
const {
60+
header = '/* Generate by @shikijs/codegen */',
61+
typescript = true,
62+
precompiled = false,
63+
format: _format = true,
64+
shorthands = true,
65+
} = options
66+
67+
const ts = (code: string): string => typescript ? code : ''
68+
69+
if (precompiled && options.engine !== 'javascript' && options.engine !== 'javascript-raw')
70+
throw new Error('Precompiled grammars are only available when using the JavaScript engine')
71+
72+
const langs = options.langs.map((lang) => {
73+
const info = bundledLanguagesInfo.find(i => i.id === lang || i.aliases?.includes(lang))
74+
if (!info)
75+
throw new Error(`Language ${lang} not found`)
76+
return info
77+
})
78+
79+
const themes = options.themes.map((theme) => {
80+
const info = bundledThemesInfo.find(i => i.id === theme)
81+
if (!info)
82+
throw new Error(`Theme ${theme} not found`)
83+
return info
84+
})
85+
86+
const langsCode = `{\n${langs.flatMap((lang) => {
87+
const ids = [lang.id, ...(lang.aliases || [])]
88+
return ids.map((id) => {
89+
return `${JSON.stringify(id)}: () => import('@shikijs/${precompiled ? 'langs-precompiled' : 'langs'}/${lang.id}'),\n`
90+
})
91+
}).join('')}}`
92+
93+
const themesCode = `{\n${themes.map((theme) => {
94+
return `${JSON.stringify(theme.id)}: () => import('@shikijs/themes/${theme.id}'),\n`
95+
}).join('')}}`
96+
97+
const typeImports: Record<string, string[]> = {
98+
'@shikijs/types': ['HighlighterGeneric', 'DynamicImportThemeRegistration', 'DynamicImportLanguageRegistration'],
99+
}
100+
const imports: Record<string, string[]> = {
101+
'@shikijs/core': ['createdBundledHighlighter'],
102+
}
103+
const lines: string[] = [
104+
'',
105+
]
106+
const exports: string[] = []
107+
const typeExports: string[] = []
108+
109+
if (typescript) {
110+
lines.push(
111+
'',
112+
`type BundledLanguage = ${langs.flatMap(lang => [lang.id, ...(lang.aliases || [])]).map(lang => `'${lang}'`).join(' | ')}`,
113+
`type BundledTheme = ${themes.map(theme => `'${theme.id}'`).join(' | ')}`,
114+
`type Highlighter = HighlighterGeneric<BundledLanguage, BundledTheme>`,
115+
'',
116+
)
117+
typeExports.push('BundledLanguage', 'BundledTheme', 'Highlighter')
118+
}
119+
120+
lines.push(
121+
'',
122+
`const bundledLanguages = ${langsCode}${ts(' as Record<BundledLanguage, DynamicImportLanguageRegistration>')}`,
123+
'',
124+
`const bundledThemes = ${themesCode}${ts(' as Record<BundledTheme, DynamicImportThemeRegistration>')}`,
125+
'',
126+
)
127+
exports.push('bundledLanguages', 'bundledThemes')
128+
129+
let engine: string
130+
131+
if (options.engine === 'javascript') {
132+
imports['@shikijs/engine-javascript'] = ['createJavaScriptRegexEngine']
133+
engine = 'createJavaScriptRegexEngine()'
134+
}
135+
else if (options.engine === 'javascript-raw') {
136+
imports['@shikijs/engine-javascript/raw'] = ['createJavaScriptRawEngine']
137+
engine = 'createJavaScriptRawEngine()'
138+
}
139+
else {
140+
imports['@shikijs/engine-oniguruma'] = ['createOnigurumaEngine']
141+
engine = 'createOnigurumaEngine(import(\'shiki/wasm\'))'
142+
}
143+
144+
lines.push(
145+
'',
146+
`const createHighlighter = /* @__PURE__ */ createdBundledHighlighter${ts('<BundledLanguage, BundledTheme>')}({`,
147+
` langs: bundledLanguages,`,
148+
` themes: bundledThemes,`,
149+
` engine: () => ${engine}`,
150+
`})`,
151+
)
152+
exports.push('createHighlighter')
153+
154+
if (shorthands) {
155+
imports['@shikijs/core'].push('createSingletonShorthands')
156+
const shorthandFunctions = [
157+
'codeToHtml',
158+
'codeToHast',
159+
'codeToTokensBase',
160+
'codeToTokens',
161+
'codeToTokensWithThemes',
162+
'getSingletonHighlighter',
163+
'getLastGrammarState',
164+
]
165+
lines.push(
166+
'',
167+
`const { ${shorthandFunctions.join(', ')} } = /* @__PURE__ */ createSingletonShorthands${ts('<BundledLanguage,BundledTheme>')}(createHighlighter)`,
168+
)
169+
exports.push(
170+
...shorthandFunctions,
171+
)
172+
}
173+
174+
// Imports
175+
lines.unshift(
176+
Object.entries(imports).map(([module, imports]) => {
177+
return `import { ${imports.sort().join(', ')} } from '${module}'`
178+
},
179+
).join('\n'),
180+
)
181+
182+
if (typescript) {
183+
lines.unshift(
184+
Object.entries(typeImports).map(([module, types]) => {
185+
return `import type { ${types.sort().join(', ')} } from '${module}'`
186+
},
187+
).join('\n'),
188+
)
189+
}
190+
191+
// Exports
192+
lines.push(
193+
'',
194+
`export { ${exports.sort().join(', ')} }`,
195+
)
196+
if (typescript) {
197+
lines.push(
198+
`export type { ${typeExports.sort().join(', ')} }`,
199+
)
200+
}
201+
202+
lines.unshift(header)
203+
204+
// Format code
205+
let code = lines.join('\n')
206+
if (_format) {
207+
const { format } = await import('prettier')
208+
const prettierOptions: PrettierOptions = {
209+
parser: typescript ? 'typescript' : 'babel',
210+
semi: false,
211+
tabWidth: 2,
212+
useTabs: false,
213+
singleQuote: true,
214+
trailingComma: 'all',
215+
...(_format === true ? {} : _format),
216+
}
217+
code = await format(code, prettierOptions)
218+
}
219+
220+
return {
221+
code,
222+
}
223+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/* Generate by @shikijs/codegen */
2+
import {
3+
createSingletonShorthands,
4+
createdBundledHighlighter,
5+
} from '@shikijs/core'
6+
import { createOnigurumaEngine } from '@shikijs/engine-oniguruma'
7+
8+
const bundledLanguages = {
9+
javascript: () => import('@shikijs/langs/javascript'),
10+
js: () => import('@shikijs/langs/javascript'),
11+
typescript: () => import('@shikijs/langs/typescript'),
12+
ts: () => import('@shikijs/langs/typescript'),
13+
tsx: () => import('@shikijs/langs/tsx'),
14+
}
15+
16+
const bundledThemes = {
17+
nord: () => import('@shikijs/themes/nord'),
18+
'vitesse-dark': () => import('@shikijs/themes/vitesse-dark'),
19+
}
20+
21+
const createHighlighter = /* @__PURE__ */ createdBundledHighlighter({
22+
langs: bundledLanguages,
23+
themes: bundledThemes,
24+
engine: () => createOnigurumaEngine(import('shiki/wasm')),
25+
})
26+
27+
const {
28+
codeToHtml,
29+
codeToHast,
30+
codeToTokensBase,
31+
codeToTokens,
32+
codeToTokensWithThemes,
33+
getSingletonHighlighter,
34+
getLastGrammarState,
35+
} = /* @__PURE__ */ createSingletonShorthands(createHighlighter)
36+
37+
export {
38+
bundledLanguages,
39+
bundledThemes,
40+
codeToHast,
41+
codeToHtml,
42+
codeToTokens,
43+
codeToTokensBase,
44+
codeToTokensWithThemes,
45+
createHighlighter,
46+
getLastGrammarState,
47+
getSingletonHighlighter,
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/* Generate by @shikijs/codegen */
2+
import type {
3+
DynamicImportLanguageRegistration,
4+
DynamicImportThemeRegistration,
5+
HighlighterGeneric,
6+
} from '@shikijs/types'
7+
import {
8+
createSingletonShorthands,
9+
createdBundledHighlighter,
10+
} from '@shikijs/core'
11+
import { createOnigurumaEngine } from '@shikijs/engine-oniguruma'
12+
13+
type BundledLanguage = 'javascript' | 'js' | 'typescript' | 'ts' | 'tsx'
14+
type BundledTheme = 'nord' | 'vitesse-dark'
15+
type Highlighter = HighlighterGeneric<BundledLanguage, BundledTheme>
16+
17+
const bundledLanguages = {
18+
javascript: () => import('@shikijs/langs/javascript'),
19+
js: () => import('@shikijs/langs/javascript'),
20+
typescript: () => import('@shikijs/langs/typescript'),
21+
ts: () => import('@shikijs/langs/typescript'),
22+
tsx: () => import('@shikijs/langs/tsx'),
23+
} as Record<BundledLanguage, DynamicImportLanguageRegistration>
24+
25+
const bundledThemes = {
26+
nord: () => import('@shikijs/themes/nord'),
27+
'vitesse-dark': () => import('@shikijs/themes/vitesse-dark'),
28+
} as Record<BundledTheme, DynamicImportThemeRegistration>
29+
30+
const createHighlighter = /* @__PURE__ */ createdBundledHighlighter<
31+
BundledLanguage,
32+
BundledTheme
33+
>({
34+
langs: bundledLanguages,
35+
themes: bundledThemes,
36+
engine: () => createOnigurumaEngine(import('shiki/wasm')),
37+
})
38+
39+
const {
40+
codeToHtml,
41+
codeToHast,
42+
codeToTokensBase,
43+
codeToTokens,
44+
codeToTokensWithThemes,
45+
getSingletonHighlighter,
46+
getLastGrammarState,
47+
} = /* @__PURE__ */ createSingletonShorthands<BundledLanguage, BundledTheme>(
48+
createHighlighter,
49+
)
50+
51+
export {
52+
bundledLanguages,
53+
bundledThemes,
54+
codeToHast,
55+
codeToHtml,
56+
codeToTokens,
57+
codeToTokensBase,
58+
codeToTokensWithThemes,
59+
createHighlighter,
60+
getLastGrammarState,
61+
getSingletonHighlighter,
62+
}
63+
export type { BundledLanguage, BundledTheme, Highlighter }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* Generate by @shikijs/codegen */
2+
import type {
3+
DynamicImportLanguageRegistration,
4+
DynamicImportThemeRegistration,
5+
HighlighterGeneric,
6+
} from '@shikijs/types'
7+
import { createdBundledHighlighter } from '@shikijs/core'
8+
import { createJavaScriptRawEngine } from '@shikijs/engine-javascript/raw'
9+
10+
type BundledLanguage = 'javascript' | 'js' | 'typescript' | 'ts' | 'tsx'
11+
type BundledTheme = 'nord' | 'vitesse-dark'
12+
type Highlighter = HighlighterGeneric<BundledLanguage, BundledTheme>
13+
14+
const bundledLanguages = {
15+
javascript: () => import('@shikijs/langs-precompiled/javascript'),
16+
js: () => import('@shikijs/langs-precompiled/javascript'),
17+
typescript: () => import('@shikijs/langs-precompiled/typescript'),
18+
ts: () => import('@shikijs/langs-precompiled/typescript'),
19+
tsx: () => import('@shikijs/langs-precompiled/tsx'),
20+
} as Record<BundledLanguage, DynamicImportLanguageRegistration>
21+
22+
const bundledThemes = {
23+
nord: () => import('@shikijs/themes/nord'),
24+
'vitesse-dark': () => import('@shikijs/themes/vitesse-dark'),
25+
} as Record<BundledTheme, DynamicImportThemeRegistration>
26+
27+
const createHighlighter = /* @__PURE__ */ createdBundledHighlighter<
28+
BundledLanguage,
29+
BundledTheme
30+
>({
31+
langs: bundledLanguages,
32+
themes: bundledThemes,
33+
engine: () => createJavaScriptRawEngine(),
34+
})
35+
36+
export { bundledLanguages, bundledThemes, createHighlighter }
37+
export type { BundledLanguage, BundledTheme, Highlighter }

‎packages/codegen/test/codegen.test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect, it } from 'vitest'
2+
import { codegen } from '../src'
3+
4+
it('should work', async () => {
5+
const langs = ['javascript', 'ts', 'tsx'] as const
6+
const themes = ['nord', 'vitesse-dark'] as const
7+
8+
await expect((await codegen({
9+
langs,
10+
themes,
11+
engine: 'oniguruma',
12+
})).code)
13+
.toMatchFileSnapshot('./__snapshots__/basic-oniguruma.ts')
14+
15+
await expect((await codegen({
16+
langs,
17+
themes,
18+
typescript: false,
19+
engine: 'oniguruma',
20+
})).code)
21+
.toMatchFileSnapshot('./__snapshots__/basic-oniguruma-js.js')
22+
23+
await expect((await codegen({
24+
langs,
25+
themes,
26+
engine: 'javascript-raw',
27+
precompiled: true,
28+
shorthands: false,
29+
})).code)
30+
.toMatchFileSnapshot('./__snapshots__/basic-precompiled.ts')
31+
})

‎packages/engine-oniguruma/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@
5555
"@shikijs/vscode-textmate": "catalog:"
5656
},
5757
"devDependencies": {
58-
"vscode-oniguruma": "^1.7.0"
58+
"vscode-oniguruma": "catalog:"
5959
}
6060
}

‎pnpm-lock.yaml

+23-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pnpm-workspace.yaml

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ catalog:
3232
'@vueuse/core': ^12.4.0
3333
ansi-sequence-parser: ^1.1.1
3434
bumpp: ^9.10.0
35+
cac: ^6.7.14
3536
chalk: ^5.4.1
37+
diff-match-patch-es: ^1.0.1
3638
eslint: ^9.18.0
3739
eslint-plugin-format: ^1.0.1
3840
esno: ^4.8.0
@@ -86,7 +88,7 @@ catalog:
8688
vitepress: ^1.5.0
8789
vitepress-plugin-mermaid: ^2.0.17
8890
vitest: ^2.1.8
89-
vscode-oniguruma: ^1.7.0
91+
vscode-oniguruma: 1.7.0
9092
vue: ^3.5.13
9193
vue-tsc: ^2.2.0
9294
wrangler: ^3.101.0

0 commit comments

Comments
 (0)
Please sign in to comment.