Repository: shikijs/shiki
Pull request #889 (2025-01-14 07:21:11)

feat(codegen): new pacakge
New codegen pacakge to compose bundles easier

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *shiki-next* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 6b7b375d729420ec2b5b50be57d402580c3011cf |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/shiki-next/deploys/678610694c9a6400080de539 |
netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *shiki-matsu* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 6b7b375d729420ec2b5b50be57d402580c3011cf |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/shiki-matsu/deploys/678610693c45d100085df837 |

Patch:
diff --git a/docs/packages/codegen.md b/docs/packages/codegen.md
new file mode 100644
index 00000000..136099e1
--- /dev/null
+++ b/docs/packages/codegen.md
@@ -0,0 +1,42 @@
+# shiki-codegen
+
+<Badges name="shiki-codegen" />
+
+Codegen for Shiki to create optimized bundles dedicated for your usage.
+
+## Usage
+
+### CLI
+
+```bash
+npx shiki-codegen \
+  --langs typescript,javascript,vue \
+  --themes light-plus,dark-plus \
+  --engine javascript \
+  ./shiki.bundle.ts
+```
+
+File `shiki.bundle.ts` will be created with the code that you can use in your project.
+
+Then you can use it in your project:
+
+```ts
+import { codeToHtml } from './shiki.bundle'
+
+const html = await codeToHtml(code, { lang: 'typescript', theme: 'light-plus' })
+```
+
+### Programmatic
+
+```ts
+import { codegen } from 'shiki-codegen'
+
+const { code } = await codegen({
+  langs: ['typescript', 'javascript', 'vue'],
+  themes: ['light-plus', 'dark-plus'],
+  engine: 'javascript',
+  typescript: true
+})
+
+// Write the code to a file
+```
diff --git a/package.json b/package.json
index a7c5a96d..551f0940 100644
--- a/package.json
+++ b/package.json
@@ -42,7 +42,7 @@
     "@vitest/coverage-v8": "catalog:",
     "ansi-sequence-parser": "catalog:",
     "bumpp": "catalog:",
-    "diff-match-patch-es": "^1.0.1",
+    "diff-match-patch-es": "catalog:",
     "eslint": "catalog:",
     "eslint-plugin-format": "catalog:",
     "esno": "catalog:",
diff --git a/packages/codegen/README.md b/packages/codegen/README.md
new file mode 100644
index 00000000..ca16c818
--- /dev/null
+++ b/packages/codegen/README.md
@@ -0,0 +1,9 @@
+# @shikijs/codegen
+
+Codegen for fine-grained Shiki bundles.
+
+[Documentation](https://shiki.style/packages/codegen)
+
+## License
+
+MIT
diff --git a/packages/codegen/bin.mjs b/packages/codegen/bin.mjs
new file mode 100755
index 00000000..38bbdf5d
--- /dev/null
+++ b/packages/codegen/bin.mjs
@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+import('./dist/cli.mjs')
diff --git a/packages/codegen/build.config.ts b/packages/codegen/build.config.ts
new file mode 100644
index 00000000..00fa0acf
--- /dev/null
+++ b/packages/codegen/build.config.ts
@@ -0,0 +1,20 @@
+import { defineBuildConfig } from 'unbuild'
+
+export default defineBuildConfig({
+  entries: [
+    'src/index.ts',
+    'src/cli.ts',
+  ],
+  declaration: true,
+  rollup: {
+    emitCJS: false,
+    dts: {
+      compilerOptions: {
+        paths: {},
+      },
+    },
+  },
+  externals: [
+    'hast',
+  ],
+})
diff --git a/packages/codegen/package.json b/packages/codegen/package.json
new file mode 100644
index 00000000..b4da29e6
--- /dev/null
+++ b/packages/codegen/package.json
@@ -0,0 +1,51 @@
+{
+  "name": "shiki-codegen",
+  "type": "module",
+  "version": "1.26.2",
+  "description": "Codegen for fine-grained Shiki bundles.",
+  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
+  "license": "MIT",
+  "homepage": "https://github.com/shikijs/shiki#readme",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/shikijs/shiki.git",
+    "directory": "packages/codegen"
+  },
+  "bugs": "https://github.com/shikijs/shiki/issues",
+  "keywords": [
+    "shiki",
+    "codegen"
+  ],
+  "sideEffects": false,
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.mts",
+      "default": "./dist/index.mjs"
+    },
+    "./cli": {
+      "types": "./dist/cli.d.mts",
+      "default": "./dist/cli.mjs"
+    }
+  },
+  "main": "./dist/index.mjs",
+  "module": "./dist/index.mjs",
+  "types": "./dist/index.d.mts",
+  "bin": {
+    "shiki-codegen": "bin.mjs"
+  },
+  "files": [
+    "bin.mjs",
+    "dist"
+  ],
+  "scripts": {
+    "build": "unbuild",
+    "dev": "unbuild --stub",
+    "prepublishOnly": "nr build",
+    "test": "vitest"
+  },
+  "dependencies": {
+    "cac": "catalog:",
+    "prettier": "catalog:",
+    "shiki": "workspace:*"
+  }
+}
diff --git a/packages/codegen/src/cli.ts b/packages/codegen/src/cli.ts
new file mode 100644
index 00000000..9fce278a
--- /dev/null
+++ b/packages/codegen/src/cli.ts
@@ -0,0 +1,53 @@
+import fs from 'node:fs/promises'
+import { dirname, resolve } from 'node:path'
+import { cac } from 'cac'
+import { codegen } from '.'
+
+const cli = cac('shiki-codegen')
+
+cli
+  .command('[path]', 'Generate shiki bundle module')
+  .option('--themes <themes>', 'Themes to include')
+  .option('--langs <langs>', 'Languages to include')
+  .option('--engine <engine>', 'Engine to use', { default: 'javascript' })
+  .option('--precompiled', 'Use precompiled languages', { default: false })
+  .option('--shorthands', 'Generate shorthands', { default: true })
+  .option('--format', 'Use prettier to format', { default: true })
+  .action(async (path, options) => {
+    const output: string = resolve(path || 'shiki.bundle.ts')
+
+    const langs = ((Array.isArray(options.langs) ? options.langs.join(',') : options.langs || '') as string)
+      .split(',')
+      .map(lang => lang.trim())
+      .filter(Boolean)
+    const themes = ((Array.isArray(options.themes) ? options.themes.join(',') : options.themes || '') as string)
+      .split(',')
+      .map(theme => theme.trim())
+      .filter(Boolean)
+
+    const isTypeScript = !!output.match(/\.[cm]?ts$/i)
+
+    if (!themes.length) {
+      throw new Error('No themes specified, use --themes=theme-name to specify themes')
+    }
+    if (!langs.length) {
+      throw new Error('No langs specified, use --langs=lang-name to specify langs')
+    }
+
+    const { code } = await codegen({
+      langs: langs as any[],
+      themes: themes as any[],
+      engine: options.engine,
+      precompiled: options.precompiled,
+      shorthands: options.shorthands,
+      format: options.format,
+      typescript: isTypeScript,
+    })
+
+    await fs.mkdir(dirname(output), { recursive: true })
+    await fs.writeFile(output, code, 'utf-8')
+    console.log(`Generated bundle to ${output}`)
+  })
+
+cli.help()
+cli.parse()
diff --git a/packages/codegen/src/index.ts b/packages/codegen/src/index.ts
new file mode 100644
index 00000000..06d3b7e4
--- /dev/null
+++ b/packages/codegen/src/index.ts
@@ -0,0 +1,223 @@
+import type { Options as PrettierOptions } from 'prettier'
+import type { BundledLanguage, BundledTheme } from 'shiki'
+import { bundledLanguagesInfo, bundledThemesInfo } from 'shiki/bundle/full'
+
+export interface ShikiCodegenOptions {
+  /**
+   * The header to add to the generated code.
+   *
+   * @default '/* Generate by @shikijs/codegen *\/'
+   */
+  header?: string
+
+  /**
+   * The languages to bundle.
+   */
+  langs: readonly BundledLanguage[]
+
+  /**
+   * The themes to bundle.
+   */
+  themes: readonly BundledTheme[]
+
+  /**
+   * The engine to use for syntax highlighting.
+   */
+  engine: 'oniguruma' | 'javascript' | 'javascript-raw'
+
+  /**
+   * Use precompiled grammars.
+   * Only available when `engine` is set to `javascript` or `javascript-raw`.
+   */
+  precompiled?: boolean
+
+  /**
+   * Whether to generate TypeScript code.
+   *
+   * @default true
+   */
+  typescript?: boolean
+
+  /**
+   * Generate shorthands for the highlighter.
+   *
+   * @default true
+   */
+  shorthands?: boolean
+
+  /**
+   * Use Prettier to format the generated code.
+   */
+  format?: boolean | PrettierOptions
+}
+
+export interface ShikiCodegenResult {
+  code: string
+}
+
+export async function codegen(options: ShikiCodegenOptions): Promise<ShikiCodegenResult> {
+  const {
+    header = '/* Generate by @shikijs/codegen */',
+    typescript = true,
+    precompiled = false,
+    format: _format = true,
+    shorthands = true,
+  } = options
+
+  const ts = (code: string): string => typescript ? code : ''
+
+  if (precompiled && options.engine !== 'javascript' && options.engine !== 'javascript-raw')
+    throw new Error('Precompiled grammars are only available when using the JavaScript engine')
+
+  const langs = options.langs.map((lang) => {
+    const info = bundledLanguagesInfo.find(i => i.id === lang || i.aliases?.includes(lang))
+    if (!info)
+      throw new Error(`Language ${lang} not found`)
+    return info
+  })
+
+  const themes = options.themes.map((theme) => {
+    const info = bundledThemesInfo.find(i => i.id === theme)
+    if (!info)
+      throw new Error(`Theme ${theme} not found`)
+    return info
+  })
+
+  const langsCode = `{\n${langs.flatMap((lang) => {
+    const ids = [lang.id, ...(lang.aliases || [])]
+    return ids.map((id) => {
+      return `${JSON.stringify(id)}: () => import('@shikijs/${precompiled ? 'langs-precompiled' : 'langs'}/${lang.id}'),\n`
+    })
+  }).join('')}}`
+
+  const themesCode = `{\n${themes.map((theme) => {
+    return `${JSON.stringify(theme.id)}: () => import('@shikijs/themes/${theme.id}'),\n`
+  }).join('')}}`
+
+  const typeImports: Record<string, string[]> = {
+    '@shikijs/types': ['HighlighterGeneric', 'DynamicImportThemeRegistration', 'DynamicImportLanguageRegistration'],
+  }
+  const imports: Record<string, string[]> = {
+    '@shikijs/core': ['createdBundledHighlighter'],
+  }
+  const lines: string[] = [
+    '',
+  ]
+  const exports: string[] = []
+  const typeExports: string[] = []
+
+  if (typescript) {
+    lines.push(
+      '',
+      `type BundledLanguage = ${langs.flatMap(lang => [lang.id, ...(lang.aliases || [])]).map(lang => `'${lang}'`).join(' | ')}`,
+      `type BundledTheme = ${themes.map(theme => `'${theme.id}'`).join(' | ')}`,
+      `type Highlighter = HighlighterGeneric<BundledLanguage, BundledTheme>`,
+      '',
+    )
+    typeExports.push('BundledLanguage', 'BundledTheme', 'Highlighter')
+  }
+
+  lines.push(
+    '',
+    `const bundledLanguages = ${langsCode}${ts(' as Record<BundledLanguage, DynamicImportLanguageRegistration>')}`,
+    '',
+    `const bundledThemes = ${themesCode}${ts(' as Record<BundledTheme, DynamicImportThemeRegistration>')}`,
+    '',
+  )
+  exports.push('bundledLanguages', 'bundledThemes')
+
+  let engine: string
+
+  if (options.engine === 'javascript') {
+    imports['@shikijs/engine-javascript'] = ['createJavaScriptRegexEngine']
+    engine = 'createJavaScriptRegexEngine()'
+  }
+  else if (options.engine === 'javascript-raw') {
+    imports['@shikijs/engine-javascript/raw'] = ['createJavaScriptRawEngine']
+    engine = 'createJavaScriptRawEngine()'
+  }
+  else {
+    imports['@shikijs/engine-oniguruma'] = ['createOnigurumaEngine']
+    engine = 'createOnigurumaEngine(import(\'shiki/wasm\'))'
+  }
+
+  lines.push(
+    '',
+    `const createHighlighter = /* @__PURE__ */ createdBundledHighlighter${ts('<BundledLanguage, BundledTheme>')}({`,
+    `  langs: bundledLanguages,`,
+    `  themes: bundledThemes,`,
+    `  engine: () => ${engine}`,
+    `})`,
+  )
+  exports.push('createHighlighter')
+
+  if (shorthands) {
+    imports['@shikijs/core'].push('createSingletonShorthands')
+    const shorthandFunctions = [
+      'codeToHtml',
+      'codeToHast',
+      'codeToTokensBase',
+      'codeToTokens',
+      'codeToTokensWithThemes',
+      'getSingletonHighlighter',
+      'getLastGrammarState',
+    ]
+    lines.push(
+      '',
+      `const { ${shorthandFunctions.join(', ')} } = /* @__PURE__ */ createSingletonShorthands${ts('<BundledLanguage,BundledTheme>')}(createHighlighter)`,
+    )
+    exports.push(
+      ...shorthandFunctions,
+    )
+  }
+
+  // Imports
+  lines.unshift(
+    Object.entries(imports).map(([module, imports]) => {
+      return `import { ${imports.sort().join(', ')} } from '${module}'`
+    },
+    ).join('\n'),
+  )
+
+  if (typescript) {
+    lines.unshift(
+      Object.entries(typeImports).map(([module, types]) => {
+        return `import type { ${types.sort().join(', ')} } from '${module}'`
+      },
+      ).join('\n'),
+    )
+  }
+
+  // Exports
+  lines.push(
+    '',
+    `export { ${exports.sort().join(', ')} }`,
+  )
+  if (typescript) {
+    lines.push(
+      `export type { ${typeExports.sort().join(', ')} }`,
+    )
+  }
+
+  lines.unshift(header)
+
+  // Format code
+  let code = lines.join('\n')
+  if (_format) {
+    const { format } = await import('prettier')
+    const prettierOptions: PrettierOptions = {
+      parser: typescript ? 'typescript' : 'babel',
+      semi: false,
+      tabWidth: 2,
+      useTabs: false,
+      singleQuote: true,
+      trailingComma: 'all',
+      ...(_format === true ? {} : _format),
+    }
+    code = await format(code, prettierOptions)
+  }
+
+  return {
+    code,
+  }
+}
diff --git a/packages/engine-oniguruma/package.json b/packages/engine-oniguruma/package.json
index f58342d7..7daef9e9 100644
--- a/packages/engine-oniguruma/package.json
+++ b/packages/engine-oniguruma/package.json
@@ -55,6 +55,6 @@
     "@shikijs/vscode-textmate": "catalog:"
   },
   "devDependencies": {
-    "vscode-oniguruma": "^1.7.0"
+    "vscode-oniguruma": "catalog:"
   }
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3e2df818..f3ad500a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -75,9 +75,15 @@ catalogs:
     bumpp:
       specifier: ^9.10.0
       version: 9.10.0
+    cac:
+      specifier: ^6.7.14
+      version: 6.7.14
     chalk:
       specifier: ^5.4.1
       version: 5.4.1
+    diff-match-patch-es:
+      specifier: ^1.0.1
+      version: 1.0.1
     eslint:
       specifier: ^9.18.0
       version: 9.18.0
@@ -255,7 +261,7 @@ overrides:
   typescript: ^5.7.3
   unified: ^11.0.5
   vite: ^6.0.7
-  vscode-oniguruma: ^1.7.0
+  vscode-oniguruma: 1.7.0
 
 importers:
 
@@ -337,7 +343,7 @@ importers:
         specifier: 'catalog:'
         version: 9.10.0(magicast@0.3.5)
       diff-match-patch-es:
-        specifier: ^1.0.1
+        specifier: 'catalog:'
         version: 1.0.1
       eslint:
         specifier: 'catalog:'
@@ -495,6 +501,18 @@ importers:
         specifier: 'catalog:'
         version: 1.2.5
 
+  packages/codegen:
+    dependencies:
+      cac:
+        specifier: 'catalog:'
+        version: 6.7.14
+      prettier:
+        specifier: 'catalog:'
+        version: 3.4.2
+      shiki:
+        specifier: workspace:*
+        version: link:../shiki
+
   packages/colorized-brackets:
     dependencies:
       shiki:
@@ -572,7 +590,7 @@ importers:
         version: 10.0.1
     devDependencies:
       vscode-oniguruma:
-        specifier: ^1.7.0
+        specifier: 1.7.0
         version: 1.7.0
 
   packages/langs:
@@ -713,7 +731,7 @@ importers:
         specifier: 'catalog:'
         version: 1.9.8
       vscode-oniguruma:
-        specifier: ^1.7.0
+        specifier: 1.7.0
         version: 1.7.0
 
   packages/themes:
@@ -6930,7 +6948,7 @@ snapshots:
       globals: 15.14.0
       kolorist: 1.8.0
       local-pkg: 0.5.1
-      mlly: 1.7.3
+      mlly: 1.7.4
     transitivePeerDependencies:
       - supports-color
 
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index c34158fe..356fc787 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -32,7 +32,9 @@ catalog:
   '@vueuse/core': ^12.4.0
   ansi-sequence-parser: ^1.1.1
   bumpp: ^9.10.0
+  cac: ^6.7.14
   chalk: ^5.4.1
+  diff-match-patch-es: ^1.0.1
   eslint: ^9.18.0
   eslint-plugin-format: ^1.0.1
   esno: ^4.8.0
@@ -86,7 +88,7 @@ catalog:
   vitepress: ^1.5.0
   vitepress-plugin-mermaid: ^2.0.17
   vitest: ^2.1.8
-  vscode-oniguruma: ^1.7.0
+  vscode-oniguruma: 1.7.0
   vue: ^3.5.13
   vue-tsc: ^2.2.0
   wrangler: ^3.101.0


Test patch:
diff --git a/packages/codegen/test/__snapshots__/basic-oniguruma-js.js b/packages/codegen/test/__snapshots__/basic-oniguruma-js.js
new file mode 100644
index 00000000..b8e8d696
--- /dev/null
+++ b/packages/codegen/test/__snapshots__/basic-oniguruma-js.js
@@ -0,0 +1,48 @@
+/* Generate by @shikijs/codegen */
+import {
+  createSingletonShorthands,
+  createdBundledHighlighter,
+} from '@shikijs/core'
+import { createOnigurumaEngine } from '@shikijs/engine-oniguruma'
+
+const bundledLanguages = {
+  javascript: () => import('@shikijs/langs/javascript'),
+  js: () => import('@shikijs/langs/javascript'),
+  typescript: () => import('@shikijs/langs/typescript'),
+  ts: () => import('@shikijs/langs/typescript'),
+  tsx: () => import('@shikijs/langs/tsx'),
+}
+
+const bundledThemes = {
+  nord: () => import('@shikijs/themes/nord'),
+  'vitesse-dark': () => import('@shikijs/themes/vitesse-dark'),
+}
+
+const createHighlighter = /* @__PURE__ */ createdBundledHighlighter({
+  langs: bundledLanguages,
+  themes: bundledThemes,
+  engine: () => createOnigurumaEngine(import('shiki/wasm')),
+})
+
+const {
+  codeToHtml,
+  codeToHast,
+  codeToTokensBase,
+  codeToTokens,
+  codeToTokensWithThemes,
+  getSingletonHighlighter,
+  getLastGrammarState,
+} = /* @__PURE__ */ createSingletonShorthands(createHighlighter)
+
+export {
+  bundledLanguages,
+  bundledThemes,
+  codeToHast,
+  codeToHtml,
+  codeToTokens,
+  codeToTokensBase,
+  codeToTokensWithThemes,
+  createHighlighter,
+  getLastGrammarState,
+  getSingletonHighlighter,
+}
diff --git a/packages/codegen/test/__snapshots__/basic-oniguruma.ts b/packages/codegen/test/__snapshots__/basic-oniguruma.ts
new file mode 100644
index 00000000..22850408
--- /dev/null
+++ b/packages/codegen/test/__snapshots__/basic-oniguruma.ts
@@ -0,0 +1,63 @@
+/* Generate by @shikijs/codegen */
+import type {
+  DynamicImportLanguageRegistration,
+  DynamicImportThemeRegistration,
+  HighlighterGeneric,
+} from '@shikijs/types'
+import {
+  createSingletonShorthands,
+  createdBundledHighlighter,
+} from '@shikijs/core'
+import { createOnigurumaEngine } from '@shikijs/engine-oniguruma'
+
+type BundledLanguage = 'javascript' | 'js' | 'typescript' | 'ts' | 'tsx'
+type BundledTheme = 'nord' | 'vitesse-dark'
+type Highlighter = HighlighterGeneric<BundledLanguage, BundledTheme>
+
+const bundledLanguages = {
+  javascript: () => import('@shikijs/langs/javascript'),
+  js: () => import('@shikijs/langs/javascript'),
+  typescript: () => import('@shikijs/langs/typescript'),
+  ts: () => import('@shikijs/langs/typescript'),
+  tsx: () => import('@shikijs/langs/tsx'),
+} as Record<BundledLanguage, DynamicImportLanguageRegistration>
+
+const bundledThemes = {
+  nord: () => import('@shikijs/themes/nord'),
+  'vitesse-dark': () => import('@shikijs/themes/vitesse-dark'),
+} as Record<BundledTheme, DynamicImportThemeRegistration>
+
+const createHighlighter = /* @__PURE__ */ createdBundledHighlighter<
+  BundledLanguage,
+  BundledTheme
+>({
+  langs: bundledLanguages,
+  themes: bundledThemes,
+  engine: () => createOnigurumaEngine(import('shiki/wasm')),
+})
+
+const {
+  codeToHtml,
+  codeToHast,
+  codeToTokensBase,
+  codeToTokens,
+  codeToTokensWithThemes,
+  getSingletonHighlighter,
+  getLastGrammarState,
+} = /* @__PURE__ */ createSingletonShorthands<BundledLanguage, BundledTheme>(
+  createHighlighter,
+)
+
+export {
+  bundledLanguages,
+  bundledThemes,
+  codeToHast,
+  codeToHtml,
+  codeToTokens,
+  codeToTokensBase,
+  codeToTokensWithThemes,
+  createHighlighter,
+  getLastGrammarState,
+  getSingletonHighlighter,
+}
+export type { BundledLanguage, BundledTheme, Highlighter }
diff --git a/packages/codegen/test/__snapshots__/basic-precompiled.ts b/packages/codegen/test/__snapshots__/basic-precompiled.ts
new file mode 100644
index 00000000..a55c7590
--- /dev/null
+++ b/packages/codegen/test/__snapshots__/basic-precompiled.ts
@@ -0,0 +1,37 @@
+/* Generate by @shikijs/codegen */
+import type {
+  DynamicImportLanguageRegistration,
+  DynamicImportThemeRegistration,
+  HighlighterGeneric,
+} from '@shikijs/types'
+import { createdBundledHighlighter } from '@shikijs/core'
+import { createJavaScriptRawEngine } from '@shikijs/engine-javascript/raw'
+
+type BundledLanguage = 'javascript' | 'js' | 'typescript' | 'ts' | 'tsx'
+type BundledTheme = 'nord' | 'vitesse-dark'
+type Highlighter = HighlighterGeneric<BundledLanguage, BundledTheme>
+
+const bundledLanguages = {
+  javascript: () => import('@shikijs/langs-precompiled/javascript'),
+  js: () => import('@shikijs/langs-precompiled/javascript'),
+  typescript: () => import('@shikijs/langs-precompiled/typescript'),
+  ts: () => import('@shikijs/langs-precompiled/typescript'),
+  tsx: () => import('@shikijs/langs-precompiled/tsx'),
+} as Record<BundledLanguage, DynamicImportLanguageRegistration>
+
+const bundledThemes = {
+  nord: () => import('@shikijs/themes/nord'),
+  'vitesse-dark': () => import('@shikijs/themes/vitesse-dark'),
+} as Record<BundledTheme, DynamicImportThemeRegistration>
+
+const createHighlighter = /* @__PURE__ */ createdBundledHighlighter<
+  BundledLanguage,
+  BundledTheme
+>({
+  langs: bundledLanguages,
+  themes: bundledThemes,
+  engine: () => createJavaScriptRawEngine(),
+})
+
+export { bundledLanguages, bundledThemes, createHighlighter }
+export type { BundledLanguage, BundledTheme, Highlighter }
diff --git a/packages/codegen/test/codegen.test.ts b/packages/codegen/test/codegen.test.ts
new file mode 100644
index 00000000..e3e4186b
--- /dev/null
+++ b/packages/codegen/test/codegen.test.ts
@@ -0,0 +1,31 @@
+import { expect, it } from 'vitest'
+import { codegen } from '../src'
+
+it('should work', async () => {
+  const langs = ['javascript', 'ts', 'tsx'] as const
+  const themes = ['nord', 'vitesse-dark'] as const
+
+  await expect((await codegen({
+    langs,
+    themes,
+    engine: 'oniguruma',
+  })).code)
+    .toMatchFileSnapshot('./__snapshots__/basic-oniguruma.ts')
+
+  await expect((await codegen({
+    langs,
+    themes,
+    typescript: false,
+    engine: 'oniguruma',
+  })).code)
+    .toMatchFileSnapshot('./__snapshots__/basic-oniguruma-js.js')
+
+  await expect((await codegen({
+    langs,
+    themes,
+    engine: 'javascript-raw',
+    precompiled: true,
+    shorthands: false,
+  })).code)
+    .toMatchFileSnapshot('./__snapshots__/basic-precompiled.ts')
+})