Repository: logue/vue-codemirror6 Branch: master Commit: d836afe8e65d Files: 45 Total size: 195.7 KB Directory structure: gitextract_ynp_uc9x/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── copilot-instructions.md │ └── workflows/ │ └── build-docs.yml ├── .gitignore ├── .nvmrc ├── .oxlintrc.json ├── .prettierignore ├── .prettierrc.yaml ├── AGENT.md ├── CHANGELOG.md ├── LICENSE ├── README.ja.md ├── README.md ├── SSR_FIX_SUMMARY.md ├── TESTING.md ├── env.d.ts ├── eslint.config.ts ├── index.html ├── package.json ├── pnpm-workspace.yaml ├── scripts/ │ └── sync-dts-entry.mjs ├── src/ │ ├── __tests__/ │ │ ├── CodeMirror.spec.ts │ │ └── CodeMirror.ssr.spec.ts │ ├── helpers/ │ │ └── h-demi.ts │ ├── index.ts │ └── interfaces/ │ └── MetaInterface.ts ├── src-docs/ │ ├── App.vue │ ├── DemoPage.vue │ ├── components/ │ │ ├── KeyMapDemo.vue │ │ ├── LinterAndCrossBindingDemo.vue │ │ ├── MarkdownDemo.vue │ │ ├── ReadonlyAndDisabledDemo.vue │ │ ├── SlotDemo.vue │ │ └── ToggleTheme.vue │ ├── main.ts │ └── style.scss ├── tsconfig.app.json ├── tsconfig.docs.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.vitest.json ├── vite.config.ts └── vitest.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 trim_trailing_whitespace = true ================================================ FILE: .gitattributes ================================================ * text=auto * vue eol=lf encoding=UTF-8 *.css eol=lf encoding=UTF-8 *.html eol=lf encoding=UTF-8 *.js eol=lf encoding=UTF-8 *.jsx eol=lf encoding=UTF-8 *.md eol=lf encoding=UTF-8 *.scss eol=lf encoding=UTF-8 *.ts eol=lf encoding=UTF-8 *.tsx eol=lf encoding=UTF-8 *.txt eol=lf encoding=UTF-8 *.xml eol=lf encoding=UTF-8 # Images *.gif binary *.ico binary *.jpg binary *.png binary *.svg eol=lf encoding=UTF-8 *.webp binary # Fonts *.eot binary *.otf binary *.ttf binary *.woff binary ================================================ FILE: .github/CODEOWNERS ================================================ # Generated by CODEOWNERS.com ================================================ FILE: .github/copilot-instructions.md ================================================ # Vue-CodeMirror6 Workspace Instructions **Project**: A Vue 2 & 3 compatible CodeMirror 6 component library **Tech Stack**: TypeScript, Vue 3 (with vue-demi for Vue 2 support), Vite, Vitest, CodeMirror 6 **Language**: Primarily TypeScript with Vue SFC components ## Essential Commands | Task | Command | | ---------------------- | -------------------------------------------------- | | **Development server** | `pnpm dev` | | **Build library** | `pnpm build` (includes type checking) | | **Build docs** | `pnpm build:docs` then `pnpm preview` | | **Run tests** | `pnpm test` (watch mode) or `pnpm test:run` (once) | | **Test coverage** | `pnpm test:coverage` | | **All linting** | `pnpm lint` (oxlint, eslint, prettier) | | **Type check** | `pnpm type-check` (vue-tsc) | ## Architecture & Key Patterns ### Main Component (`src/index.ts`, `src/Meta.ts`) - **Vue Composition API** with `vue-demi` for Vue 2/3 compatibility - **Core Props**: `modelValue`, `lang`, `extensions`, `linter`, `keymap`, `dark`, `readonly`, `disabled`, etc. - **Exposed Methods**: `getView()`, `focus()`, `getRange()`, `setCursor()`, `getSelection()` (CodeMirror5 API compatibility layer) - **Event Emitters**: `ready`, `update`, `change`, `destroy` - **Two Setup Modes**: `basic` (basicSetup) or `minimal` (minimalSetup) ### Design Principles 1. **Unidirectional + v-model binding**: Text content updates flow via v-model 2. **Optional ChainING on `view.value`**: All CodeMirror view access uses `view.value?.` to handle SSR 3. **Props over Extensions**: Explicit props (`lang`, `linter`, `keymap`) separate from generic `extensions[]` for better type safety and DX 4. **Lazy Initialization**: Editor only initializes in browser (client-side), SSR-safe with `onMounted` checks ### Testing Strategy (`src/__tests__/`) - **`CodeMirror.spec.ts`**: Component functionality (rendering, props, v-model, events, slots, public methods) - **`CodeMirror.ssr.spec.ts`**: SSR compatibility (server-side rendering, safe method calls, hydration, cleanup) - **Framework**: Vitest with happy-dom environment - **Setup**: Uses Vue Test Utils for component mounting - **Coverage Targets**: All public methods and critical code paths (see vitest.config.ts for exclusions) ## Code Quality Standards ### Linting & Formatting - **Oxlint**: Fast, Rust-based linting (primary) - **ESLint**: Vue plugin + TypeScript rules + accessibility checks - **Prettier**: Code formatting - **vue-tsc**: Type checking before build Run all checks: `pnpm lint` (automatically fixes most issues) ### TypeScript - **Strict Mode**: Enabled - **Vue Support**: `@vue/eslint-config-typescript` - **Type Declarations**: Auto-generated via `vite-plugin-dts` during build - **Aliases**: `@` → `src/`, `vue-codemirror6` → `src/` ## Important Context ### SSR Compatibility (Critical) The component must work in SSR environments (Nuxt.js, etc.): - `view.value` may be `undefined` on server ⚠️ - Always use optional chaining: `view.value?.method()` - Browser-only code wrapped in `if (typeof window !== 'undefined')` - See [SSR_FIX_SUMMARY.md](../SSR_FIX_SUMMARY.md) for detailed changes ### Build Outputs ### Generated Metadata - `src/Meta.ts` is generated automatically when starting the dev server or running the library build. - If `src/Meta.ts` is missing in a fresh checkout, run `pnpm dev` or `pnpm build` before treating it as a broken import. Multiple formats in `dist/`: - ES modules: `index.es.js` - CommonJS: `index.cjs.js` - UMD: `index.umd.js` - IIFE: `index.iife.js` - Types: `index.d.ts` ### Peer Dependencies CodeMirror 6 packages are peer deps (not bundled): ```text @codemirror/{commands,language,lint,search,state,view} @codemirror/autocomplete codemirror (state/view core) style-mod vue: ^2.7.14 || ^3.3.4 ``` Users must install these separately to avoid duplication. ### Common Development Tasks **Adding a new prop**: Add to interface → component props → applicable compartment/state → test it **Adding language support demo**: Add to `src-docs/components/` and import in `App.vue` **Fixing a bug**: Create test first in `__tests__/`, implement fix in `src/`, verify with `pnpm test` **Updating themes or extensions**: Use the `extensions` prop or create a helper function in `src/helpers/` ## Key Files Reference | File | Purpose | | ------------------------------------------------------------------------------- | ----------------------------------------------- | | [src/index.ts](../src/index.ts) | Main component definition | | [src/Meta.ts](../src/Meta.ts) | Component metadata and type definitions | | [src/**tests**/CodeMirror.spec.ts](../src/__tests__/CodeMirror.spec.ts) | Component tests | | [src/**tests**/CodeMirror.ssr.spec.ts](../src/__tests__/CodeMirror.ssr.spec.ts) | SSR tests | | [vite.config.ts](../vite.config.ts) | Build config (outputs, plugins, DTS generation) | | [vitest.config.ts](../vitest.config.ts) | Test config (happy-dom, coverage) | | [eslint.config.ts](../eslint.config.ts) | Linting config | ## Common Pitfalls to Avoid 1. **Direct `view` access without optional chaining** → SSR will break 2. **Importing `@codemirror` modules in component** → May cause bundling issues; prefer `extensions` prop 3. **Mutating props directly** → Use emitted events or expose methods instead 4. **Async state changes without `nextTick`** → Can cause race conditions in updates 5. **Forgetting to test SSR mode** → Use `pnpm test` to run all tests including SSR ## Workspace Conventions - **Vue 3 syntax throughout** (vue-demi handles Vue 2 compatibility) - **TypeScript strict mode** - **Composition API only** (no Options API) - **Kebab-case for component props** (auto-converted from camelCase) - **PascalCase for type names**, camelCase for variables/functions - **Comments for complex CodeMirror API usage** (document non-obvious patterns) ================================================ FILE: .github/workflows/build-docs.yml ================================================ name: NodeJS with Vite on: push: branches: ['master'] pull_request: branches: ['master'] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [22.x] steps: - name: Checkout ✅ uses: actions/checkout@v4.2.2 - name: Use Node.js ${{ matrix.node-version }} ⚡ uses: actions/setup-node@v4.1.0 with: node-version: ${{ matrix.node-version }} - name: Install pnpm 🎁 uses: pnpm/action-setup@v4 - name: Build 🔧 run: | pnpm install pnpm run build:docs - name: Deploy to gh-pages 🚀 uses: JamesIves/github-pages-deploy-action@v4.6.8 with: branch: gh-pages # The branch the action should deploy to. folder: docs # The folder the action should deploy. ================================================ FILE: .gitignore ================================================ /.yarn/* # !/.yarn/patches # !/.yarn/plugins !/.yarn/releases # !/.yarn/sdks # !/.yarn/cache /.vscode/* node_modules .DS_Store dist dist-ssr *.local /src/Meta.ts docs/ stats.html *.tsbuildinfo ================================================ FILE: .nvmrc ================================================ 24.14.1 ================================================ FILE: .oxlintrc.json ================================================ { "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"], "env": { "browser": true }, "categories": { "correctness": "error" } } ================================================ FILE: .prettierignore ================================================ .husky/ .vscode/ .yarn/ dist/ public/assets/ docs/ stats.html ================================================ FILE: .prettierrc.yaml ================================================ printWidth: 80 tabWidth: 2 useTabs: false semi: true singleQuote: true trailingComma: es5 bracketSpacing: true bracketSameLine: false arrowParens: avoid htmlWhitespaceSensitivity: ignore endOfLine: lf ================================================ FILE: AGENT.md ================================================ # AGENT.md This file provides guidance for AI coding agents (GitHub Copilot, Claude, Cursor, etc.) working in this repository. --- ## Project Overview CodeMirror component for Vue2 and Vue3. - **Framework**: Vue 3 (` ``` ### スロットを使用した例 スロットの内容は既存の`v-model`を上書きします。このため、`v-model`を使用せずに`readonly` propで単に表示する場合に使用することをお勧めします。 また、スロット内のテキストが自動的にフォーマットされないように、`
`タグを挿入します。

```vue



```

### SSR(Nuxt.jsなど)での使用

このコンポーネントはSSR互換になりました。CodeMirrorはクライアント側でのみ初期化され、サーバー側レンダリング中にコンポーネントがエラーなく安全にレンダリングされます。

Nuxt 3を使用している場合は、コンポーネントを直接使用できます:

```vue



```

Nuxt 2を使用している場合や問題が発生した場合は、コンポーネントを``でラップできます:

```vue

```

### 完全な例

[vite-vue3-ts-starter](https://github.com/logue/vite-vue3-ts-starter)でMarkdownエディターとして使用する場合。

```vue



```

## イベント

| イベント | 説明                                                                                                                     |
| -------- | ------------------------------------------------------------------------------------------------------------------------ |
| ready    | CodeMirrorが読み込まれたとき。                                                                                           |
| focus    | フォーカスが変更されたとき。                                                                                             |
| update   | CodeMirrorの状態が変更されたとき。[ViewUpdate](https://codemirror.net/docs/ref/#view.ViewUpdate)オブジェクトを返します。 |
| change   | 値が変更されたとき。[EditorState](https://codemirror.net/docs/ref/#state.EditorState)を返します。                        |

## パラメータ / 関数

```vue


```

| 関数 / パラメータ  | 説明                                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------------------- |
| view               | [EditorView](https://codemirror.net/docs/ref/#view.EditorView)を取得および設定します。                        |
| selection          | [EditorSelection](https://codemirror.net/docs/ref/#state.EditorSelection)インスタンスを取得および設定します。 |
| cursor             | [Cursor](https://codemirror.net/docs/ref/#state.EditorSelection^cursor)の位置を取得および設定します。         |
| json               | 状態をJSON直列化可能なオブジェクトとして取得および設定します。                                                |
| focus              | [Focus](https://codemirror.net/docs/ref/#view.EditorView.focus)を取得および設定します。                       |
| lint()             | リンターを強制実行します(`linter`propが指定されている場合のみ)。                                            |
| forceReconfigure() | すべての拡張機能を再登録します。                                                                              |

### CodeMirror5の後方互換関数

以下の説明は、[codemirror5](https://codemirror.net/5/)に精通している方向けの互換メソッドです。
通常、上記のメソッドで十分であるため、**積極的な使用は推奨されません**。

| 関数                                                                | 説明                                                                                     |
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| getRange(from?: number, to?: number)                                | エディター内の指定されたポイント間のテキストを取得します。                               |
| getLine(number: number)                                             | 行の内容を取得します。                                                                   |
| lineCount()                                                         | エディターの行数を取得します。                                                           |
| getCursor()                                                         | 主選択の一方の端を取得します。                                                           |
| listSelections()                                                    | 現在のすべての選択のリストを取得します。                                                 |
| getSelection()                                                      | 現在選択されているコードを取得します。                                                   |
| getSelections()                                                     | 指定された配列の長さは、アクティブな選択の数と同じである必要があります。                 |
| somethingSelected()                                                 | テキストが選択されている場合はtrueを返します。                                           |
| replaceRange(replacement: string \| Text, from: number, to: number) | fromからtoまでのドキュメントの部分を指定された文字列で置き換えます。                     |
| replaceSelection(replacement: string \| Text)                       | 選択を指定された文字列で置き換えます。                                                   |
| setCursor(position: number)                                         | カーソル位置を設定します。                                                               |
| setSelection(anchor: number, head?: number)                         | 単一の選択範囲を設定します。                                                             |
| setSelections(ranges: readonly SelectionRange[], primary?: number)  | 新しい選択のセットを設定します。                                                         |
| extendSelectionsBy(f: Function)                                     | すべての既存の選択に指定された関数を適用し、結果に対してextendSelectionsを呼び出します。 |

## 推奨事項

CodeMirrorは比較的大容量であるため、[vite](https://vitejs.dev)を使用する場合は、ビルド時に以下のように[`manualChunks`](https://vitejs.dev/guide/build.html#chunking-strategy)オプションを使用して別ファイルとして出力するように設定することをお勧めします。

```ts
const config: UserConfig = {
  // ...
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // ...
          codemirror: ['vue-codemirror6'],
          'codemirror-lang': [
            // 必要に応じて以下を追加してください。
            '@codemirror/lang-html',
            '@codemirror/lang-javascript',
            '@codemirror/lang-markdown',
          ],
          // ...
        },
      },
    },
  },
  // ...
};
```

## 開発

### テスト

このプロジェクトは、ユニットテストに[Vitest](https://vitest.dev/)を使用しています。

```bash
# テストを実行
pnpm test

# ウォッチモードでテストを実行
pnpm test:watch

# UIを使用してテストを実行
pnpm test:ui

# カバレッジを使用してテストを実行
pnpm test:coverage
```

テストスイートには以下が含まれます:

- **コンポーネントテスト**: 基本的なレンダリング、props、イベント、v-modelバインディングのテスト
- **SSRテスト**: Nuxt.jsやその他のSSRフレームワークの適切なサーバー側レンダリング互換性の確保
- **メソッドテスト**: 公開されているすべてのメソッドが正しく動作することを検証
- **エッジケース**: エラー処理と異常なシナリオのテスト

## ライセンス

©2022-2026 by Logue.
[MITライセンス](LICENSE)の下でライセンスされています。

## 🎨 開発者のために作られました

このライブラリは、**最新の開発者体験**に焦点を当てて構築されています。これを維持するには、すべてがシームレスに動作することを確認するための継続的なテストと更新が必要です。

このプロジェクトの細部へのこだわりを評価していただける場合は、Vue.jsとMetaverseエコシステム全体での私の仕事をサポートするために、小額のスポンサーシップをいただければ幸いです。

[![GitHub Sponsors](https://img.shields.io/github/sponsors/logue?label=Sponsor&logo=github&color=ea4aaa)](https://github.com/sponsors/logue)


================================================
FILE: README.md
================================================
# vue-codemirror6

English | [日本語](README.ja.md)

logo

[![jsdelivr CDN](https://data.jsdelivr.com/v1/package/npm/vue-codemirror6/badge)](https://www.jsdelivr.com/package/npm/vue-codemirror6) [![NPM Downloads](https://img.shields.io/npm/dm/vue-codemirror6.svg?style=flat)](https://www.npmjs.com/package/vue-codemirror6) [![Open in unpkg](https://img.shields.io/badge/Open%20in-unpkg-blue)](https://uiwjs.github.io/npm-unpkg/#/pkg/vue-codemirror6/file/README.md) [![npm version](https://img.shields.io/npm/v/vue-codemirror6.svg)](https://www.npmjs.com/package/vue-codemirror6) [![Open in Gitpod](https://shields.io/badge/Open%20in-Gitpod-green?logo=Gitpod)](https://gitpod.io/#https://github.com/logue/vue-codemirror6) [![Twitter Follow](https://img.shields.io/twitter/follow/logue256?style=plastic)](https://twitter.com/logue256) A component for using [CodeMirror6](https://codemirror.net/6/) with Vue. This component works in both Vue2 and Vue3. - [CHANGELOG](./CHANGELOG.md) ## Usage ```sh yarn add vue-codemirror6 codemirror ``` For Vue 2.7 or below, [@vue/composition-api](https://www.npmjs.com/package/@vue/composition-api) is required separately. ```sh yarn add vue-codemirror6 @vue/composition-api ``` This component can handle bidirectional binding by `v-model` like a general Vue component. ## Props | Props | Type | Information | | ------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | model-value | string \| Text | Text value. (Not `value`) | | basic | boolean | Use [basicSetup](https://codemirror.net/docs/ref/#codemirror.basicSetup). | | minimal | boolean | Use [miniSetup](https://codemirror.net/docs/ref/#codemirror.minimalSetup). If a `basic` prop is also specified, that setting will take precedence. | | dark | boolean | Toggle Darkmode. | | placeholder | string | Add placeholder text (or HTML DOM) when blank | | wrap | boolean | Line text wrapping. see [lineWrapping](https://codemirror.net/6/docs/ref/#view.EditorView.lineWrapping). | | tab | boolean | Enables tab indentation. | | allow-multiple-selections | boolean | Allow Multiple Selection. See [allowMultipleSelections](https://codemirror.net/docs/ref/#state.EditorState^allowMultipleSelections) | | tab-size | number | Configures the tab size to use in this state. | | line-separator | string | Set line break (separetor) char. (Default is `\n`.) | | theme | { [selector: string]: StyleSpec } | Specify the theme. For example, if you use [@codemirror/theme-one-dark](https://github.com/codemirror/theme-one-dark), import `oneDark` and put it in this prop. | | readonly | boolean | Makes the cursor visible or you can drag the text but not edit the value. | | disabled | boolean | This is the reversed value of the CodeMirror editable. Similar to `readonly`, but setting this value to true disables dragging. | | lang | LanguageSupport | The language you want to have syntax highlighting. see | | phrases | Record<string, string> | Specify here if you want to make the displayed character string multilingual. see | | extensions | Extension[] | Includes enhancements to extend CodeMirror. | | linter | LintSource | Set Linter. Enter a linter (eg `esLint([arbitrary rule])` function for `@codemirror/lang-javascript`, `jsonParseLinter()`function for`@codemirror/json`). See the sources for various language libraries for more information. | | linterConfig | Object | see | | forceLinting | boolean | see | | gutter | boolean | Display 🔴 on the line where there was an error when `linter` was specified. It will not work if `linter` is not specified. | | gutterConfig | Object | see | | tag | string | HTML tags used in the component. (Default is `div` tag.) | | scrollIntoView | boolean | Allows an external update to scroll the form. (Default is `true`) | | preserveScrollPosition | boolean | Preserves the editor scroll position when `modelValue` is updated externally. Useful to prevent jumping to the top during appended updates. (Default is `false`) | | keymap | KeyBinding[] | Key bindings associate key names with command-style functions. See | ⚠ Notice: `lang` and `linter` can also be set together in `extensions`. These are separated for compatibility with previous versions of CodeMirror settings and for typing props. ### Supported Languages #### Official - [`@codemirror/lang-angular`](https://www.npmjs.com/package/@codemirror/lang-angular) - [`@codemirror/lang-cpp`](https://www.npmjs.com/package/@codemirror/lang-cpp) - [`@codemirror/lang-css`](https://www.npmjs.com/package/@codemirror/lang-css) - [`@codemirror/lang-html`](https://www.npmjs.com/package/@codemirror/lang-html) - [`@codemirror/lang-java`](https://www.npmjs.com/package/@codemirror/lang-java) - [`@codemirror/lang-javascript`](https://www.npmjs.com/package/@codemirror/lang-javascript) - [`@codemirror/lang-json`](https://www.npmjs.com/package/@codemirror/lang-json) - [`@codemirror/lang-lezer`](https://www.npmjs.com/package/@codemirror/lang-lezer) - [`@codemirror/lang-markdown`](https://www.npmjs.com/package/@codemirror/lang-markdown) - [`@codemirror/lang-php`](https://www.npmjs.com/package/@codemirror/lang-php) - [`@codemirror/lang-python`](https://www.npmjs.com/package/@codemirror/lang-python) - [`@codemirror/lang-rust`](https://www.npmjs.com/package/@codemirror/lang-rust) - [`@codemirror/lang-sql`](https://www.npmjs.com/package/@codemirror/lang-sql) - [`@codemirror/lang-vue`](https://www.npmjs.com/package/@codemirror/lang-vue) - [`@codemirror/lang-west`](https://www.npmjs.com/package/@codemirror/lang-west) - [`@codemirror/lang-xml`](https://www.npmjs.com/package/@codemirror/lang-xml) ### Unofficial - [`@phoenix-plugin-registry/petetnt.brackets-codemirror-fortran`](https://www.npmjs.com/package/@phoenix-plugin-registry/petetnt.brackets-codemirror-fortran) - [`@phoenix-plugin-registry/petetnt.brackets-codemirror-go`](https://www.npmjs.com/package/@phoenix-plugin-registry/petetnt.brackets-codemirror-go) - [`@acarl005/lang-sql`](https://www.npmjs.com/package/@acarl005/lang-sql) - [`@ark-us/codemirror-lang-taylor`](https://www.npmjs.com/package/@ark-us/codemirror-lang-taylor) - [`@formulavize/lang-fiz`](https://www.npmjs.com/package/@formulavize/lang-fiz) - [`@gravitywiz/codemirror-lang-gfcalc`](https://www.npmjs.com/package/@gravitywiz/codemirror-lang-gfcalc) - [`@nextjournal/lang-clojure`](https://www.npmjs.com/package/@nextjournal/lang-clojure) - [`@plutojl/lang-julia`](https://www.npmjs.com/package/@plutojl/lang-julia) - [`@polybase/codemirror-lang-javascript`](https://www.npmjs.com/package/@polybase/codemirror-lang-javascript) -[`@replit/codemirror-lang-nix`](https://www.npmjs.com/package/@replit/codemirror-lang-nix) - [`@replit/codemirror-lang-csharp`](https://www.npmjs.com/package/@replit/codemirror-lang-csharp) - [`@replit/codemirror-lang-solidity`](https://www.npmjs.com/package/@replit/codemirror-lang-solidity) - [`@replit/codemirror-lang-svelte`](https://www.npmjs.com/package/@replit/codemirror-lang-svelte) - [`@zhijiu/lang-sql`](https://www.npmjs.com/package/@zhijiu/lang-sql) - [`codemirror-lang-bool`](https://www.npmjs.com/package/codemirror-lang-bool) - [`codemirror-lang-brainfuck`](https://www.npmjs.com/package/codemirror-lang-brainfuck) - [`codemirror-lang-cherry`](https://www.npmjs.com/package/codemirror-lang-cherry) - [`codemirror-lang-chordpro`](https://www.npmjs.com/package/codemirror-lang-chordpro) - [`codemirror-lang-circom`](https://www.npmjs.com/package/codemirror-lang-circom) - [`codemirror-lang-edn`](https://www.npmjs.com/package/codemirror-lang-edn) - [`codemirror-lang-ejs`](https://www.npmjs.com/package/codemirror-lang-ejs) - [`codemirror-lang-fsl`](https://www.npmjs.com/package/codemirror-lang-fsl) - [`codemirror-lang-gml`](https://www.npmjs.com/package/codemirror-lang-gml) - [`codemirror-lang-golfscript`](https://www.npmjs.com/package/codemirror-lang-golfscript) - [`codemirror-lang-homescript`](https://www.npmjs.com/package/codemirror-lang-homescript) - [`codemirror-lang-html-n8n`](https://www.npmjs.com/package/codemirror-lang-html-n8n) - [`codemirror-lang-inform7`](https://www.npmjs.com/package/codemirror-lang-inform7) - [`codemirror-lang-j`](https://www.npmjs.com/package/codemirror-lang-j) - [`codemirror-lang-janet`](https://www.npmjs.com/package/codemirror-lang-janet) - [`codemirror-lang-k`](https://www.npmjs.com/package/codemirror-lang-k) - [`codemirror-lang-karol`](https://www.npmjs.com/package/codemirror-lang-karol) - [`codemirror-lang-mermaid`](https://www.npmjs.com/package/codemirror-lang-mermaid) - [`codemirror-lang-n8n-expression`](https://www.npmjs.com/package/codemirror-lang-n8n-expression) - [`codemirror-lang-prolog`](https://www.npmjs.com/package/codemirror-lang-prolog) - [`codemirror-lang-qpen`](https://www.npmjs.com/package/codemirror-lang-qpen) - [`codemirror-lang-qtam`](https://www.npmjs.com/package/codemirror-lang-qtam) - [`codemirror-lang-r`](https://www.npmjs.com/package/codemirror-lang-r) - [`codemirror-lang-rome-ast`](https://www.npmjs.com/package/codemirror-lang-rome-ast) - [`codemirror-lang-rome`](https://www.npmjs.com/package/codemirror-lang-rome) - [`codemirror-lang-rush`](https://www.npmjs.com/package/codemirror-lang-rush) - [`codemirror-lang-scopescript`](https://www.npmjs.com/package/codemirror-lang-scopescript) - [`codemirror-lang-statement`](https://www.npmjs.com/package/codemirror-lang-statement) - [`gcode-lang-codemirror`](https://www.npmjs.com/package/gcode-lang-codemirror) - [`gmail-lang`](https://www.npmjs.com/package/gmail-lang) - [`lang-bqn`](https://www.npmjs.com/package/lang-bqn) - [`lang-clojure`](https://www.npmjs.com/package/lang-clojure) - [`lang-d`](https://www.npmjs.com/package/lang-d) - [`lang-feel`](https://www.npmjs.com/package/lang-feel) - [`lang-firestore`](https://www.npmjs.com/package/lang-firestore) ### Supported Themes - [`@codemirror/theme-one-dark`](https://github.com/codemirror/theme-one-dark) - [`upleveled/theme-vs-code-dark-plus`](https://github.com/upleveled/theme-vs-code-dark-plus) - [`codemirror6-bootstrap-theme`](https://github.com/logue/codemirror6-bootstrap-theme) ## Example Mark up as follows to make it work at a minimum. ```vue ``` ### Example using Slots The contents of the slot will overwrite the existing `v-model`. For this reason, it is recommended to use it when simply displaying with a `readonly` prop without using `v-model`. Also, insert a `
` tag to prevent the text in the slot from being automatically formatted.

```vue



```

### Using with SSR (Nuxt.js, etc.)

This component is now SSR-compatible. CodeMirror will only be initialized on the client side, and the component will safely render without errors during server-side rendering.

If you're using Nuxt 3, you can use the component directly:

```vue



```

For Nuxt 2 or if you encounter any issues, you can wrap the component with ``:

```vue

```

### Full Example

When using as a Markdown editor on [vite-vue3-ts-starter](https://github.com/logue/vite-vue3-ts-starter).

```vue



```

## Events

| Event  | Description                                                                                                   |
| ------ | ------------------------------------------------------------------------------------------------------------- |
| ready  | When CodeMirror loaded.                                                                                       |
| focus  | When focus changed.                                                                                           |
| update | When CodeMirror state changed. Returns [ViewUpdate](https://codemirror.net/docs/ref/#view.ViewUpdate) object. |
| change | Value changed. Returns [EditorState](https://codemirror.net/docs/ref/#state.EditorState)                      |

## Parameter / Function

```vue


```

| Function / Parameter | Description                                                                                         |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| view                 | Get and set [EditorView](https://codemirror.net/docs/ref/#view.EditorView).                         |
| selection            | Get and set the [EditorSelection](https://codemirror.net/docs/ref/#state.EditorSelection) instance. |
| cursor               | Get and set the [cursor](https://codemirror.net/docs/ref/#state.EditorSelection^cursor) location.   |
| json                 | Get and set state to a JSON-serializable object.                                                    |
| focus                | Get and set [focus](https://codemirror.net/docs/ref/#view.EditorView.focus).                        |
| lint()               | Force run linter (Only if `linter` prop is specified)                                               |
| forceReconfigure()   | Re register all extensions.                                                                         |

### CodeMirror5 backward compatible functions

The instructions below are compatible methods for those familiar with [codemirror5](https://codemirror.net/5/).
Since the above method is usually sufficient, its **active use is not recommended**.

| Function                                                            | Description                                                                                      |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| getRange(from?: number, to?: number)                                | Get the text between the given points in the editor.                                             |
| getLine(number: number)                                             | Get the content of line.                                                                         |
| lineCount()                                                         | Get the number of lines in the editor.                                                           |
| getCursor()                                                         | Retrieve one end of the primary selection.                                                       |
| listSelections()                                                    | Retrieves a list of all current selections.                                                      |
| getSelection()                                                      | Get the currently selected code.                                                                 |
| getSelections()                                                     | The length of the given array should be the same as the number of active selections.             |
| somethingSelected()                                                 | Return true if any text is selected.                                                             |
| replaceRange(replacement: string \| Text, from: number, to: number) | Replace the part of the document between from and to with the given string.                      |
| replaceSelection(replacement: string \| Text)                       | Replace the selection(s) with the given string.                                                  |
| setCursor(position: number)                                         | Set the cursor position.                                                                         |
| setSelection(anchor: number, head?: number)                         | Set a single selection range.                                                                    |
| setSelections(ranges: readonly SelectionRange[], primary?: number)  | Sets a new set of selections.                                                                    |
| extendSelectionsBy(f: Function)                                     | Applies the given function to all existing selections, and calls extendSelections on the result. |

## Recommendations

Since CodeMirror has a relatively large capacity, when using [vite](https://vitejs.dev), it is recommended to set it to output as a separate file using the [`manualChunks`](https://vitejs.dev/guide/build.html#chunking-strategy) option at build time as shown below.

```ts
const config: UserConfig = {
  // ...
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // ...
          codemirror: ['vue-codemirror6'],
          'codemirror-lang': [
            // Add the following as needed.
            '@codemirror/lang-html',
            '@codemirror/lang-javascript',
            '@codemirror/lang-markdown',
          ],
          // ...
        },
      },
    },
  },
  // ...
};
```

## Development

### Testing

This project uses [Vitest](https://vitest.dev/) for unit testing.

```bash
# Run tests
pnpm test

# Run tests in watch mode
pnpm test:watch

# Run tests with UI
pnpm test:ui

# Run tests with coverage
pnpm test:coverage
```

The test suite includes:

- **Component Tests**: Testing basic rendering, props, events, and v-model binding
- **SSR Tests**: Ensuring proper server-side rendering compatibility for Nuxt.js and other SSR frameworks
- **Method Tests**: Verifying all exposed methods work correctly
- **Edge Cases**: Testing error handling and unusual scenarios

## LICENSE

©2022-2026 by Logue.
Licensed under the [MIT License](LICENSE).

## 🎨 Crafted for Developers

This library is built with a focus **modern developer experience**. Maintaining it involves constant testing and updates to ensure everything works seamlessly.

If you appreciate the attention to detail in this project, a small sponsorship would go a long way in supporting my work across the Vue.js and Metaverse ecosystems.

[![GitHub Sponsors](https://img.shields.io/github/sponsors/logue?label=Sponsor&logo=github&color=ea4aaa)](https://github.com/sponsors/logue)


================================================
FILE: SSR_FIX_SUMMARY.md
================================================
# SSR対応の修正サマリー

## 変更内容

このプルリクエストでは、Nuxt.jsなどのSSR(Server-Side Rendering)環境での動作をサポートするための修正を行いました。

## 主な変更点

### 1. `src/components/CodeMirror.ts`

#### SSR環境のチェック

- `onMounted`内でブラウザ環境をチェックし、サーバーサイドでは初期化をスキップ
- `typeof window !== 'undefined'` でブラウザ環境を確認

#### `view`の型変更

- `ShallowRef` から `ShallowRef` に変更
- サーバーサイドでは`undefined`となる可能性を考慮

#### すべての`view.value`アクセスを安全に

- `view.value?.` の Optional Chaining を使用
- 各関数で`view.value`の存在をチェック

#### computed プロパティの修正

- `focus`: `view.value?.hasFocus ?? false` で安全にアクセス
- `selection`: `view.value?.state.selection` でOptionalに
- `cursor`: `view.value?.state.selection.main.head ?? 0` でデフォルト値を提供
- `json`: `view.value?.state.toJSON()` でOptionalに

#### ヘルパー関数の修正

すべてのCodeMirror5互換関数を安全に修正:

- `getRange()`, `getLine()`, `lineCount()`, `getCursor()`
- `listSelections()`, `getSelection()`, `getSelections()`
- `somethingSelected()`
- `replaceRange()`, `replaceSelection()`
- `setCursor()`, `setSelection()`, `setSelections()`
- `extendSelectionsBy()`

各関数で`view.value`の存在を確認し、存在しない場合は適切なデフォルト値を返すか、処理をスキップ

### 2. `README.md`

#### SSR使用例セクションの追加

- Nuxt.jsでの使用方法を説明
- Nuxt 3での直接使用方法
- Nuxt 2や問題が発生した場合の``ラッパーの使用方法

## テスト方法

### ローカルでのビルド確認

```bash
pnpm type-check  # 型チェック成功
pnpm build       # ビルド成功
```

### Nuxtでのテスト方法

#### Nuxt 3での使用例

```vue



```

#### Nuxt 2での使用例

```vue



```

## 互換性

- ✅ Vue 2.7以上
- ✅ Vue 3.3以上
- ✅ Nuxt 2
- ✅ Nuxt 3
- ✅ その他のSSRフレームワーク(VitePress、Quasar SSRなど)

## 破壊的変更

なし。既存の使用方法はすべて互換性を維持しています。

## 次のバージョンで推奨される変更

次のメジャーバージョン(2.0.0)で以下を検討:

- `view`、`selection`、`json`などの型を常にOptionalとして扱う
- TypeScript strictモードでのより厳密な型チェック

## 関連Issue

この修正は、NuxtやVitePressなどのSSR環境でコンポーネントが正しく動作しない問題を解決します。

## ライセンス

©2022-2025 by Logue.
Licensed under the MIT License.


================================================
FILE: TESTING.md
================================================
# Testing Guide

このプロジェクトは[Vitest](https://vitest.dev/)を使用してテストを行っています。

## テストの実行

### 基本的なコマンド

```bash
# 全テストを実行
pnpm test:run

# ウォッチモードでテストを実行(開発時に便利)
pnpm test

# UIモードでテストを実行
pnpm test:ui

# カバレッジレポートを生成
pnpm test:coverage
```

## テストの構成

### 1. コンポーネントテスト (`CodeMirror.spec.ts`)

基本的なコンポーネントの機能をテストします:

- **レンダリング**: コンポーネントが正しくレンダリングされるか
- **Props**: 各プロパティが正しく動作するか
- **イベント**: `ready`, `update`, `change`, `destroy`などのイベントが正しく発火するか
- **V-Model**: 双方向バインディングが正しく動作するか
- **スロット**: スロットコンテンツが正しく表示されるか
- **公開メソッド**: `getRange()`, `setCursor()`などのメソッドが正しく動作するか

### 2. SSR互換性テスト (`CodeMirror.ssr.spec.ts`)

サーバーサイドレンダリング環境での動作をテストします:

- **サーバーサイドレンダリング**: SSR環境でエラーなくレンダリングされるか
- **安全なメソッド呼び出し**: `view`が未初期化でもメソッドがエラーを投げないか
- **クライアントサイドハイドレーション**: ブラウザでの初期化が正しく行われるか
- **グレースフルデグラデーション**: 機能が段階的に提供されるか
- **メモリリーク防止**: コンポーネントが正しくクリーンアップされるか

## テストの追加

新しい機能を追加する場合は、対応するテストも追加してください:

```typescript
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import CodeMirror from '@/components/CodeMirror';

describe('New Feature', () => {
  it('should work correctly', async () => {
    const wrapper = mount(CodeMirror, {
      props: {
        modelValue: 'test',
        // 新機能のprops
      },
    });

    // テストロジック
    expect(wrapper.exists()).toBe(true);
  });
});
```

## テスト環境

- **テストランナー**: Vitest
- **DOM環境**: happy-dom(軽量で高速)
- **Vueテストユーティリティ**: @vue/test-utils
- **アサーション**: Vitest標準のexpect API

## カバレッジ

カバレッジレポートは以下を除外しています:

- `node_modules/`
- `src-docs/` (ドキュメントサイト)
- `dist/` (ビルド出力)
- `**/*.d.ts` (型定義ファイル)
- `**/*.config.*` (設定ファイル)
- `src/Meta.ts` (自動生成ファイル)
- `src/helpers/h-demi.ts` (Vue 2/3互換レイヤー)

## ベストプラクティス

### 1. テストは独立させる

各テストは他のテストに依存しないようにしてください。

```typescript
beforeEach(() => {
  // 各テストの前にクリーンアップ
  document.body.innerHTML = '';
});
```

### 2. 非同期処理を待つ

コンポーネントのライフサイクルを待つために`nextTick()`を使用してください。

```typescript
await nextTick();
await nextTick(); // onMountedを待つ
```

### 3. クリーンアップ

テスト後はコンポーネントをアンマウントしてください。

```typescript
wrapper.unmount();
```

### 4. 意味のあるアサーション

テストは何をテストしているかが明確になるようにしてください。

```typescript
// 良い例
expect(wrapper.props('readonly')).toBe(true);

// 避けるべき例
expect(wrapper.props('readonly')).toBeTruthy();
```

## トラブルシューティング

### テストがタイムアウトする

長時間かかるテストにはタイムアウトを設定できます:

```typescript
it('long running test', { timeout: 10000 }, async () => {
  // テストコード
});
```

### DOMが見つからない

`attachTo`オプションを使用してDOMに直接マウントしてください:

```typescript
const wrapper = mount(CodeMirror, {
  props: { modelValue: 'test' },
  attachTo: document.body,
});

// 忘れずにクリーンアップ
wrapper.unmount();
```

### メモリリーク

テスト後に適切にクリーンアップされているか確認してください:

```typescript
afterEach(() => {
  // 必要に応じてグローバルな状態をリセット
});
```

## CI/CD

GitHub ActionsなどのCI環境でテストを実行する場合は、`pnpm test:run`を使用してください。これはウォッチモードなしで一度だけテストを実行します。

```yaml
- name: Run tests
  run: pnpm test:run
```

## 参考リンク

- [Vitest Documentation](https://vitest.dev/)
- [Vue Test Utils](https://test-utils.vuejs.org/)
- [Happy DOM](https://github.com/capricorn86/happy-dom)


================================================
FILE: env.d.ts
================================================
/// 

declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- Vite env extension point; add VITE_APP_* variable types here as needed
interface ImportMetaEnv {
  // see https://vitejs.dev/guide/env-and-mode.html#env-files
  // add .env variables.
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}


================================================
FILE: eslint.config.ts
================================================
import configPrettier from '@vue/eslint-config-prettier';
import {
  defineConfigWithVueTs,
  vueTsConfigs,
} from '@vue/eslint-config-typescript';

import markdown from '@eslint/markdown';
import comments from '@eslint-community/eslint-plugin-eslint-comments/configs';
import pluginVitest from '@vitest/eslint-plugin';
import { globalIgnores } from 'eslint/config';
import pluginImport from 'eslint-plugin-import-x';
import pluginOxlint from 'eslint-plugin-oxlint';
import pluginPlaywright from 'eslint-plugin-playwright';
// @ts-ignore
import pluginSecurity from 'eslint-plugin-security';
import pluginVue from 'eslint-plugin-vue';
import pluginVueA11y from 'eslint-plugin-vuejs-accessibility';

import type { Linter } from 'eslint';

// Lint policy:
// 1) Keep oxlint + prettier as primary formatting/quick-check tools.
// 2) Keep ESLint focused on framework/type/import correctness.
// 3) Scope plugin presets to relevant file types to avoid cross-file crashes.
// 4) Restrict markdown lint to workspace instruction docs under .github.
// 5) Prefer small, explicit overrides over broad global exceptions.
const APP_FILES = ['**/*.{vue,ts,mts,tsx}'];
const VUE_FILES = ['*.vue', '**/*.vue'];
const MARKDOWN_FILES = ['.github/**/*.md'];
const E2E_FILES = ['e2e/**/*.{test,spec}.{js,ts,jsx,tsx}'];
const UNIT_TEST_FILES = ['src/**/__tests__/*'];
const GLOBAL_IGNORES = ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'];

const scopeConfigsToFiles = (configs: Linter.Config[], files: string[]) =>
  configs.map(config => (config.files ? config : { ...config, files }));

const markdownRecommendedConfigs = markdown.configs.recommended.map(config => ({
  ...config,
  files: MARKDOWN_FILES,
}));

const appRules: Linter.Config['rules'] = {
  '@eslint-community/eslint-comments/require-description': 'error',
  'no-unused-vars': 'off',
  // const lines: string[] = []; style
  '@typescript-eslint/array-type': [
    'error',
    {
      default: 'array',
    },
  ],
  // Enable @ts-ignore etc.
  '@typescript-eslint/ban-ts-comment': 'off',
  // Left-hand side style
  '@typescript-eslint/consistent-generic-constructors': [
    'error',
    'type-annotation',
  ],
  // Enable import sort order, see bellow.
  '@typescript-eslint/consistent-type-imports': [
    'off',
    {
      prefer: 'type-imports',
    },
  ],
  // Fix for pinia
  '@typescript-eslint/explicit-function-return-type': 'off',
  // Exclude variables with leading underscores
  '@typescript-eslint/no-unused-vars': [
    'error',
    {
      args: 'all',
      argsIgnorePattern: '^_',
      caughtErrors: 'all',
      caughtErrorsIgnorePattern: '^_',
      destructuredArrayIgnorePattern: '^_',
      varsIgnorePattern: '^_',
      ignoreRestSiblings: true,
    },
  ],
  // Fix for vite import.meta.env
  '@typescript-eslint/strict-boolean-expressions': 'off',
  // Fix for vite env.d.ts.
  '@typescript-eslint/triple-slash-reference': 'off',
  // Fix for Vue setup style
  'import-x/default': 'off',
  // Fix for vite
  'import-x/namespace': 'off',
  'import-x/no-default-export': 'off',
  'import-x/no-named-as-default-member': 'off',
  'import-x/no-named-as-default': 'off',
  // Sort Import Order.
  // see https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md#importorder-enforce-a-convention-in-module-import-order
  'import-x/order': [
    'error',
    {
      groups: [
        'builtin',
        'external',
        'parent',
        'sibling',
        'index',
        'object',
        'type',
      ],
      pathGroups: [
        // Vue Core
        {
          pattern:
            '{vue,vue-router,vuex,@/store,vue-i18n,pinia,vite,vitest,vitest/**,@vitejs/**,@vue/**}',
          group: 'external',
          position: 'before',
        },
        // Internal Codes
        {
          pattern: '{@/**}',
          group: 'internal',
          position: 'before',
        },
      ],
      pathGroupsExcludedImportTypes: ['builtin'],
      alphabetize: {
        order: 'asc',
      },
      'newlines-between': 'always',
    },
  ],
  // Using `../` to navigate back to parent directories is completely prohibited (using `./foo` at the same level is OK).
  // Alias imports like `@/` are excluded because they resolve via the configured alias, not a relative parent path.
  'import-x/no-relative-parent-imports': ['error', { ignore: ['^@/', '^~/'] }],
};

const appSettings = {
  // This will do the trick
  'import-x/parsers': {
    espree: ['.js', '.cjs', '.mjs', '.jsx'],
    '@typescript-eslint/parser': ['.ts', '.tsx'],
    'vue-eslint-parser': ['.vue'],
  },
  'import-x/resolver': {
    // You will also need to install and configure the TypeScript resolver
    // See also https://github.com/import-js/eslint-import-resolver-typescript#configuration
    typescript: true,
    node: true,
    'eslint-import-resolver-custom-alias': {
      alias: {
        '@': './src',
        '~': './node_modules',
      },
      extensions: ['.js', '.ts', '.jsx', '.tsx', '.vue'],
    },
  },
};

// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup

export default defineConfigWithVueTs(
  ...markdownRecommendedConfigs,

  globalIgnores(GLOBAL_IGNORES),

  ...scopeConfigsToFiles(pluginVue.configs['flat/recommended'], VUE_FILES),
  ...scopeConfigsToFiles(pluginVueA11y.configs['flat/recommended'], VUE_FILES),
  vueTsConfigs.recommended,
  comments.recommended,

  {
    ...pluginImport.flatConfigs.recommended,
    files: APP_FILES,
  },
  {
    ...pluginImport.flatConfigs.typescript,
    files: APP_FILES,
  },
  {
    ...pluginSecurity.configs.recommended,
    files: APP_FILES,
  },
  {
    name: 'app/rules',
    files: APP_FILES,
    settings: appSettings,
    rules: appRules,
  },
  {
    name: 'vue/rules',
    files: VUE_FILES,
    rules: {
      // 
    
  

  
    
We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to continue.
================================================ FILE: package.json ================================================ { "$schema": "https://json.schemastore.org/package.json", "name": "vue-codemirror6", "version": "1.5.2", "license": "MIT", "description": "CodeMirror6 Component for vue2 and vue3.", "keywords": [ "vuejs", "vue", "vue-components", "vue-codemirror", "code-editor", "text-editor", "vue2", "vue3", "web-editor", "vue-plugin", "vue-component", "codemirror-editor", "vue-resource", "codemirror6" ], "type": "module", "author": { "name": "Logue", "email": "logue@hotmail.co.jp", "url": "https://logue.dev/" }, "homepage": "https://github.com/logue/vue-codemirror6", "repository": { "type": "git", "url": "git+ssh://git@github.com/logue/vue-codemirror6.git" }, "bugs": { "url": "https://github.com/logue/vue-codemirror6/issues" }, "main": "dist/index.cjs.js", "module": "dist/index.es.js", "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.es.js", "types": "./dist/index.d.ts", "require": "./dist/index.cjs.js", "default": "./dist/index.es.js" }, "./umd": { "default": "./dist/index.umd.js" }, "./iife": { "default": "./dist/index.iife.js" } }, "files": [ "CHANGELOG.md", "/dist" ], "sideEffects": false, "engines": { "node": "^20.19.0 || >=22.12.0", "pnpm": ">=10.3.0" }, "packageManager": "pnpm@10.33.2", "scripts": { "dev": "vite", "clean": "rimraf node_modules/.vite", "build": "run-p type-check \"build-only {@}\" --", "build:analyze": "vite build --mode=analyze", "build:clean": "rimraf dist docs", "build:docs": "vite build --mode=docs", "lint": "run-s lint:*", "lint:oxlint": "oxlint . --fix", "lint:eslint": "eslint . --fix --cache --cache-location ./node_modules/.vite/eslint-cache", "lint:prettier": "prettier \"./**/*.{js,ts,json,css,sass,scss,htm,html,vue,md}\" -w -u", "preview": "vite preview --mode=docs", "build-only": "vite build && node scripts/sync-dts-entry.mjs", "type-check": "vue-tsc --declaration --emitDeclarationOnly", "test": "vitest", "test:ui": "vitest --ui", "test:run": "vitest run", "test:coverage": "vitest run --coverage", "prepare": "husky", "version": "auto-changelog -p && git add CHANGELOG.md" }, "dependencies": { "vue-demi": "latest" }, "peerDependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "codemirror": "^6.0.0", "style-mod": "^4.0.0", "vue": "^2.7.14 || ^3.3.4" }, "devDependencies": { "@codemirror/autocomplete": "^6.20.1", "@codemirror/commands": "^6.10.3", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-vue": "^0.1.3", "@codemirror/language": "^6.12.3", "@codemirror/lint": "^6.9.5", "@codemirror/search": "^6.7.0", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.41.1", "@eslint-community/eslint-plugin-eslint-comments": "^4.7.1", "@eslint/markdown": "^8.0.1", "@tsconfig/node-lts": "^24.0.0", "@types/node": "^25.6.0", "@vitejs/plugin-vue": "^6.0.6", "@vitest/eslint-plugin": "^1.6.16", "@vitest/ui": "^4.1.5", "@vue/compiler-sfc": "^3.5.33", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", "@vue/test-utils": "^2.4.10", "@vue/tsconfig": "^0.9.1", "@vueuse/core": "^14.3.0", "bootstrap": "^5.3.8", "codemirror": "^6.0.2", "eslint": "^10.2.1", "eslint-import-resolver-custom-alias": "^1.3.2", "eslint-import-resolver-typescript": "^4.4.4", "eslint-linter-browserify": "^10.2.1", "eslint-plugin-import-x": "^4.16.2", "eslint-plugin-oxlint": "^1.62.0", "eslint-plugin-playwright": "^2.10.2", "eslint-plugin-security": "^4.0.0", "eslint-plugin-vue": "^10.9.0", "eslint-plugin-vuejs-accessibility": "^2.5.0", "happy-dom": "^20.9.0", "husky": "^9.1.7", "jiti": "^2.6.1", "lint-staged": "^16.4.0", "npm-run-all2": "^8.0.4", "oxlint": "^1.62.0", "prettier": "^3.8.3", "rimraf": "^6.1.3", "rollup-plugin-visualizer": "^7.0.1", "sass-embedded": "^1.99.0", "style-mod": "^4.1.3", "supports-color": "^10.2.2", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "vite": "^8.0.10", "vite-plugin-banner": "^0.8.1", "vite-plugin-checker": "^0.13.0", "vite-plugin-dts": "^5.0.0", "vitest": "^4.1.5", "vue": "^3.5.33", "vue-eslint-parser": "^10.4.0", "vue-markdown-wasm": "^1.0.1", "vue-tsc": "^3.2.7" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{js,ts,json,htm,html,vue}": "eslint --fix --cache --cache-location ./node_modules/.vite/vite-plugin-eslint", "*": "prettier -w -u" }, "resolutions": { "lodash": ">=4.18.1" } } ================================================ FILE: pnpm-workspace.yaml ================================================ onlyBuiltDependencies: - '@parcel/watcher' - esbuild - unrs-resolver - vue-demi ================================================ FILE: scripts/sync-dts-entry.mjs ================================================ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; const src = resolve('dist/src/index.d.ts'); const dest = resolve('dist/index.d.ts'); if (!existsSync(src)) { console.error(`[sync-dts-entry] Source declaration not found: ${src}`); process.exit(1); } const content = readFileSync(src, 'utf8').replaceAll( "from '../Meta'", "from './src/Meta'" ); writeFileSync(dest, content, 'utf8'); console.log(`[sync-dts-entry] Synced declaration entry: ${src} -> ${dest}`); ================================================ FILE: src/__tests__/CodeMirror.spec.ts ================================================ import { mount } from '@vue/test-utils'; import { describe, it, expect, beforeEach, vi } from 'vitest'; import { nextTick, ref } from 'vue'; import { javascript } from '@codemirror/lang-javascript'; import { EditorView } from '@codemirror/view'; import CodeMirror, { type CodeMirrorExposed } from '../index'; describe('CodeMirror Component', () => { beforeEach(() => { // Clear any previous DOM document.body.innerHTML = ''; }); describe('Basic Rendering', () => { it('should render the component', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test code', }, }); expect(wrapper.exists()).toBe(true); expect(wrapper.classes()).toContain('vue-codemirror'); }); it('should use custom tag when specified', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', tag: 'section', }, }); expect(wrapper.element.tagName.toLowerCase()).toBe('section'); }); it('should apply custom class', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, attrs: { class: 'custom-class', }, }); expect(wrapper.classes()).toContain('vue-codemirror'); expect(wrapper.classes()).toContain('custom-class'); }); }); describe('Props', () => { it('should accept modelValue prop', () => { const testValue = 'const x = 42;'; const wrapper = mount(CodeMirror, { props: { modelValue: testValue, }, }); expect(wrapper.props('modelValue')).toBe(testValue); }); it('should accept basic setup prop', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', basic: true, }, }); expect(wrapper.props('basic')).toBe(true); }); it('should accept minimal setup prop', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', minimal: true, }, }); expect(wrapper.props('minimal')).toBe(true); }); it('should accept dark mode prop', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', dark: true, }, }); expect(wrapper.props('dark')).toBe(true); }); it('should accept readonly prop', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', readonly: true, }, }); expect(wrapper.props('readonly')).toBe(true); }); it('should accept disabled prop', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', disabled: true, }, }); expect(wrapper.props('disabled')).toBe(true); }); it('should accept wrap prop', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', wrap: true, }, }); expect(wrapper.props('wrap')).toBe(true); }); it('should accept tab prop', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', tab: true, }, }); expect(wrapper.props('tab')).toBe(true); }); it('should accept placeholder prop', () => { const placeholderText = 'Enter code here...'; const wrapper = mount(CodeMirror, { props: { modelValue: '', placeholder: placeholderText, }, }); expect(wrapper.props('placeholder')).toBe(placeholderText); }); it('should accept lang prop', { timeout: 10000 }, () => { const lang = javascript(); const wrapper = mount(CodeMirror, { props: { modelValue: 'const x = 1;', lang, }, }); // Just verify the prop is set, don't compare object identity expect(wrapper.props('lang')).toBeDefined(); expect(wrapper.props('lang')).toHaveProperty('language'); }); it('should accept preserveScrollPosition prop', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', preserveScrollPosition: true, }, }); expect(wrapper.props('preserveScrollPosition')).toBe(true); }); }); describe('Events', () => { it('should emit ready event after mount', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); await nextTick(); await nextTick(); // Wait for onMounted const readyEvents = wrapper.emitted('ready'); expect(readyEvents).toBeTruthy(); expect(readyEvents!.length).toBeGreaterThan(0); const firstEvent = readyEvents![0]?.[0]; expect(firstEvent).toHaveProperty('view'); expect(firstEvent).toHaveProperty('state'); expect(firstEvent).toHaveProperty('container'); }); it('should emit update:modelValue on text change', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'initial', }, }); await nextTick(); await nextTick(); // Simulate text change through exposed view const vm = wrapper.vm as unknown as CodeMirrorExposed; expect(vm.view).toBeDefined(); vm.view!.dispatch({ changes: { from: 0, to: vm.view!.state.doc.length, insert: 'updated' }, }); await nextTick(); const updateEvents = wrapper.emitted('update:modelValue'); expect(updateEvents).toBeTruthy(); }); it('should emit destroy event on unmount', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); await nextTick(); await nextTick(); wrapper.unmount(); const destroyEvents = wrapper.emitted('destroy'); expect(destroyEvents).toBeTruthy(); }); }); describe('SSR Compatibility', () => { it('should handle missing window object gracefully', () => { // This test ensures the component doesn't crash in SSR environment // In happy-dom, window exists, but we test the defensive coding const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); expect(wrapper.exists()).toBe(true); }); it('should not initialize EditorView before mount', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, attachTo: document.body, }); const vm = wrapper.vm as unknown as CodeMirrorExposed; // Before nextTick, view might not be fully initialized expect(vm).toBeDefined(); wrapper.unmount(); }); it('should safely handle view operations when view is undefined', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); const vm = wrapper.vm as unknown as CodeMirrorExposed; // These should not throw even if view is undefined expect(() => vm.getRange()).not.toThrow(); expect(() => vm.getLine(0)).not.toThrow(); expect(() => vm.lineCount()).not.toThrow(); expect(() => vm.getCursor()).not.toThrow(); expect(() => vm.listSelections()).not.toThrow(); expect(() => vm.getSelection()).not.toThrow(); expect(() => vm.getSelections()).not.toThrow(); expect(() => vm.somethingSelected()).not.toThrow(); wrapper.unmount(); }); }); describe('Exposed Methods', () => { it('should expose editor ref', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; expect(vm.editor).toBeDefined(); }); it('should expose view', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); await nextTick(); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; expect(vm.view).toBeDefined(); expect(vm.view).toBeInstanceOf(EditorView); }); it('should expose cursor getter/setter', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test code', }, }); await nextTick(); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; const initialCursor = vm.cursor; expect(typeof initialCursor).toBe('number'); }); it('should expose length property', async () => { const testValue = 'hello world'; const wrapper = mount(CodeMirror, { props: { modelValue: testValue, }, }); await nextTick(); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; // length is updated in the update listener, so it might be 0 initially expect(typeof vm.length).toBe('number'); }); it('should expose getRange method', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'hello world', }, }); await nextTick(); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; expect(vm.view).toBeDefined(); const range = vm.getRange(0, 5); expect(range).toBe('hello'); }); it('should expose lineCount method', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'line1\nline2\nline3', }, }); await nextTick(); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; expect(vm.view).toBeDefined(); const count = vm.lineCount(); expect(count).toBeGreaterThan(0); }); it('should expose getLine method', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'first line\nsecond line', }, }); await nextTick(); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; expect(vm.view).toBeDefined(); const line = vm.getLine(0); expect(line).toBe('first line'); }); it('should expose lint method', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); await nextTick(); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; expect(() => vm.lint()).not.toThrow(); }); it('should expose forceReconfigure method', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); await nextTick(); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; expect(() => vm.forceReconfigure()).not.toThrow(); }); }); describe('V-Model Binding', () => { it('should update when modelValue prop changes', async () => { const modelValue = ref('initial'); const wrapper = mount(CodeMirror, { props: { modelValue: modelValue.value, 'onUpdate:modelValue': ( value?: string | import('@codemirror/state').Text ) => { modelValue.value = typeof value === 'string' ? value : (value?.toString() ?? ''); }, }, }); await nextTick(); await nextTick(); // Update the prop modelValue.value = 'updated'; await wrapper.setProps({ modelValue: modelValue.value }); await nextTick(); expect(wrapper.props('modelValue')).toBe('updated'); }); it('should preserve scroll position on external updates when enabled', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'initial', preserveScrollPosition: true, }, }); await nextTick(); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; expect(vm.view).toBeDefined(); const scrollSnapshotSpy = vi.spyOn( vm.view as EditorView, 'scrollSnapshot' ); await wrapper.setProps({ modelValue: 'initial\nupdated' }); await nextTick(); expect(scrollSnapshotSpy).toHaveBeenCalledTimes(1); }); it('should not preserve scroll position on external updates by default', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'initial', }, }); await nextTick(); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; expect(vm.view).toBeDefined(); const scrollSnapshotSpy = vi.spyOn( vm.view as EditorView, 'scrollSnapshot' ); await wrapper.setProps({ modelValue: 'initial\nupdated' }); await nextTick(); expect(scrollSnapshotSpy).not.toHaveBeenCalled(); }); it('should emit update:modelValue when content changes', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'initial', }, }); await nextTick(); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; expect(vm.view).toBeDefined(); vm.view!.dispatch({ changes: { from: 0, to: vm.view!.state.doc.length, insert: 'changed' }, }); await nextTick(); const updateEvents = wrapper.emitted('update:modelValue'); expect(updateEvents).toBeTruthy(); expect(updateEvents!.at(-1)?.[0]).toBe('changed'); }); }); describe('Slots', () => { it('should render slot content', () => { const wrapper = mount(CodeMirror, { props: { modelValue: '', }, slots: { default: '
Slot Content
', }, }); expect(wrapper.html()).toContain('Slot Content'); }); it('should hide slot content with aria-hidden', () => { const wrapper = mount(CodeMirror, { props: { modelValue: '', }, slots: { default: '
Hidden Content
', }, }); const aside = wrapper.find('aside'); expect(aside.exists()).toBe(true); expect(aside.attributes('aria-hidden')).toBe('true'); }); }); describe('Edge Cases', () => { it('should handle empty modelValue', () => { const wrapper = mount(CodeMirror, { props: { modelValue: '', }, }); expect(wrapper.props('modelValue')).toBe(''); }); it('should handle very long content', async () => { const longContent = 'x'.repeat(10000); const wrapper = mount(CodeMirror, { props: { modelValue: longContent, }, }); await nextTick(); await nextTick(); const vm = wrapper.vm as unknown as CodeMirrorExposed; // length is a reactive value that gets updated expect(typeof vm.length).toBe('number'); expect(vm.length).toBeGreaterThanOrEqual(0); }); it('should handle special characters', () => { const specialContent = '日本語\n한글\n中文\n😀🎉'; const wrapper = mount(CodeMirror, { props: { modelValue: specialContent, }, }); expect(wrapper.props('modelValue')).toBe(specialContent); }); it('should handle line separators', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'line1\nline2', lineSeparator: '\n', }, }); expect(wrapper.props('lineSeparator')).toBe('\n'); }); }); }); ================================================ FILE: src/__tests__/CodeMirror.ssr.spec.ts ================================================ import { mount } from '@vue/test-utils'; import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { nextTick } from 'vue'; import CodeMirror, { type CodeMirrorExposed } from '../index'; describe('CodeMirror SSR Compatibility', () => { let originalWindow: Window & typeof globalThis; beforeEach(() => { originalWindow = globalThis.window; }); afterEach(() => { // Restore window if (!(globalThis as Record).window && originalWindow) { (globalThis as Record).window = originalWindow; } }); describe('Server-Side Rendering', () => { it('should render without errors in SSR environment', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test code for SSR', }, }); expect(wrapper.exists()).toBe(true); expect(wrapper.classes()).toContain('vue-codemirror'); }); it('should not initialize EditorView on server', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'server side code', }, }); const vm = wrapper.vm as unknown as CodeMirrorExposed; // In SSR, view should remain undefined until client-side hydration // Since we're testing in happy-dom (which has window), we test the defensive approach expect(vm).toBeDefined(); }); it('should handle props correctly in SSR', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'ssr test', basic: true, dark: true, readonly: true, placeholder: 'Enter code', }, }); expect(wrapper.props('modelValue')).toBe('ssr test'); expect(wrapper.props('basic')).toBe(true); expect(wrapper.props('dark')).toBe(true); expect(wrapper.props('readonly')).toBe(true); expect(wrapper.props('placeholder')).toBe('Enter code'); }); it('should safely render with all prop combinations', () => { const props = { modelValue: 'test', basic: true, minimal: false, dark: true, wrap: true, tab: true, readonly: false, disabled: false, placeholder: 'Type here...', allowMultipleSelections: true, tabSize: 4, lineSeparator: '\n', }; const wrapper = mount(CodeMirror, { props }); expect(wrapper.exists()).toBe(true); for (const [key, value] of Object.entries(props)) { expect(wrapper.props(key as keyof typeof props)).toBe(value); } }); }); describe('Safe Method Calls in SSR', () => { it('should return safe defaults when view is undefined', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); const vm = wrapper.vm as unknown as CodeMirrorExposed; // Test all exposed methods return safe values expect(vm.getCursor()).toBe(0); expect(vm.lineCount()).toBeGreaterThanOrEqual(0); // May be 1 if doc exists // listSelections may return a default selection if view is initialized expect(Array.isArray(vm.listSelections())).toBe(true); expect(typeof vm.getSelection()).toBe('string'); expect(Array.isArray(vm.getSelections())).toBe(true); expect(vm.somethingSelected()).toBe(false); // getRange and getLine return string or undefined depending on whether view is initialized expect(['string', 'undefined']).toContain(typeof vm.getRange()); expect(['string', 'undefined']).toContain(typeof vm.getLine(0)); }); it('should not throw when calling methods before view initialization', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); const vm = wrapper.vm as unknown as CodeMirrorExposed; // None of these should throw expect(() => vm.lint()).not.toThrow(); expect(() => vm.forceReconfigure()).not.toThrow(); expect(() => vm.setCursor(0)).not.toThrow(); // setSelection and setSelections with invalid args might throw, which is expected behavior // These are only tested to verify they don't crash when view exists expect(() => vm.replaceRange('new', 0, 3)).not.toThrow(); expect(() => vm.replaceSelection('new')).not.toThrow(); // extendSelectionsBy should be safe expect(() => vm.extendSelectionsBy(noopExtendSelectionsBy)).not.toThrow(); }); // Move to outer scope function noopExtendSelectionsBy() {} it('should handle computed properties safely', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); const vm = wrapper.vm as unknown as CodeMirrorExposed; // These should return safe defaults expect(typeof vm.focus).toBe('boolean'); expect(typeof vm.cursor).toBe('number'); expect(typeof vm.length).toBe('number'); }); }); describe('Client-Side Hydration', () => { it('should initialize view after mount in browser', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'hydration test', }, attachTo: document.body, }); await nextTick(); await nextTick(); // Wait for onMounted to complete const vm = wrapper.vm as unknown as CodeMirrorExposed; // After mounting in browser environment, view should be initialized expect(vm.view).toBeDefined(); wrapper.unmount(); }); it('should emit ready event after client-side initialization', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'ready test', }, attachTo: document.body, }); await nextTick(); await nextTick(); const readyEvents = wrapper.emitted('ready'); expect(readyEvents).toBeTruthy(); wrapper.unmount(); }); it('should handle modelValue updates after hydration', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'initial', }, attachTo: document.body, }); await nextTick(); await nextTick(); await wrapper.setProps({ modelValue: 'updated' }); await nextTick(); expect(wrapper.props('modelValue')).toBe('updated'); wrapper.unmount(); }); }); describe('Graceful Degradation', () => { it('should render placeholder in SSR', () => { const wrapper = mount(CodeMirror, { props: { modelValue: '', placeholder: 'Enter your code here', }, }); expect(wrapper.props('placeholder')).toBe('Enter your code here'); }); it('should preserve readonly state in SSR', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'readonly content', readonly: true, }, }); expect(wrapper.props('readonly')).toBe(true); }); it('should preserve disabled state in SSR', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'disabled content', disabled: true, }, }); expect(wrapper.props('disabled')).toBe(true); }); it('should handle slots in SSR', () => { const wrapper = mount(CodeMirror, { props: { modelValue: '', }, slots: { default: '
SSR Slot Content
', }, }); expect(wrapper.html()).toContain('SSR Slot Content'); }); }); describe('Edge Cases in SSR', () => { it('should handle missing editor ref', () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); const vm = wrapper.vm as unknown as CodeMirrorExposed; // Should not throw expect(vm.editor).toBeDefined(); }); it('should handle rapid prop changes before initialization', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'v1', }, }); // Rapidly change props before view is initialized await wrapper.setProps({ modelValue: 'v2' }); await wrapper.setProps({ modelValue: 'v3' }); await wrapper.setProps({ modelValue: 'v4' }); expect(wrapper.props('modelValue')).toBe('v4'); }); it('should handle unmount before initialization', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'test', }, }); // Unmount immediately without waiting for initialization expect(() => wrapper.unmount()).not.toThrow(); }); it('should handle multiple instances', () => { const wrapper1 = mount(CodeMirror, { props: { modelValue: 'instance 1' }, }); const wrapper2 = mount(CodeMirror, { props: { modelValue: 'instance 2' }, }); const wrapper3 = mount(CodeMirror, { props: { modelValue: 'instance 3' }, }); expect(wrapper1.props('modelValue')).toBe('instance 1'); expect(wrapper2.props('modelValue')).toBe('instance 2'); expect(wrapper3.props('modelValue')).toBe('instance 3'); wrapper1.unmount(); wrapper2.unmount(); wrapper3.unmount(); }); }); describe('Memory Leaks Prevention', () => { it('should clean up properly on unmount', async () => { const wrapper = mount(CodeMirror, { props: { modelValue: 'cleanup test', }, attachTo: document.body, }); await nextTick(); await nextTick(); wrapper.unmount(); // After unmount, destroy event should be emitted const destroyEvents = wrapper.emitted('destroy'); expect(destroyEvents).toBeTruthy(); }); it('should handle multiple mount/unmount cycles', async () => { for (let i = 0; i < 5; i++) { const wrapper = mount(CodeMirror, { props: { modelValue: `cycle ${i}`, }, attachTo: document.body, }); await nextTick(); await nextTick(); expect(wrapper.exists()).toBe(true); wrapper.unmount(); } }); }); }); ================================================ FILE: src/helpers/h-demi.ts ================================================ /** * h-demi - h function for Vue 2 and 3 * * @see {@link https://github.com/vueuse/vue-demi/issues/65} */ import { h as hDemi, isVue2, type Component, type Slots, type VNode, type VNodeProps, } from 'vue-demi'; interface Options extends VNodeProps { class?: string; domProps?: VNodeProps; on?: Record void>; props?: VNodeProps; style?: string; 'aria-hidden'?: string; } const adaptOnsV3 = ( ons: Record void> ): Record void> => { if (!ons) return {}; return Object.entries(ons).reduce((ret, [key, handler]) => { key = key.charAt(0).toUpperCase() + key.slice(1); key = `on${key}`; return { ...ret, [key]: handler }; }, {}); }; /** * hDemi function. */ export default function h( type: string | Component, options: Options = {}, children?: VNode | VNode[] | null ): VNode { if (isVue2) { // Makeshift support :( // Since Vue2.7 includes the Composition API, the functions in vue-demi are not used. return hDemi(type, options, children); } const { props, domProps, on, ...extraOptions } = options; const ons = on ? adaptOnsV3(on) : {}; return hDemi( type, { ...extraOptions, ...props, ...domProps, ...ons }, children ); } export const slot = ( defaultSlots: (() => VNode[]) | VNode[] | undefined ): VNode[] => typeof defaultSlots === 'function' ? defaultSlots() : (defaultSlots ?? []); ================================================ FILE: src/index.ts ================================================ import { indentWithTab } from '@codemirror/commands'; import { indentUnit, type LanguageSupport } from '@codemirror/language'; import { diagnosticCount as linterDiagnosticCount, forceLinting, linter, lintGutter, type Diagnostic, type LintSource, } from '@codemirror/lint'; import { Compartment, EditorSelection, EditorState, StateEffect, type Transaction, type Extension, type SelectionRange, type StateField, type Text, } from '@codemirror/state'; import { EditorView, keymap, placeholder, type KeyBinding, type ViewUpdate, } from '@codemirror/view'; import { basicSetup, minimalSetup } from 'codemirror'; import { computed, defineComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, watch, type App, type ComputedRef, type PropType, type Ref, type ShallowRef, type WritableComputedRef, } from 'vue-demi'; import type { StyleSpec } from 'style-mod'; import Meta from '@/Meta'; import h, { slot } from '@/helpers/h-demi'; /** CodeMirror Component */ const CodeMirror = defineComponent({ /** Component Name */ name: 'CodeMirror', /** Model Definition */ model: { prop: 'modelValue', event: 'update:modelValue', }, /** Props Definition */ props: { /** Model value */ modelValue: { type: String as PropType, default: '', }, /** * Theme * * @see {@link https://codemirror.net/docs/ref/#view.EditorView^theme} */ theme: { type: Object as PropType>, default: () => { return {}; }, }, /** Dark Mode */ dark: { type: Boolean, default: false, }, /** * Use Basic Setup * * This will enable the basic setup for the editor. * * @see {@link https://codemirror.net/docs/ref/#codemirror.basicSetup} */ basic: { type: Boolean, default: false, }, /** * Use Minimal Setup (The basic setting has priority.) * * @see {@link https://codemirror.net/docs/ref/#codemirror.minimalSetup} */ minimal: { type: Boolean, default: false, }, /** * Placeholder * * @see {@link https://codemirror.net/docs/ref/#view.placeholder} */ placeholder: { type: String as PropType, default: undefined, }, /** * Line wrapping * * An extension that enables line wrapping in the editor (by setting CSS white-space to pre-wrap in the content). * * @see {@link https://codemirror.net/docs/ref/#view.EditorView%5ElineWrapping} */ wrap: { type: Boolean, default: false, }, /** * Allow tab key indent. * * This will enable the tab key to indent the current line or selection. * * @see {@link https://codemirror.net/examples/tab/} */ tab: { type: Boolean, default: false, }, /** * Tab character * * This is the unit of indentation used when the editor is configured to indent with tabs. * It is also used to determine the size of the tab character when the editor is configured to use tabs for indentation.. * * @see {@link https://codemirror.net/docs/ref/#state.EditorState^indentUnit} */ indentUnit: { type: String, default: undefined, }, /** * Allow Multiple Selection. * * This allows the editor to have multiple selections at the same time. * This is useful for editing multiple parts of the document at once. * If this is set to true, the editor will allow multiple selections. * If this is set to false, the editor will only allow a single selection. * * @see {@link https://codemirror.net/docs/ref/#state.EditorState^allowMultipleSelections} */ allowMultipleSelections: { type: Boolean, default: false, }, /** * Tab size * * This is the number of spaces that a tab character represents in the editor. * It is used to determine the size of the tab character when the editor is configured to use tabs for indentation. * If this is set to a number, the editor will use that number of spaces for each tab character. * * @see {@link https://codemirror.net/docs/ref/#state.EditorState^tabSize} */ tabSize: { type: Number, default: undefined, }, /** * Set line break (separetor) char. * * This is the character that is used to separate lines in the editor. * It is used to determine the line break character when the editor is configured to use a specific line break character. * * @see {@link https://codemirror.net/docs/ref/#state.EditorState^lineSeparator} */ lineSeparator: { type: String, default: undefined, }, /** * Readonly * * This is a CodeMirror Facet that allows you to set the editor to read-only mode. * When this is set to true, the editor will not allow any changes to be made to the document. * This is useful for displaying code that should not be edited, such as documentation or examples. * If this is set to false, the editor will allow changes to be made to the document. * * @see {@link https://codemirror.net/docs/ref/#state.EditorState^readOnly} */ readonly: { type: Boolean, default: false, }, /** * Disable input. * * This is the reversed value of the CodeMirror editable. * Similar to `readonly`, but setting this value to true disables dragging. * * @see {@link https://codemirror.net/docs/ref/#view.EditorView^editable} */ disabled: { type: Boolean, default: false, }, /** * Additional Extension * * You can use this to add any additional extensions that you want to use in the editor. * * @see {@link https://codemirror.net/docs/ref/#state.Extension} */ extensions: { type: Array as PropType, default: () => { return []; }, }, /** * Language Phreses * * This is a CodeMirror Facet that allows you to define custom phrases for the editor. * It can be used to override default phrases or add new ones. * This is useful for translating the editor to different languages or for customizing the editor's UI. * * @see {@link https://codemirror.net/examples/translate/} */ phrases: { type: Object as PropType>, default: undefined, }, /** * CodeMirror Language * * This is a CodeMirror Facet that allows you to define the language of the editor. * It can be used to enable syntax highlighting and other language-specific features. * It is useful for displaying code in a specific language, such as JavaScript, Python, or HTML. * * @see {@link https://codemirror.net/docs/ref/#language} */ lang: { type: Object as PropType, default: undefined, }, /** * CodeMirror Linter * * This is a CodeMirror Facet that allows you to define a linter for the editor. * It can be used to check the code for errors and warnings, and to provide feedback to the user. * It is useful for displaying code in a specific language, such as JavaScript, Python, or HTML. * This is useful for providing feedback to the user about the code they are writing. * * @see {@link https://codemirror.net/docs/ref/#lint.linter} */ linter: { type: Function as PropType, default: undefined, }, /** * Linter Config * * This is a CodeMirror Facet that allows you to define the configuration for the linter. * It can be used to specify options for the linter, such as the severity of errors and warnings, and to customize the behavior of the linter. * This is useful for providing feedback to the user about the code they are writing. * * @see {@link https://codemirror.net/docs/ref/#lint.linter^config} */ linterConfig: { type: Object, default: () => { return {}; }, }, /** * Forces any linters configured to run when the editor is idle to run right away. * * This is useful for running linters on the initial load of the editor, or when the user has made changes to the code and wants to see the results immediately. * * @see {@link https://codemirror.net/docs/ref/#lint.forceLinting} */ forceLinting: { type: Boolean, default: false, }, /** * Show Linter Gutter * * An area to 🔴 the lines with errors will be displayed. * This feature is not enabled if `linter` is not specified. * * @see {@link https://codemirror.net/docs/ref/#lint.lintGutter} */ gutter: { type: Boolean, default: false, }, /** * Gutter Config * * This is a CodeMirror Facet that allows you to define the configuration for the gutter. * It can be used to specify options for the gutter, such as the size of the gutter, the position of the gutter, and to customize the behavior of the gutter. * This is useful for providing feedback to the user about the code they are writing. * * @see {@link https://codemirror.net/docs/ref/#lint.lintGutter^config} */ gutterConfig: { type: Object, default: undefined, }, /** * Using tag */ tag: { type: String, default: 'div', }, /** * Allows an external update to scroll the form. * * This is useful for scrolling the editor to a specific position when the user has made changes to the code and wants to see the results immediately. * If this is set to true, the editor will scroll to the position specified in the transaction. * If this is set to false, the editor will not scroll to the position specified in the transaction. * * @see {@link https://codemirror.net/docs/ref/#state.TransactionSpec.scrollIntoView} */ scrollIntoView: { type: Boolean, default: true, }, /** * Preserve the current scroll position on external updates. * * This keeps the editor's own scroll container from jumping when * `modelValue` is updated from outside the component. * * @see {@link https://codemirror.net/docs/ref/#view.EditorView.scrollSnapshot} */ preserveScrollPosition: { type: Boolean, default: false, }, /** * Key map * This is a CodeMirror Facet that allows you to define custom key bindings. * It can be used to override default key bindings or add new ones. * * @see {@link https://codemirror.net/docs/ref/#view.keymap} */ keymap: { type: Array as PropType, default: () => [], }, }, /** Emits */ emits: { /** Model Update */ 'update:modelValue': (_value: string | Text = '') => true, /** CodeMirror ViewUpdate */ update: (_value: ViewUpdate) => true, /** CodeMirror onReady */ ready: (_value: { view: EditorView; state: EditorState; container: HTMLElement; }) => true, /** CodeMirror onFocus */ focus: (_value: boolean) => true, /** State Changed */ change: (_value: EditorState) => true, /** CodeMirror onDestroy */ destroy: () => true, }, /** * Setup * * @param props - Props * @param context - Context */ setup(props, context) { /** Editor DOM */ const editor: Ref = ref(); /** Internal value */ const doc: Ref = ref(props.modelValue); /** * CodeMirror Editor View * * @see {@link https://codemirror.net/docs/ref/#view.EditorView} */ const view: ShallowRef = shallowRef(undefined); /** * Focus * * @see {@link https://codemirror.net/docs/ref/#view.EditorView.hasFocus} */ const focus: WritableComputedRef = computed({ get: () => view.value?.hasFocus ?? false, set: f => { if (f && view.value) { view.value.focus(); } }, }); /** * Editor Selection * * @see {@link https://codemirror.net/docs/ref/#state.EditorSelection} */ const selection: WritableComputedRef = computed({ get: () => view.value?.state.selection, set: selection => { if (view.value && selection) { view.value.dispatch({ selection }); } }, }); /** Cursor Position */ const cursor: WritableComputedRef = computed({ get: () => view.value?.state.selection.main.head ?? 0, set: anchor => { if (view.value) { view.value.dispatch({ selection: { anchor } }); } }, }); /** JSON */ const json: WritableComputedRef< Record> | undefined > = computed({ get: () => view.value?.state.toJSON(), set: j => { if (view.value && j) { view.value.setState(EditorState.fromJSON(j)); } }, }); /** Text length */ const length: Ref = ref(0); /** * Returns the number of active lint diagnostics in the given state. * * @see {@link https://codemirror.net/docs/ref/#lint.diagnosticCount} */ const diagnosticCount: Ref = ref(0); /** Get CodeMirror Extension */ const extensions: ComputedRef = computed(() => { // Synamic Reconfiguration // @see https://codemirror.net/examples/config/ const language = new Compartment(); const tabSize = new Compartment(); if (props.basic && props.minimal) { throw new Error( '[Vue CodeMirror] Both basic and minimal cannot be specified.' ); } /** Keymap */ let keymaps: KeyBinding[] = []; if (props.keymap && props.keymap.length > 0) { // If keymap is specified, use it. keymaps = props.keymap; } if (props.tab) { // If tab is enabled, add indentWithTab to keymap. keymaps.push(indentWithTab); } // TODO: Ignore previous prop was not changed. return [ // Toggle basic setup props.basic && !props.minimal ? basicSetup : undefined, // Toggle minimal setup props.minimal && !props.basic ? minimalSetup : undefined, // ViewUpdate event listener EditorView.updateListener.of((update: ViewUpdate): void => { if (!view.value) { return; } // Emit focus status context.emit('focus', view.value.hasFocus); // Update count length.value = view.value.state.doc?.length; if (update.changes.empty || !update.docChanged) { // Suppress event firing if no change return; } if (props.linter) { // Linter process if (props.forceLinting) { // If forceLinting enabled, first liting. forceLinting(view.value); } // Count diagnostics. diagnosticCount.value = ( props.linter(view.value) as readonly Diagnostic[] ).length; } context.emit('update', update); }), // Toggle light/dark mode. EditorView.theme(props.theme, { dark: props.dark }), // Toggle line wrapping props.wrap ? EditorView.lineWrapping : undefined, // Tab character props.indentUnit ? indentUnit.of(props.indentUnit) : undefined, // Allow Multiple Selections EditorState.allowMultipleSelections.of(props.allowMultipleSelections), // Indent tab size props.tabSize ? tabSize.of(EditorState.tabSize.of(props.tabSize)) : undefined, // locale settings props.phrases ? EditorState.phrases.of(props.phrases) : undefined, // Readonly option EditorState.readOnly.of(props.readonly), // Editable option EditorView.editable.of(!props.disabled), // Set Line break char props.lineSeparator ? EditorState.lineSeparator.of(props.lineSeparator) : undefined, // Lang props.lang ? language.of(props.lang) : undefined, // Append Linter settings props.linter ? linter(props.linter, props.linterConfig) : undefined, // Show 🔴 to error line when linter enabled. props.linter && props.gutter ? lintGutter(props.gutterConfig) : undefined, // Placeholder props.placeholder ? placeholder(props.placeholder) : undefined, // Keymap and Indent with Tab keymaps.length > 0 ? keymap.of(keymaps) : undefined, // Append Extensions ...props.extensions, ].filter((extension): extension is Extension => !!extension); // Filter undefined }); // Extension (mostly props) Changed watch( extensions, exts => view.value?.dispatch({ effects: StateEffect.reconfigure.of(exts) }), { immediate: true } ); // for parent-to-child binding. watch( () => props.modelValue, async value => { if (!view.value) { return; } if ( view.value.composing || // IME fix view.value.state.doc.toJSON().join(props.lineSeparator ?? '\n') === value // don't need to update ) { // Do not commit CodeMirror's store. return; } // Range Fix ? // https://github.com/logue/vue-codemirror6/issues/27 const isSelectionOutOfRange = !view.value.state.selection.ranges.every( range => range.anchor < value.length && range.head < value.length ); /** Scroll Fix */ const changes = { from: 0, to: view.value.state.doc.length, insert: value, }; // If preserveScrollPosition is enabled, create a scroll snapshot before updating the document. const scrollSnapshot = props.preserveScrollPosition ? view.value.scrollSnapshot().map(view.value.state.changes(changes)) : undefined; // Update view.value.dispatch({ changes, selection: isSelectionOutOfRange ? { anchor: 0, head: 0 } : view.value.state.selection, scrollIntoView: props.scrollIntoView, effects: scrollSnapshot ? [scrollSnapshot] : undefined, }); }, { immediate: true } ); /** When loaded */ onMounted(async () => { // Skip initialization in SSR environment if (globalThis.window === undefined || !editor.value) { return; } /** Initial value */ let value: string | Text = doc.value; if (editor.value.childNodes[0]) { // when slot mode, overwrite initial value if (doc.value !== '') { console.warn( '[CodeMirror.vue] The tag contains child elements that overwrite the `v-model` values.' ); } value = (editor.value.childNodes[0] as HTMLElement).innerText.trim(); } // Register Codemirror view.value = new EditorView({ parent: editor.value, state: EditorState.create({ doc: value, extensions: extensions.value }), dispatch: (tr: Transaction) => { if (!view.value) { return; } view.value.update([tr]); if (tr.changes.empty || !tr.docChanged) { // if not change value, no fire emit event return; } // console.log(view.state.doc.toString(), tr); // state.toString() is not defined, so use toJSON and toText function to convert string. context.emit('update:modelValue', tr.state.doc.toString()); // Emit EditorState context.emit('change', tr.state); }, }); await nextTick(); context.emit('ready', { view: view.value, state: view.value.state, container: editor.value, }); }); /** Destroy */ onUnmounted(() => { if (view.value) { view.value.destroy(); context.emit('destroy'); } }); /** * Forces any linters configured to run when the editor is idle to run right away. * * @see {@link https://codemirror.net/docs/ref/#lint.forceLinting} */ const lint = (): void => { if (!props.linter || !view.value) { return; } if (props.forceLinting) { forceLinting(view.value); } diagnosticCount.value = linterDiagnosticCount(view.value.state); }; /** * Force Reconfigure Extension * * @see {@link https://codemirror.net/examples/config/#top-level-reconfiguration} */ const forceReconfigure = (): void => { // Deconfigure all Extensions view.value?.dispatch({ effects: StateEffect.reconfigure.of([]), }); // Register extensions view.value?.dispatch({ effects: StateEffect.appendConfig.of(extensions.value), }); }; /* ----- Bellow is experimental. ------ */ /** * Get the text between the given points in the editor. * * @param from - start line number * @param to - end line number */ const getRange = (from?: number, to?: number): string | undefined => view.value?.state.sliceDoc(from, to); /** * Get the content of line. * * @param number - line number */ const getLine = (number: number): string | undefined => view.value?.state.doc.line(number + 1).text; /** Get the number of lines in the editor. */ const lineCount = (): number => view.value?.state.doc.lines ?? 0; /** Retrieve one end of the primary selection. */ const getCursor = (): number => view.value?.state.selection.main.head ?? 0; /** Retrieves a list of all current selections. */ const listSelections = (): readonly SelectionRange[] => { return view.value?.state.selection.ranges ?? []; }; /** Get the currently selected code. */ const getSelection = (): string => { if (!view.value) { return ''; } return view.value.state.sliceDoc( view.value.state.selection.main.from, view.value.state.selection.main.to ); }; /** * The length of the given array should be the same as the number of active selections. * Replaces the content of the selections with the strings in the array. */ const getSelections = (): string[] => { const s = view.value?.state; if (!s) { return []; } return s.selection.ranges.map((r: { from: number; to: number }) => s.sliceDoc(r.from, r.to) ); }; /** Return true if any text is selected. */ const somethingSelected = (): boolean => view.value?.state.selection.ranges.some( (r: { empty: boolean }) => !r.empty ) ?? false; /** * Replace the part of the document between from and to with the given string. * * @param replacement - replacement text * @param from - start string at position * @param to - insert the string at position */ const replaceRange = ( replacement: string | Text, from: number, to: number ): void => { if (view.value) { view.value.dispatch({ changes: { from, to, insert: replacement }, }); } }; /** * Replace the selection(s) with the given string. * By default, the new selection ends up after the inserted text. * * @param replacement - replacement text */ const replaceSelection = (replacement: string | Text): void => { if (view.value) { view.value.dispatch(view.value.state.replaceSelection(replacement)); } }; /** * Set the cursor position. * * @param position - position. */ const setCursor = (position: number): void => { if (view.value) { view.value.dispatch({ selection: { anchor: position } }); } }; /** * Set a single selection range. * * @param anchor - anchor position * @param head - */ const setSelection = (anchor: number, head?: number): void => { if (view.value) { view.value.dispatch({ selection: { anchor, head } }); } }; /** * Sets a new set of selections. There must be at least one selection in the given array. * * @param ranges - Selection range * @param primary - */ const setSelections = ( ranges: readonly SelectionRange[], primary?: number ): void => { if (view.value) { view.value.dispatch({ selection: EditorSelection.create(ranges, primary), }); } }; /** * Applies the given function to all existing selections, and calls extendSelections on the result. * * @param f - function */ const extendSelectionsBy = (f: (range: SelectionRange) => number): void => { if (view.value && selection.value) { view.value.dispatch({ selection: EditorSelection.create( selection.value.ranges.map((r: SelectionRange) => r.extend(f(r))) ), }); } }; const exposed = { editor, view, cursor, selection, focus, length, json, diagnosticCount, dom: view.value?.contentDOM, lint, forceReconfigure, // Bellow is CodeMirror5's function getRange, getLine, lineCount, getCursor, listSelections, getSelection, getSelections, somethingSelected, replaceRange, replaceSelection, setCursor, setSelection, setSelections, extendSelectionsBy, }; /** Export properties and functions */ context.expose(exposed); return exposed; }, render() { // return h( this.$props.tag, { ref: 'editor', class: 'vue-codemirror', }, this.$slots.default ? // Hide original content h( 'aside', { style: 'display: none;', 'aria-hidden': 'true' }, slot(this.$slots.default) ) : undefined ); }, }); const installCodeMirror = (app: App): void => { app.component('CodeMirror', CodeMirror); }; /** Public API exposed from the CodeMirror component instance. * Note: Vue 3 auto-unwraps refs on the component instance proxy, * so reactive refs appear as their underlying value types here. */ export type CodeMirrorExposed = { editor: HTMLElement | undefined; view: EditorView | undefined; cursor: number; selection: EditorSelection | undefined; focus: boolean; length: number; json: Record> | undefined; diagnosticCount: number; dom: Element | undefined; lint: () => void; forceReconfigure: () => void; getRange: (from?: number, to?: number) => string | undefined; getLine: (number: number) => string | undefined; lineCount: () => number; getCursor: () => number; listSelections: () => readonly SelectionRange[]; getSelection: () => string; getSelections: () => string[]; somethingSelected: () => boolean; replaceRange: (replacement: string | Text, from: number, to: number) => void; replaceSelection: (replacement: string | Text) => void; setCursor: (position: number) => void; setSelection: (anchor: number, head?: number) => void; setSelections: (ranges: readonly SelectionRange[], primary?: number) => void; extendSelectionsBy: (f: (range: SelectionRange) => number) => void; }; export { CodeMirror as default, installCodeMirror as install, Meta }; ================================================ FILE: src/interfaces/MetaInterface.ts ================================================ /** Build information meta data */ type MetaInterface = { /** Version */ version: string; /** Build date */ date: string; }; export default MetaInterface; ================================================ FILE: src-docs/App.vue ================================================ ================================================ FILE: src-docs/DemoPage.vue ================================================ ================================================ FILE: src-docs/components/KeyMapDemo.vue ================================================ ================================================ FILE: src-docs/components/LinterAndCrossBindingDemo.vue ================================================ ================================================ FILE: src-docs/components/MarkdownDemo.vue ================================================ ================================================ FILE: src-docs/components/ReadonlyAndDisabledDemo.vue ================================================ ================================================ FILE: src-docs/components/SlotDemo.vue ================================================ ================================================ FILE: src-docs/components/ToggleTheme.vue ================================================ ================================================ FILE: src-docs/main.ts ================================================ import './style.scss'; /** Demo Code */ import { createApp } from 'vue'; import App from './App.vue'; createApp(App).mount('#app'); ================================================ FILE: src-docs/style.scss ================================================ // 1. Include functions first (so you can manipulate colors, SVGs, calc, etc) @import '~/bootstrap/scss/functions'; // 2. Include any default variable overrides here // 3. Include remainder of required Bootstrap stylesheets (including any separate color mode stylesheets) @import '~/bootstrap/scss/variables'; @import '~/bootstrap/scss/variables-dark'; // 4. Include any default map overrides here // 5. Include remainder of required parts @import '~/bootstrap/scss/maps'; @import '~/bootstrap/scss/mixins'; @import '~/bootstrap/scss/root'; // 6. Optionally include any other parts as needed @import '~/bootstrap/scss/utilities'; @import '~/bootstrap/scss/reboot'; // @import '~/bootstrap/scss/type'; @import '~/bootstrap/scss/forms'; // @import '~/bootstrap/scss/dropdown'; @import '~/bootstrap/scss/alert'; @import '~/bootstrap/scss/nav'; @import '~/bootstrap/scss/navbar'; @import '~/bootstrap/scss/containers'; @import '~/bootstrap/scss/grid'; @import '~/bootstrap/scss/helpers'; // 7. Optionally include utilities API last to generate classes based on the Sass map in `_utilities.scss` @import '~/bootstrap/scss/utilities/api'; // 8. Add additional custom code here .vue-codemirror * { font-family: var(--bs-font-monospace); } // GitHub Markdown CSS (used in MarkdownDemo) @import url(https://cdn.jsdelivr.net/npm/github-markdown-css@5.1.0/github-markdown.min.css); .markdown-body { // Override prefers-color-scheme to respect Vue's dark mode setting @media (prefers-color-scheme: dark) { &[data-color-mode='light'] { color-scheme: light; --color-prettylights-syntax-comment: #57606a; --color-prettylights-syntax-constant: #0550ae; --color-prettylights-syntax-entity: #6639ba; --color-prettylights-syntax-storage-modifier-import: #24292f; --color-prettylights-syntax-entity-tag: #116329; --color-prettylights-syntax-keyword: #cf222e; --color-prettylights-syntax-string: #0a3069; --color-prettylights-syntax-variable: #953800; --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; --color-prettylights-syntax-invalid-illegal-bg: #82071e; --color-prettylights-syntax-carriage-return-text: #f6f8fa; --color-prettylights-syntax-carriage-return-bg: #cf222e; --color-prettylights-syntax-string-regexp: #116329; --color-prettylights-syntax-markup-list: #3b2300; --color-prettylights-syntax-markup-heading: #0550ae; --color-prettylights-syntax-markup-italic: #24292f; --color-prettylights-syntax-markup-bold: #24292f; --color-prettylights-syntax-markup-deleted-text: #82071e; --color-prettylights-syntax-markup-deleted-bg: #ffebe9; --color-prettylights-syntax-markup-inserted-text: #116329; --color-prettylights-syntax-markup-inserted-bg: #dafbe1; --color-prettylights-syntax-markup-changed-text: #953800; --color-prettylights-syntax-markup-changed-bg: #ffd8b5; --color-prettylights-syntax-markup-ignored-text: #eaeef2; --color-prettylights-syntax-markup-ignored-bg: #0550ae; --color-prettylights-syntax-meta-diff-range: #8250df; --color-prettylights-syntax-brackethighlighter-angle: #57606a; --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; --color-prettylights-syntax-constant-other-reference-link: #0a3069; --color-fg-default: #24292f; --color-fg-muted: #57606a; --color-fg-subtle: #6e7781; --color-canvas-default: #ffffff; --color-canvas-subtle: #f6f8fa; --color-border-default: #d0d7de; --color-border-muted: hsla(210, 18%, 87%, 1); --color-neutral-muted: rgba(175, 184, 193, 0.2); --color-accent-fg: #0969da; --color-accent-emphasis: #0969da; --color-attention-subtle: #fff8c5; --color-danger-fg: #cf222e; } } @media (prefers-color-scheme: light) { &[data-color-mode='dark'] { color-scheme: dark; --color-prettylights-syntax-comment: #8b949e; --color-prettylights-syntax-constant: #79c0ff; --color-prettylights-syntax-entity: #d2a8ff; --color-prettylights-syntax-storage-modifier-import: #c9d1d9; --color-prettylights-syntax-entity-tag: #7ee787; --color-prettylights-syntax-keyword: #ff7b72; --color-prettylights-syntax-string: #a5d6ff; --color-prettylights-syntax-variable: #ffa657; --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; --color-prettylights-syntax-invalid-illegal-bg: #8e1519; --color-prettylights-syntax-carriage-return-text: #f0f6fc; --color-prettylights-syntax-carriage-return-bg: #b62324; --color-prettylights-syntax-string-regexp: #7ee787; --color-prettylights-syntax-markup-list: #f2cc60; --color-prettylights-syntax-markup-heading: #1f6feb; --color-prettylights-syntax-markup-italic: #c9d1d9; --color-prettylights-syntax-markup-bold: #c9d1d9; --color-prettylights-syntax-markup-deleted-text: #ffdcd7; --color-prettylights-syntax-markup-deleted-bg: #67060c; --color-prettylights-syntax-markup-inserted-text: #aff5b4; --color-prettylights-syntax-markup-inserted-bg: #033a16; --color-prettylights-syntax-markup-changed-text: #ffdfb6; --color-prettylights-syntax-markup-changed-bg: #5a1e02; --color-prettylights-syntax-markup-ignored-text: #c9d1d9; --color-prettylights-syntax-markup-ignored-bg: #1158c7; --color-prettylights-syntax-meta-diff-range: #d2a8ff; --color-prettylights-syntax-brackethighlighter-angle: #8b949e; --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; --color-fg-default: #c9d1d9; --color-fg-muted: #8b949e; --color-fg-subtle: #6e7681; --color-canvas-default: #0d1117; --color-canvas-subtle: #161b22; --color-border-default: #30363d; --color-border-muted: #21262d; --color-neutral-muted: rgba(110, 118, 129, 0.4); --color-accent-fg: #58a6ff; --color-accent-emphasis: #1f6feb; --color-attention-subtle: rgba(187, 128, 9, 0.15); --color-danger-fg: #f85149; } } h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { display: block; float: left; height: 1.2em; width: 1em; margin-left: -1em; position: relative; outline: none; } /*.anchor:target { background: yellow; }*/ h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { visibility: hidden; position: absolute; opacity: 0.2; right: 0; top: 0; width: 1.2em; line-height: inherit; content: '🔗'; } h1 > a.anchor:hover:before, h2 > a.anchor:hover:before, h3 > a.anchor:hover:before, h4 > a.anchor:hover:before, h5 > a.anchor:hover:before, h6 > a.anchor:hover:before { visibility: visible; opacity: 0.8; } h1:hover .anchor:before, h2:hover .anchor:before, h3:hover .anchor:before, h4:hover .anchor:before, h5:hover .anchor:before, h6:hover .anchor:before { visibility: visible; } } ================================================ FILE: tsconfig.app.json ================================================ { "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "exclude": ["src/**/__tests__/*"], "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "allowImportingTsExtensions": true, "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "preserve", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "forceConsistentCasingInFileNames": true, "paths": { "@/*": ["./src/*"] } } } ================================================ FILE: tsconfig.docs.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "./tsconfig.app.json", "include": [ "./env.d.ts", "./src/**/*", "./src/**/*.vue", "./src-docs/**/*", "./src-docs/**/*.vue" ], "compilerOptions": { "composite": true, "declarationMap": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "paths": { "@/*": ["./src/*"], "vue-codemirror6": ["./src/index.ts"] } } } ================================================ FILE: tsconfig.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig.json", "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.docs.json" }, { "path": "./tsconfig.node.json" }, { "path": "./tsconfig.vitest.json" } ] } ================================================ FILE: tsconfig.node.json ================================================ { "extends": "@tsconfig/node-lts/tsconfig.json", "include": [ "vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*", "eslint.config.*" ], "compilerOptions": { "noEmit": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "module": "ESNext", "moduleResolution": "Bundler", "types": ["node"] } } ================================================ FILE: tsconfig.vitest.json ================================================ { "extends": "./tsconfig.app.json", "include": ["src/**/*", "src/**/__tests__/*", "env.d.ts"], "exclude": [], "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", "composite": true, "lib": ["ES2022", "DOM"], "types": ["node", "vitest/globals", "@vue/test-utils"] } } ================================================ FILE: vite.config.ts ================================================ import { writeFileSync } from 'node:fs'; import { fileURLToPath, URL } from 'node:url'; import Vue from '@vitejs/plugin-vue'; import { defineConfig, type UserConfig } from 'vite'; import { visualizer } from 'rollup-plugin-visualizer'; import banner from 'vite-plugin-banner'; import { checker } from 'vite-plugin-checker'; import dts from 'vite-plugin-dts'; import pkg from './package.json'; // https://vitejs.dev/config/ export default defineConfig(({ mode, command }): UserConfig => { const config: UserConfig = { base: './', publicDir: command === 'serve' ? 'public' : false, // Resolver resolve: { // https://vitejs.dev/config/shared-options.html#resolve-alias alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), '~': fileURLToPath(new URL('./node_modules', import.meta.url)), 'vue-codemirror6': fileURLToPath(new URL('./src', import.meta.url)), }, }, plugins: [ Vue(), // vite-plugin-checker // https://github.com/fi3ework/vite-plugin-checker checker({ typescript: true, // vueTsc: true, // eslint: { lintCommand: 'eslint' }, }), // vite-plugin-banner // https://github.com/chengpeiquan/vite-plugin-banner banner(`/** * ${pkg.name} * * @description ${pkg.description} * @author ${pkg.author.name} <${pkg.author.email}> * @copyright 2022-2026 By Masashi Yoshikawa All rights reserved. * @license ${pkg.license} * @version ${pkg.version} * @see {@link ${pkg.homepage}} */ `), // vite-plugin-dts // https://github.com/qmhc/vite-plugin-dts mode === 'docs' ? undefined : dts({ tsconfigPath: './tsconfig.app.json' }), ], optimizeDeps: { exclude: [ 'vue-demi', // https://github.com/codemirror/dev/issues/608 '@codemirror/state', ], }, // Build Options // https://vitejs.dev/config/#build-options build: { outDir: mode === 'docs' ? 'docs' : undefined, lib: mode === 'docs' ? undefined : { entry: fileURLToPath(new URL('./src/index.ts', import.meta.url)), name: 'CodeMirror', formats: ['es', 'cjs', 'umd', 'iife'], fileName: format => `index.${format}.js`, }, rollupOptions: { plugins: [ mode === 'analyze' ? // rollup-plugin-visualizer // https://github.com/btd/rollup-plugin-visualizer visualizer({ open: true, filename: 'stats.html', gzipSize: false, brotliSize: false, }) : undefined, ], external: mode === 'docs' ? undefined : [ '@codemirror/autocomplete', '@codemirror/commands', '@codemirror/language', '@codemirror/lint', '@codemirror/search', '@codemirror/state', '@codemirror/view', 'codemirror', 'style-mod', 'vue-demi', 'vue', ], output: { esModule: true, exports: 'named', globals: { '@codemirror/autocomplete': 'autocomplete', '@codemirror/commands': 'commands', '@codemirror/language': 'language', '@codemirror/lint': 'lint', '@codemirror/search': 'search', '@codemirror/state': 'state', '@codemirror/view': 'view', 'style-mod': 'styleMod', 'vue-demi': 'VueDemi', codemirror: 'codemirror', vue: 'Vue', }, manualChunks: mode !== 'docs' ? undefined : (id: string) => { if ( id.includes('/node_modules/@vue/') || id.includes('/node_modules/vue') ) { return 'vue'; } if (id.includes('/node_modules/eslint-linter-browserify/')) { return 'eslint-linter-browserify'; } // CodeMirror6 if ( id.includes('/node_modules/codemirror/') || id.includes('/node_modules/@codemirror/') || id.includes('/node_modules/style-mod/') ) { // Split CodeMirror and CodeMirror language into separate chunks return !/lang-/.exec(id) ? 'codemirror' : 'codemirror-lang'; } if (id.includes('/node_modules/')) { // Other chunks return 'vendor'; } }, }, }, // Minify option target: 'esnext', // minify: mode === 'docs', }, }; // Write meta data. // eslint-disable-next-line security/detect-non-literal-fs-filename -- path is resolved from import.meta.url which is a static ESM URL, not user input writeFileSync( fileURLToPath(new URL('./src/Meta.ts', import.meta.url)), `import type MetaInterface from '@/interfaces/MetaInterface'; // This file is auto-generated by the build system. const meta: MetaInterface = { version: '${pkg.version}', date: '${new Date().toISOString()}', }; export default meta; ` ); // Export vite config return config; }); ================================================ FILE: vitest.config.ts ================================================ import { fileURLToPath, URL } from 'node:url'; import Vue from '@vitejs/plugin-vue'; import { defineConfig } from 'vitest/config'; // https://vitest.dev/config/ export default defineConfig({ plugins: [Vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, test: { globals: true, environment: 'happy-dom', coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/', 'src-docs/', 'dist/', 'docs/', '**/*.d.ts', '**/*.config.*', '**/mockData/**', 'src/Meta.ts', 'src/helpers/h-demi.ts', ], }, }, });