├── .editorconfig
├── .eslintignore
├── .eslintrc.cjs
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .npmignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .releaserc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
├── all-options.png
└── basic-input.png
├── babel.config.cjs
├── commitlint.config.cjs
├── jest.config.cjs
├── lint-staged.config.cjs
├── package-lock.json
├── package.config.ts
├── package.json
├── renovate.json
├── sanity.config.ts
├── sanity.json
├── src
├── CodeInput.tsx
├── LanguageField.tsx
├── LanguageInput.tsx
├── PreviewCode.tsx
├── codemirror
│ ├── CodeMirrorProxy.tsx
│ ├── CodeModeContext.tsx
│ ├── defaultCodeModes.ts
│ ├── extensions
│ │ ├── backwardsCompatibleTone.ts
│ │ ├── highlightLineExtension.ts
│ │ ├── theme.ts
│ │ ├── useCodeMirrorTheme.ts
│ │ └── useFontSize.ts
│ ├── useCodeMirror-client.test.tsx
│ ├── useCodeMirror-server.test.tsx
│ ├── useCodeMirror.tsx
│ └── useLanguageMode.tsx
├── config.ts
├── getMedia.tsx
├── index.ts
├── plugin.tsx
├── schema.tsx
├── types.ts
├── ui
│ └── focusRingStyle.ts
└── useFieldMember.ts
├── test
└── schema.ts
├── tsconfig.json
├── tsconfig.lib.json
├── tsconfig.settings.json
└── v2-incompatible.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; editorconfig.org
2 | root = true
3 | charset= utf8
4 |
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 | indent_style = space
10 | indent_size = 2
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .workshop
2 | .eslintrc.js
3 | babel.config.js
4 | commitlint.config.js
5 | jest.config.js
6 | lib
7 | lint-staged.config.js
8 | package.config.ts
9 | v2-incompatible.js
10 | workshop.config.ts
11 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | node: false,
5 | jest: true,
6 | },
7 | extends: [
8 | 'sanity',
9 | 'sanity/react', // must come before sanity/typescript
10 | 'sanity/typescript',
11 | 'plugin:react/jsx-runtime',
12 | 'plugin:react-hooks/recommended',
13 | 'plugin:prettier/recommended',
14 | ],
15 | overrides: [
16 | {
17 | files: ['*.{ts,tsx}'],
18 | },
19 | ],
20 | parser: '@typescript-eslint/parser',
21 | parserOptions: {
22 | ecmaFeatures: {
23 | jsx: true,
24 | },
25 | project: './tsconfig.json',
26 | },
27 | plugins: ['prettier'],
28 | rules: {
29 | '@typescript-eslint/explicit-function-return-type': 0,
30 | '@typescript-eslint/no-shadow': 'error',
31 | '@typescript-eslint/no-unused-vars': 1,
32 | 'no-shadow': 'off',
33 | 'react/display-name': 0,
34 | 'react/jsx-handler-names': 0,
35 | 'react/jsx-no-bind': 0,
36 | },
37 | settings: {
38 | 'import/ignore': ['\\.css$', '.*node_modules.*', '.*:.*'],
39 | 'import/resolver': {
40 | node: {
41 | paths: ['src'],
42 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
43 | },
44 | },
45 | },
46 | }
47 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: CI & Release
3 |
4 | # Workflow name based on selected inputs. Fallback to default Github naming when expression evaluates to empty string
5 | run-name: >-
6 | ${{
7 | inputs.release && inputs.test && 'Build ➤ Test ➤ Publish to NPM' ||
8 | inputs.release && !inputs.test && 'Build ➤ Skip Tests ➤ Publish to NPM' ||
9 | github.event_name == 'workflow_dispatch' && inputs.test && 'Build ➤ Test' ||
10 | github.event_name == 'workflow_dispatch' && !inputs.test && 'Build ➤ Skip Tests' ||
11 | ''
12 | }}
13 |
14 | on:
15 | # Build on pushes branches that have a PR (including drafts)
16 | pull_request:
17 | # Build on commits pushed to branches without a PR if it's in the allowlist
18 | push:
19 | branches: [main]
20 | # https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow
21 | workflow_dispatch:
22 | inputs:
23 | test:
24 | description: Run tests
25 | required: true
26 | default: true
27 | type: boolean
28 | release:
29 | description: Release new version
30 | required: true
31 | default: false
32 | type: boolean
33 |
34 | concurrency:
35 | # On PRs builds will cancel if new pushes happen before the CI completes, as it defines `github.head_ref` and gives it the name of the branch the PR wants to merge into
36 | # Otherwise `github.run_id` ensures that you can quickly merge a queue of PRs without causing tests to auto cancel on any of the commits pushed to main.
37 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
38 | cancel-in-progress: true
39 |
40 | jobs:
41 | log-the-inputs:
42 | name: Log inputs
43 | runs-on: ubuntu-latest
44 | steps:
45 | - run: |
46 | echo "Inputs: $INPUTS"
47 | env:
48 | INPUTS: ${{ toJSON(inputs) }}
49 |
50 | build:
51 | runs-on: ubuntu-latest
52 | name: Lint & Build
53 | steps:
54 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
55 | - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
56 | with:
57 | cache: npm
58 | node-version: lts/*
59 | - run: npm ci
60 | # Linting can be skipped
61 | - run: npm run lint --if-present
62 | if: github.event.inputs.test != 'false'
63 | # But not the build script, as semantic-release will crash if this command fails so it makes sense to test it early
64 | - run: npm run prepublishOnly --if-present
65 |
66 | test:
67 | needs: build
68 | # The test matrix can be skipped, in case a new release needs to be fast-tracked and tests are already passing on main
69 | if: github.event.inputs.test != 'false'
70 | runs-on: ${{ matrix.os }}
71 | name: Node.js ${{ matrix.node }} / ${{ matrix.os }}
72 | strategy:
73 | # A test failing on windows doesn't mean it'll fail on macos. It's useful to let all tests run to its completion to get the full picture
74 | fail-fast: false
75 | matrix:
76 | # Run the testing suite on each major OS with the latest LTS release of Node.js
77 | os: [macos-latest, ubuntu-latest, windows-latest]
78 | node: [lts/*]
79 | # It makes sense to also test the oldest, and latest, versions of Node.js, on ubuntu-only since it's the fastest CI runner
80 | include:
81 | - os: ubuntu-latest
82 | # Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life
83 | node: lts/-2
84 | - os: ubuntu-latest
85 | # Test the actively developed version that will become the latest LTS release next October
86 | node: current
87 | steps:
88 | # It's only necessary to do this for windows, as mac and ubuntu are sane OS's that already use LF
89 | - name: Set git to use LF
90 | if: matrix.os == 'windows-latest'
91 | run: |
92 | git config --global core.autocrlf false
93 | git config --global core.eol lf
94 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
95 | - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
96 | with:
97 | cache: npm
98 | node-version: ${{ matrix.node }}
99 | - run: npm i
100 | - run: npm test
101 |
102 | release:
103 | needs: [build, test]
104 | # only run if opt-in during workflow_dispatch
105 | if: always() && github.event.inputs.release == 'true' && needs.build.result != 'failure' && needs.test.result != 'failure' && needs.test.result != 'cancelled'
106 | runs-on: ubuntu-latest
107 | name: Semantic release
108 | steps:
109 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
110 | with:
111 | # Need to fetch entire commit history to
112 | # analyze every commit since last release
113 | fetch-depth: 0
114 | - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
115 | with:
116 | cache: npm
117 | node-version: lts/*
118 | - run: npm ci
119 | # Branches that will release new versions are defined in .releaserc.json
120 | - run: npx semantic-release
121 | # Don't allow interrupting the release step if the job is cancelled, as it can lead to an inconsistent state
122 | # e.g. git tags were pushed but it exited before `npm publish`
123 | if: always()
124 | env:
125 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
126 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
127 | # Re-run semantic release with rich logs if it failed to publish for easier debugging
128 | - run: npx semantic-release --dry-run --debug
129 | if: failure()
130 | env:
131 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
132 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
133 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
39 | # macOS finder cache file
40 | .DS_Store
41 |
42 | # VS Code settings
43 | .vscode
44 |
45 | # IntelliJ
46 | .idea
47 | *.iml
48 |
49 | # Cache
50 | .cache
51 |
52 | # Yalc
53 | .yalc
54 | yalc.lock
55 |
56 | # npm package zips
57 | *.tgz
58 |
59 | # Compiled plugin
60 | lib
61 |
62 | # Workshop runtime files
63 | .workshop
64 |
65 | # Typescript build info
66 | tsconfig.tsbuildinfo
67 |
68 | # Sanity runtime output
69 | .sanity
70 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no -- commitlint --edit ""
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /.workshop
2 | /test
3 | /coverage
4 | .editorconfig
5 | .eslintrc
6 | .gitignore
7 | .github
8 | .prettierrc
9 | .travis.yml
10 | .nyc_output
11 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | legacy-peer-deps=true
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .workshop
2 | lib
3 | pnpm-lock.yaml
4 | yarn.lock
5 | package-lock.json
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "printWidth": 100,
4 | "bracketSpacing": false,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/.releaserc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sanity/semantic-release-preset",
3 | "branches": ["main"]
4 | }
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 📓 Changelog
4 |
5 | All notable changes to this project will be documented in this file. See
6 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
7 |
8 | ## [5.1.2](https://github.com/sanity-io/code-input/compare/v5.1.1...v5.1.2) (2024-12-16)
9 |
10 | ### Bug Fixes
11 |
12 | - warnings from styled-components on language input render ([#112](https://github.com/sanity-io/code-input/issues/112)) ([4c71607](https://github.com/sanity-io/code-input/commit/4c71607ecadd358d55c928ef6c31ae42712dfbcc))
13 |
14 | ## [5.1.1](https://github.com/sanity-io/code-input/compare/v5.1.0...v5.1.1) (2024-12-14)
15 |
16 | ### Bug Fixes
17 |
18 | - **deps:** upgrade `@sanity/icons` to v3 ([2870c01](https://github.com/sanity-io/code-input/commit/2870c01eae2055c1658166d4c3effff9ba7daca2))
19 |
20 | ## [5.1.0](https://github.com/sanity-io/code-input/compare/v5.0.0...v5.1.0) (2024-12-14)
21 |
22 | ### Features
23 |
24 | - support react 19, upgrade build tooling ([#111](https://github.com/sanity-io/code-input/issues/111)) ([d9d2826](https://github.com/sanity-io/code-input/commit/d9d2826f7eec60e8e3439cb22b938fc2f2d318e8))
25 |
26 | ## [5.0.0](https://github.com/sanity-io/code-input/compare/v4.1.4...v5.0.0) (2024-11-28)
27 |
28 | ### ⚠ BREAKING CHANGES
29 |
30 | - **deps:** Update dependency @sanity/ui to v2 (#94)
31 |
32 | ### Bug Fixes
33 |
34 | - **deps:** Update dependency @sanity/ui to v2 ([#94](https://github.com/sanity-io/code-input/issues/94)) ([7178d87](https://github.com/sanity-io/code-input/commit/7178d8737574a36407b21f44ee4feb11a48701a7))
35 |
36 | ## [4.1.4](https://github.com/sanity-io/code-input/compare/v4.1.3...v4.1.4) (2024-04-09)
37 |
38 | ### Bug Fixes
39 |
40 | - remove incorrect autocomplete from GROQ ([#103](https://github.com/sanity-io/code-input/issues/103)) ([1f45acb](https://github.com/sanity-io/code-input/commit/1f45acb709018d273b105f870fdf5ae323d0cd38))
41 |
42 | ## [4.1.3](https://github.com/sanity-io/code-input/compare/v4.1.2...v4.1.3) (2024-02-21)
43 |
44 | ### Bug Fixes
45 |
46 | - update dependencies ([#99](https://github.com/sanity-io/code-input/issues/99)) ([ab8634d](https://github.com/sanity-io/code-input/commit/ab8634d516b06d526528690e3e3332bad2b9d359))
47 |
48 | ## [4.1.2](https://github.com/sanity-io/code-input/compare/v4.1.1...v4.1.2) (2023-12-15)
49 |
50 | ### Bug Fixes
51 |
52 | - **deps:** allow styled-components v6 as peer dependency ([#89](https://github.com/sanity-io/code-input/issues/89)) ([2ba3bf2](https://github.com/sanity-io/code-input/commit/2ba3bf2d8540e7e2415e08be229d030cf2b97d16))
53 | - **deps:** update codemirror ([#52](https://github.com/sanity-io/code-input/issues/52)) ([d883481](https://github.com/sanity-io/code-input/commit/d883481c0bde01b8b27af43caac3263abcb125a3))
54 | - **deps:** update codemirror ([#65](https://github.com/sanity-io/code-input/issues/65)) ([b866640](https://github.com/sanity-io/code-input/commit/b8666405755e582842da7995ed9929e8fc3e3571))
55 | - **deps:** update codemirror ([#66](https://github.com/sanity-io/code-input/issues/66)) ([5d1605e](https://github.com/sanity-io/code-input/commit/5d1605e1545ad3b33a951fb471f4f70b8f46427d))
56 | - **deps:** update dependencies (non-major) ([#32](https://github.com/sanity-io/code-input/issues/32)) ([5f2ab52](https://github.com/sanity-io/code-input/commit/5f2ab52e467aa5b7af134b49f4023b6db4b0d1a5))
57 | - **deps:** update dependencies (non-major) to v4.20.2 ([#67](https://github.com/sanity-io/code-input/issues/67)) ([ec2dcef](https://github.com/sanity-io/code-input/commit/ec2dcef85c506fd911cea5b05274e1c78dfc6fde))
58 | - update package description (codemirror, not ace) ([3b769e2](https://github.com/sanity-io/code-input/commit/3b769e26b3b57cc778dadb540f9007ee97a29c30))
59 |
60 | ## [4.1.1](https://github.com/sanity-io/code-input/compare/v4.1.0...v4.1.1) (2023-05-10)
61 |
62 | ### Bug Fixes
63 |
64 | - editor should no longer crash when highlightlines are unsorted ([e2f9d98](https://github.com/sanity-io/code-input/commit/e2f9d981997c2431fb8f6ab35ffbef359862ea80))
65 |
66 | ## [4.1.0](https://github.com/sanity-io/code-input/compare/v4.0.0...v4.1.0) (2023-03-01)
67 |
68 | ### Features
69 |
70 | - richer preview component ([5b751e9](https://github.com/sanity-io/code-input/commit/5b751e92c2079b2e69f8f9f639cdb7cd98cfa990))
71 |
72 | ## [4.0.0](https://github.com/sanity-io/code-input/compare/v3.0.1...v4.0.0) (2023-01-16)
73 |
74 | ### ⚠ BREAKING CHANGES
75 |
76 | - api for configuring custom languages has changed. Consult the README for details.
77 |
78 | ### Features
79 |
80 | - click line-number to highlight line ([8e5cf07](https://github.com/sanity-io/code-input/commit/8e5cf072bd70a8a6526daf55423ba040eef40d43))
81 | - replaced AceEditor with CodeMirror as code editor ([ef4fe48](https://github.com/sanity-io/code-input/commit/ef4fe48ceab4f713873b3aeebe0b611b070ff79f))
82 |
83 | ### Bug Fixes
84 |
85 | - **deps:** applied npx @sanity/plugin-kit inject ([9ccfe69](https://github.com/sanity-io/code-input/commit/9ccfe69ae11bc48b6fdf22d1d823a82ced561a32))
86 | - implement sanity theming ([9566cd2](https://github.com/sanity-io/code-input/commit/9566cd2dc8d2adc5c312ee5dde0044001a401028))
87 | - improved highlighted line color ([c151e95](https://github.com/sanity-io/code-input/commit/c151e9591ffb4abf3f7bb6bb72f558d9a951737c))
88 | - refactored default language ([2111776](https://github.com/sanity-io/code-input/commit/2111776f91cda2a472b5d1bfaba36e2fa5f2a68e))
89 | - render focus ring ([5fc3cc1](https://github.com/sanity-io/code-input/commit/5fc3cc14b91c524026e2ae551faa5bc735150a2c))
90 | - wrap lines ([301640e](https://github.com/sanity-io/code-input/commit/301640e7a3614cc22dd34cdff71901521f7ea67a))
91 |
92 | ## [3.0.1](https://github.com/sanity-io/code-input/compare/v3.0.0...v3.0.1) (2022-12-06)
93 |
94 | ### Bug Fixes
95 |
96 | - preview for portable text and arrays now works in 3.0.0+ ([f4dfd0a](https://github.com/sanity-io/code-input/commit/f4dfd0a0f34039f31c76dff750ae697eb918014f))
97 |
98 | ## [3.0.0](https://github.com/sanity-io/code-input/compare/v2.35.2...v3.0.0) (2022-11-25)
99 |
100 | ### ⚠ BREAKING CHANGES
101 |
102 | - this version does not work in Sanity Studio v2
103 |
104 | ### Features
105 |
106 | - initial Sanity Studio v3 release ([045dd4b](https://github.com/sanity-io/code-input/commit/045dd4bb51d7611c3eb1c72706539797c951ebfe))
107 |
108 | ### Bug Fixes
109 |
110 | - @sanity/ui 1.0.0-beta.31 ([b4d6575](https://github.com/sanity-io/code-input/commit/b4d657556be578399375328080e1e21e641097cb))
111 | - code-definition extension to intrinsics should work again ([c7b3c1f](https://github.com/sanity-io/code-input/commit/c7b3c1fe4c667302ec7aa7d34bc131d467156406))
112 | - compiled for dev-preview.22 ([bb9a6a7](https://github.com/sanity-io/code-input/commit/bb9a6a774c310ff3f12e5df4db48a8522632912c))
113 | - compiled for sanity 3.0.0-rc.0 ([46085b7](https://github.com/sanity-io/code-input/commit/46085b712754c418f599b837301b43b204e72f9c))
114 | - compiled for sanity 3.0.0-rc.0 ([ccf2b61](https://github.com/sanity-io/code-input/commit/ccf2b619d2c08b21444de666ff2a3c5f67f9e476))
115 | - **deps:** dev-preview.21 ([89ecdc3](https://github.com/sanity-io/code-input/commit/89ecdc316d652d6456ff677b338df1a6b35be92b))
116 | - **deps:** pin dependencies ([#28](https://github.com/sanity-io/code-input/issues/28)) ([59d92d9](https://github.com/sanity-io/code-input/commit/59d92d9ce0a6ce0cd403c32dd9058e37e7ac87e4))
117 | - **deps:** pkg-utils & @sanity/plugin-kit ([ba975f9](https://github.com/sanity-io/code-input/commit/ba975f9cfe1b1204757673b4fbdc0f1e824f07d7))
118 | - **deps:** react-dom as peer ([bee9c70](https://github.com/sanity-io/code-input/commit/bee9c7042f9e62a48f22c9ad2d505e8fc3c70c12))
119 | - **deps:** sanity ^3.0.0 (works with rc.3) ([764c10a](https://github.com/sanity-io/code-input/commit/764c10abef4ac376f7c1271a197cce1140cd7e32))
120 | - **deps:** sanity 3.0.0-dev-preview.17 and ui 0.38 ([4bef2ab](https://github.com/sanity-io/code-input/commit/4bef2ab5505144da4a286c9481c12f85c61af11c))
121 | - **deps:** update dependency @sanity/icons to v1.3.9-beta.3 ([#30](https://github.com/sanity-io/code-input/issues/30)) ([9b49361](https://github.com/sanity-io/code-input/commit/9b493612e388dfa71dfc6be0afb5b1d7306136e3))
122 | - **deps:** update dependency ace-builds to ^1.12.5 ([#8](https://github.com/sanity-io/code-input/issues/8)) ([ae169c7](https://github.com/sanity-io/code-input/commit/ae169c75e162322d7396132448cde942511a79d7))
123 | - **deps:** update sanity packages to v1 (major) ([#10](https://github.com/sanity-io/code-input/issues/10)) ([260a911](https://github.com/sanity-io/code-input/commit/260a911fdf3715d5d73554488595209b1aad9e65))
124 | - lazy load AceEditor to allow server side rendering the input ([d1d173b](https://github.com/sanity-io/code-input/commit/d1d173b01d360af69d35a8f6d0708785df439639))
125 | - makes PreviewCode also load ace async ([#11](https://github.com/sanity-io/code-input/issues/11)) ([16eb077](https://github.com/sanity-io/code-input/commit/16eb077901266b28038142408d2e74f2c20e5aec))
126 | - preview component now uses v3 api correctly ([280b620](https://github.com/sanity-io/code-input/commit/280b6204ac7a7863d54618be8b220ae8c6b0e718))
127 | - preview is now dev-preview.17 compatible ([58a75d9](https://github.com/sanity-io/code-input/commit/58a75d96cf8f2b51aabea6e3f573922ee5fb7827))
128 | - setup changelog generator ([a52df38](https://github.com/sanity-io/code-input/commit/a52df3808aa8448b43b812f4b6ed3058b1f3f9bb))
129 | - update lockfile ([cf401bb](https://github.com/sanity-io/code-input/commit/cf401bbc18b921d2e7d780448aadcc9fc48cbe2a))
130 | - use `@sanity/semantic-release-preset` ([fb0c785](https://github.com/sanity-io/code-input/commit/fb0c785b3d8b8e38cba16157452ea517ac23e05f))
131 |
132 | ## [3.0.0-v3-studio.15](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.14...v3.0.0-v3-studio.15) (2022-11-04)
133 |
134 | ### Bug Fixes
135 |
136 | - **deps:** pkg-utils & @sanity/plugin-kit ([ba975f9](https://github.com/sanity-io/code-input/commit/ba975f9cfe1b1204757673b4fbdc0f1e824f07d7))
137 |
138 | ## [3.0.0-v3-studio.14](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.13...v3.0.0-v3-studio.14) (2022-11-04)
139 |
140 | ### Bug Fixes
141 |
142 | - **deps:** pin dependencies ([#28](https://github.com/sanity-io/code-input/issues/28)) ([59d92d9](https://github.com/sanity-io/code-input/commit/59d92d9ce0a6ce0cd403c32dd9058e37e7ac87e4))
143 | - **deps:** update dependency @sanity/icons to v1.3.9-beta.3 ([#30](https://github.com/sanity-io/code-input/issues/30)) ([9b49361](https://github.com/sanity-io/code-input/commit/9b493612e388dfa71dfc6be0afb5b1d7306136e3))
144 | - **deps:** update dependency ace-builds to ^1.12.5 ([#8](https://github.com/sanity-io/code-input/issues/8)) ([ae169c7](https://github.com/sanity-io/code-input/commit/ae169c75e162322d7396132448cde942511a79d7))
145 |
146 | ## [3.0.0-v3-studio.13](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.12...v3.0.0-v3-studio.13) (2022-11-02)
147 |
148 | ### Bug Fixes
149 |
150 | - compiled for sanity 3.0.0-rc.0 ([46085b7](https://github.com/sanity-io/code-input/commit/46085b712754c418f599b837301b43b204e72f9c))
151 | - compiled for sanity 3.0.0-rc.0 ([ccf2b61](https://github.com/sanity-io/code-input/commit/ccf2b619d2c08b21444de666ff2a3c5f67f9e476))
152 |
153 | ## [3.0.0-v3-studio.12](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.11...v3.0.0-v3-studio.12) (2022-10-27)
154 |
155 | ### Bug Fixes
156 |
157 | - @sanity/ui 1.0.0-beta.31 ([b4d6575](https://github.com/sanity-io/code-input/commit/b4d657556be578399375328080e1e21e641097cb))
158 | - compiled for dev-preview.22 ([bb9a6a7](https://github.com/sanity-io/code-input/commit/bb9a6a774c310ff3f12e5df4db48a8522632912c))
159 |
160 | ## [3.0.0-v3-studio.11](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.10...v3.0.0-v3-studio.11) (2022-10-07)
161 |
162 | ### Bug Fixes
163 |
164 | - code-definition extension to intrinsics should work again ([c7b3c1f](https://github.com/sanity-io/code-input/commit/c7b3c1fe4c667302ec7aa7d34bc131d467156406))
165 |
166 | ## [3.0.0-v3-studio.10](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.9...v3.0.0-v3-studio.10) (2022-10-07)
167 |
168 | ### Bug Fixes
169 |
170 | - **deps:** dev-preview.21 ([89ecdc3](https://github.com/sanity-io/code-input/commit/89ecdc316d652d6456ff677b338df1a6b35be92b))
171 |
172 | ## [3.0.0-v3-studio.9](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.8...v3.0.0-v3-studio.9) (2022-09-15)
173 |
174 | ### Bug Fixes
175 |
176 | - preview is now dev-preview.17 compatible ([58a75d9](https://github.com/sanity-io/code-input/commit/58a75d96cf8f2b51aabea6e3f573922ee5fb7827))
177 |
178 | ## [3.0.0-v3-studio.8](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.7...v3.0.0-v3-studio.8) (2022-09-15)
179 |
180 | ### Bug Fixes
181 |
182 | - **deps:** react-dom as peer ([bee9c70](https://github.com/sanity-io/code-input/commit/bee9c7042f9e62a48f22c9ad2d505e8fc3c70c12))
183 | - **deps:** sanity 3.0.0-dev-preview.17 and ui 0.38 ([4bef2ab](https://github.com/sanity-io/code-input/commit/4bef2ab5505144da4a286c9481c12f85c61af11c))
184 |
185 | # [3.0.0-v3-studio.7](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.6...v3.0.0-v3-studio.7) (2022-08-17)
186 |
187 | ### Bug Fixes
188 |
189 | - makes PreviewCode also load ace async ([#11](https://github.com/sanity-io/code-input/issues/11)) ([16eb077](https://github.com/sanity-io/code-input/commit/16eb077901266b28038142408d2e74f2c20e5aec))
190 |
191 | # [3.0.0-v3-studio.6](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.5...v3.0.0-v3-studio.6) (2022-08-17)
192 |
193 | ### Bug Fixes
194 |
195 | - **deps:** update sanity packages to v1 (major) ([#10](https://github.com/sanity-io/code-input/issues/10)) ([260a911](https://github.com/sanity-io/code-input/commit/260a911fdf3715d5d73554488595209b1aad9e65))
196 |
197 | # [3.0.0-v3-studio.5](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.4...v3.0.0-v3-studio.5) (2022-08-17)
198 |
199 | ### Bug Fixes
200 |
201 | - use `@sanity/semantic-release-preset` ([fb0c785](https://github.com/sanity-io/code-input/commit/fb0c785b3d8b8e38cba16157452ea517ac23e05f))
202 |
203 | # [3.0.0-v3-studio.4](https://github.com/sanity-io/code-input/compare/v3.0.0-v3-studio.3...v3.0.0-v3-studio.4) (2022-08-16)
204 |
205 | ### Bug Fixes
206 |
207 | - setup changelog generator ([a52df38](https://github.com/sanity-io/code-input/commit/a52df3808aa8448b43b812f4b6ed3058b1f3f9bb))
208 | - update lockfile ([cf401bb](https://github.com/sanity-io/code-input/commit/cf401bbc18b921d2e7d780448aadcc9fc48cbe2a))
209 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Sanity.io
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @sanity/code-input
2 |
3 | > This is a **Sanity Studio v3** plugin.
4 | > For the v2 version, please refer to the [v2-branch](https://github.com/sanity-io/sanity/tree/next/packages/%40sanity/code-input).
5 |
6 | ## What is it?
7 |
8 | Code input for [Sanity](https://sanity.io/).
9 |
10 | A subset of languages and features are exposed by default. More can be added via the plugin options.
11 |
12 | 
13 |
14 | Click the line numbers to toggle line highlighting.
15 |
16 | ## Installation
17 |
18 | ```sh
19 | npm install @sanity/code-input
20 | ```
21 |
22 | ## Usage
23 |
24 | Add it as a plugin in `sanity.config.ts` (or .js):
25 |
26 | ```js
27 | import {codeInput} from '@sanity/code-input'
28 |
29 | export default defineConfig({
30 | // ...
31 | plugins: [codeInput()],
32 | })
33 | ```
34 |
35 | Now you can use the `code` type in your schema types:
36 |
37 | ```js
38 | import {defineType, defineField} from 'sanity'
39 |
40 | defineType({
41 | // [...]
42 | fields: [
43 | defineField({
44 | type: 'code',
45 | name: 'myCodeField',
46 | title: 'My code field',
47 | }),
48 | ],
49 | })
50 | ```
51 |
52 | ## Options
53 |
54 | - `language` - Default language for this code field.
55 | - `languageAlternatives` - Array of languages that should be available (se its format in the example below)
56 | - `withFilename` - Boolean option to display input field for filename
57 |
58 | ```js
59 | //...fields,
60 | defineField({
61 | type: 'code',
62 | name: 'myCodeField',
63 | title: 'Code with all options',
64 | options: {
65 | language: 'javascript',
66 | languageAlternatives: [
67 | {title: 'Javascript', value: 'javascript'},
68 | {title: 'HTML', value: 'html'},
69 | {title: 'CSS', value: 'css'},
70 | ],
71 | withFilename: true,
72 | },
73 | })
74 | ```
75 |
76 | 
77 |
78 | ## Add support for more languages
79 |
80 | Only a subset of languages are have syntax highlighting support by default (see full list [here](https://github.com/sanity-io/code-input/blob/main/src/codemirror/defaultCodeModes.ts)).
81 |
82 | ### Mode: Reuse an existing language
83 |
84 | Some languages are similar enough, that reusing one of the default highlighters will be "good enough".
85 | To reuse an existing language, specify mode for a value in `languageAlternatives`:
86 |
87 | ```js
88 | //...fields,
89 | defineField({
90 | name: 'zhOnly',
91 | type: 'code',
92 | options: {
93 | language: 'zh',
94 | languageAlternatives: [
95 | //Adds support for zh language, using sh syntax highlighting
96 | {title: 'ZH', value: 'zh', mode: 'sh'},
97 | ],
98 | },
99 | })
100 | ```
101 |
102 | ### Add more languages
103 |
104 | You can add support for additional languages, or override existing ones, by providing a `codeModes` array to the plugin.
105 | `codeModes` should be an array where each value is an object with a name and a loader function.
106 | The loader function should return a codemirror `Extension` or a `Promise` that resolves to `Extension`.
107 |
108 | The loader function will be invoked when the language is selected.
109 |
110 | For a full list of officialy code-mirror languages, see:
111 |
112 | ### Example: Add support for CodeMirror 6 language (Angular)
113 |
114 | We can add support for a [CodeMirror 6 lang package](https://github.com/orgs/codemirror/repositories?language=&q=lang-&sort=&type=all):
115 |
116 | ```js
117 | // sanity.config.js
118 |
119 | // ... in the plugins array of defineConfig, where we add the codeInput plugin
120 | codeInput({
121 | codeModes: [
122 | {
123 | name: 'angular',
124 | // dynamic import the angular package, and initialize the plugin after it is loaded
125 | // This way, the language is only when it is selected
126 | loader: () => import('@codemirror/lang-angular').then(({angular}) => angular()),
127 | },
128 | ],
129 | })
130 | ```
131 |
132 | ```js
133 | // in a code field, you can now use rust as a language as a value, or mode
134 | defineField({
135 | name: 'exampleRust',
136 | title: 'Example usage',
137 | type: 'code',
138 | options: {
139 | languageAlternatives: [
140 | {title: 'Javascript', value: 'javascript'},
141 | {title: 'Angular', value: 'angular'},
142 | {title: 'Angular-like', value: 'angular-like', mode: 'angular'}, // uses angular highlighter
143 | ],
144 | },
145 | })
146 | ```
147 |
148 | For this to work, you will have to run `npm i @codemirror/lang-angular` as this package is not included by @sanity/code-input.
149 |
150 | ### Example: Add support for CodeMirror 5 legacy language (Rust)
151 |
152 | We can add support for any [CodeMirror 5 legacy language](https://github.com/codemirror/legacy-modes/tree/main/mode) using
153 | [CodeMirror 6 StreamLanguage](https://codemirror.net/docs/ref/#language.StreamLanguage).
154 |
155 | ```js
156 | // sanity.config.js
157 | import {StreamLanguage} from '@codemirror/language'
158 |
159 | // ... in the plugins array of defineConfig, where we add the codeInput plugin
160 | codeInput({
161 | codeModes: [
162 | {
163 | name: 'rust',
164 | // dynamic import so the language is only be loaded on demand
165 | loader: () =>
166 | import('@codemirror/legacy-modes/mode/rust').then(({rust}) => StreamLanguage.define(rust)),
167 | },
168 | ],
169 | })
170 | ```
171 |
172 | ```js
173 | // in a code field, you can now use rust as a language as a value, or mode
174 | defineField({
175 | name: 'exampleRust',
176 | title: 'Example usage',
177 | type: 'code',
178 | options: {
179 | languageAlternatives: [
180 | {title: 'Javascript', value: 'javascript'},
181 | {title: 'Rust', value: 'rust'},
182 | {title: 'Rust-like', value: 'rust-like', mode: 'rust'}, // uses rust highlighter
183 | ],
184 | },
185 | })
186 | ```
187 |
188 | Note: `@sanity/code-input` already includes the `@codemirror/legacy-modes` and `@codemirror/language` dependencies,
189 | so no need to install them explicitly.
190 |
191 | ## Data model
192 |
193 | ```js
194 | {
195 | _type: 'code',
196 | language: 'js',
197 | highlightedLines: [1, 2],
198 | code: 'const foo = "bar"\nconsole.log(foo.toUpperCase())\n// BAR',
199 | filename: 'available when enabled'
200 | }
201 | ```
202 |
203 | ## Example usage in frontend (React)
204 |
205 | You can use any syntax highlighter you want - but not all of them might support highlighted lines or the syntax you've defined.
206 |
207 | As outlined above, the actual code is stored in a `code` property, so if your schema has a field called `codeExample` of type `code`, the property you'd want to pass to the highlighter would be `codeExample.code`.
208 |
209 | Here's an example using [react-refractor](https://github.com/rexxars/react-refractor):
210 |
211 | ```jsx
212 | import React from 'react'
213 | import Refractor from 'react-refractor'
214 | import js from 'refractor/lang/javascript'
215 |
216 | Refractor.registerLanguage(js)
217 |
218 | export function Code(props) {
219 | return (
220 |
226 | )
227 | }
228 | ```
229 |
230 | Other syntax highlighters include:
231 |
232 | - [react-lowlight](https://github.com/rexxars/react-lowlight)
233 | - [react-syntax-highlighter](https://github.com/react-syntax-highlighter/react-syntax-highlighter)
234 | - [highlight.js](https://github.com/highlightjs/highlight.js)
235 | - [prism](https://github.com/PrismJS/prism)
236 |
237 | ## License
238 |
239 | MIT-licensed. See LICENSE.
240 |
241 | ## Develop & test
242 |
243 | This plugin uses [@sanity/plugin-kit](https://github.com/sanity-io/plugin-kit)
244 | with default configuration for build & watch scripts.
245 |
246 | See [Testing a plugin in Sanity Studio](https://github.com/sanity-io/plugin-kit#testing-a-plugin-in-sanity-studio)
247 | on how to run this plugin with hotreload in the studio.
248 |
249 | ### UI Workshop
250 |
251 | Run
252 | `workshop dev`
253 |
254 | To test the CodeMirror lazy component.
255 |
256 | ### Release new version
257 |
258 | Run ["CI & Release" workflow](https://github.com/sanity-io/code-input/actions/workflows/main.yml).
259 | Make sure to select the main branch and check "Release new version".
260 |
261 | Semantic release will only release on configured branches, so it is safe to run release on any branch.
262 |
--------------------------------------------------------------------------------
/assets/all-options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanity-io/code-input/55a08f9740624d3e117242601b3190fe2fbd9190/assets/all-options.png
--------------------------------------------------------------------------------
/assets/basic-input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanity-io/code-input/55a08f9740624d3e117242601b3190fe2fbd9190/assets/basic-input.png
--------------------------------------------------------------------------------
/babel.config.cjs:
--------------------------------------------------------------------------------
1 | // used only by jest, .parcelrc configured to ignore this file
2 | module.exports = {
3 | presets: [
4 | '@babel/preset-env',
5 | [
6 | '@babel/preset-react',
7 | {
8 | runtime: 'automatic',
9 | },
10 | ],
11 | ],
12 | }
13 |
--------------------------------------------------------------------------------
/commitlint.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | }
4 |
--------------------------------------------------------------------------------
/jest.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2 | const crypto = require('crypto')
3 | module.exports = {
4 | preset: 'ts-jest',
5 | testEnvironment: 'node',
6 | testPathIgnorePatterns: ['.yalc', 'node_modules', '.idea', 'lib', '.parcel-cache'],
7 | globals: {
8 | crypto: {
9 | getRandomValues: (arr) => crypto.randomBytes(arr.length),
10 | },
11 | },
12 | transform: {
13 | '^.+\\.(ts|tsx)?$': ['ts-jest', {babelConfig: true}],
14 | '^.+\\.(mjs|js|jsx)$': 'babel-jest',
15 | },
16 | transformIgnorePatterns: ['node_modules/(?!(nanoid|uuid|get-random-values-esm))'],
17 | }
18 |
--------------------------------------------------------------------------------
/lint-staged.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | '**/*.{js,jsx}': ['eslint'],
3 | '**/*.{ts,tsx}': ['eslint', () => 'tsc --noEmit'],
4 | }
5 |
--------------------------------------------------------------------------------
/package.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from '@sanity/pkg-utils'
2 |
3 | export default defineConfig({
4 | legacyExports: true,
5 | dist: 'lib',
6 | tsconfig: 'tsconfig.lib.json',
7 |
8 | // Remove this block to enable strict export validation
9 | extract: {
10 | rules: {
11 | 'ae-forgotten-export': 'off',
12 | 'ae-incompatible-release-tags': 'off',
13 | 'ae-internal-missing-underscore': 'off',
14 | 'ae-missing-release-tag': 'off',
15 | },
16 | },
17 | })
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sanity/code-input",
3 | "version": "5.1.2",
4 | "description": "Sanity input component for code, powered by CodeMirror",
5 | "keywords": [
6 | "sanity",
7 | "cms",
8 | "headless",
9 | "realtime",
10 | "content",
11 | "code-input",
12 | "sanity-plugin",
13 | "code-editor"
14 | ],
15 | "homepage": "https://github.com/sanity-io/code-input#readme",
16 | "bugs": {
17 | "url": "https://github.com/sanity-io/code-input/issues"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git@github.com:sanity-io/code-input.git"
22 | },
23 | "license": "MIT",
24 | "author": "Sanity.io ",
25 | "type": "module",
26 | "exports": {
27 | ".": {
28 | "source": "./src/index.ts",
29 | "import": "./lib/index.js",
30 | "require": "./lib/index.cjs",
31 | "default": "./lib/index.js"
32 | },
33 | "./package.json": "./package.json"
34 | },
35 | "main": "./lib/index.cjs",
36 | "module": "./lib/index.esm.js",
37 | "types": "./lib/index.d.ts",
38 | "files": [
39 | "lib",
40 | "sanity.json",
41 | "src",
42 | "v2-incompatible.js"
43 | ],
44 | "sideEffects": false,
45 | "browserslist": "extends @sanity/browserslist-config",
46 | "scripts": {
47 | "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
48 | "clean": "rimraf lib",
49 | "compile": "tsc --noEmit",
50 | "dev": "sanity dev",
51 | "format": "prettier --write --cache --ignore-unknown .",
52 | "link-watch": "plugin-kit link-watch",
53 | "lint": "eslint .",
54 | "prepare": "husky install",
55 | "prepublishOnly": "npm run build",
56 | "test": "jest",
57 | "watch": "pkg-utils watch --strict"
58 | },
59 | "dependencies": {
60 | "@codemirror/autocomplete": "^6.18.3",
61 | "@codemirror/commands": "^6.7.1",
62 | "@codemirror/lang-html": "^6.4.9",
63 | "@codemirror/lang-java": "^6.0.1",
64 | "@codemirror/lang-javascript": "^6.2.2",
65 | "@codemirror/lang-json": "^6.0.1",
66 | "@codemirror/lang-markdown": "^6.3.1",
67 | "@codemirror/lang-php": "^6.0.1",
68 | "@codemirror/lang-sql": "^6.8.0",
69 | "@codemirror/language": "^6.10.6",
70 | "@codemirror/legacy-modes": "^6.4.2",
71 | "@codemirror/search": "^6.5.8",
72 | "@codemirror/state": "^6.5.0",
73 | "@codemirror/view": "^6.35.3",
74 | "@juggle/resize-observer": "^3.4.0",
75 | "@lezer/highlight": "^1.2.1",
76 | "@sanity/icons": "^3.5.2",
77 | "@sanity/incompatible-plugin": "^1.0.4",
78 | "@sanity/ui": "^2.10.9",
79 | "@uiw/codemirror-themes": "^4.23.6",
80 | "@uiw/react-codemirror": "^4.23.6"
81 | },
82 | "devDependencies": {
83 | "@babel/core": "^7.23.6",
84 | "@babel/preset-env": "^7.23.6",
85 | "@babel/preset-react": "^7.23.3",
86 | "@commitlint/cli": "^19.6.0",
87 | "@commitlint/config-conventional": "^19.6.0",
88 | "@sanity/pkg-utils": "^6.12.0",
89 | "@sanity/plugin-kit": "^4.0.18",
90 | "@sanity/semantic-release-preset": "^5.0.0",
91 | "@testing-library/jest-dom": "^6.1.5",
92 | "@testing-library/react": "^14.1.2",
93 | "@types/jest": "^29.5.11",
94 | "@types/react": "^18.2.45",
95 | "@types/styled-components": "^5.1.34",
96 | "@typescript-eslint/eslint-plugin": "^6.14.0",
97 | "@typescript-eslint/parser": "^6.14.0",
98 | "eslint": "^8.55.0",
99 | "eslint-config-prettier": "^9.1.0",
100 | "eslint-config-sanity": "^7.0.1",
101 | "eslint-plugin-prettier": "^5.0.1",
102 | "eslint-plugin-react": "^7.33.2",
103 | "eslint-plugin-react-hooks": "^4.6.0",
104 | "husky": "^8.0.1",
105 | "jest": "^29.7.0",
106 | "jest-environment-jsdom": "^29.7.0",
107 | "lint-staged": "^15.2.0",
108 | "npm-run-all": "^4.1.5",
109 | "prettier": "^3.1.1",
110 | "prettier-plugin-packagejson": "^2.4.7",
111 | "react": "^18.2.0",
112 | "react-dom": "^18.2.0",
113 | "react-is": "^18.2.0",
114 | "rimraf": "^5.0.5",
115 | "sanity": "^3.67.1",
116 | "semantic-release": "^24.2.0",
117 | "styled-components": "^6.1.1",
118 | "ts-jest": "^29.1.1",
119 | "typescript": "^5.3.3"
120 | },
121 | "peerDependencies": {
122 | "react": "^18 || >=19.0.0-0",
123 | "react-dom": "^18 || >=19.0.0-0",
124 | "sanity": "^3",
125 | "styled-components": "^5.2 || ^6"
126 | },
127 | "engines": {
128 | "node": ">=18"
129 | },
130 | "sanityExchangeUrl": "https://www.sanity.io/plugins/code-input",
131 | "sanityPlugin": {
132 | "verifyPackage": {
133 | "babelConfig": false
134 | }
135 | },
136 | "overrides": {
137 | "conventional-changelog-conventionalcommits": ">= 8.0.0"
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "github>sanity-io/renovate-config",
5 | "github>sanity-io/renovate-config:studio-v3",
6 | ":reviewer(team:ecosystem)"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/sanity.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'sanity'
2 | import {structureTool} from 'sanity/structure'
3 |
4 | import {codeInput} from './src/index'
5 |
6 | export default defineConfig({
7 | projectId: 'ppsg7ml5',
8 | dataset: 'test',
9 | plugins: [
10 | structureTool({
11 | structure: (S) => S.documentTypeList('codeTest'),
12 | }),
13 | codeInput(),
14 | ],
15 | schema: {
16 | types: [
17 | {
18 | name: 'codeTest',
19 | type: 'document',
20 | fields: [
21 | {name: 'title', type: 'string'},
22 | {name: 'code', type: 'code'},
23 | ],
24 | },
25 | ],
26 | },
27 | tasks: {
28 | enabled: false,
29 | },
30 | scheduledPublishing: {
31 | enabled: false,
32 | },
33 | announcements: {
34 | enabled: false,
35 | },
36 | beta: {
37 | create: {
38 | startInCreateEnabled: false,
39 | },
40 | },
41 | })
42 |
--------------------------------------------------------------------------------
/sanity.json:
--------------------------------------------------------------------------------
1 | {
2 | "parts": [
3 | {
4 | "implements": "part:@sanity/base/sanity-root",
5 | "path": "./v2-incompatible.js"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/src/CodeInput.tsx:
--------------------------------------------------------------------------------
1 | import {Box, Card, Stack, Text} from '@sanity/ui'
2 | import {Suspense, useCallback} from 'react'
3 | import {MemberField, ObjectInputProps, RenderInputCallback, set, setIfMissing, unset} from 'sanity'
4 | import styled, {css} from 'styled-components'
5 |
6 | import {useCodeMirror} from './codemirror/useCodeMirror'
7 | import {useLanguageMode} from './codemirror/useLanguageMode'
8 | import {PATH_CODE} from './config'
9 | import {LanguageField} from './LanguageField'
10 | import {CodeInputValue, CodeSchemaType} from './types'
11 | import {focusRingBorderStyle, focusRingStyle} from './ui/focusRingStyle'
12 | import {useFieldMember} from './useFieldMember'
13 |
14 | export type {CodeInputLanguage, CodeInputValue} from './types'
15 |
16 | /**
17 | * @public
18 | */
19 | export interface CodeInputProps extends ObjectInputProps {}
20 |
21 | const EditorContainer = styled(Card)(({theme}) => {
22 | const {focusRing, input} = theme.sanity
23 | const base = theme.sanity.color.base
24 | const color = theme.sanity.color.input
25 | const border = {
26 | color: color.default.enabled.border,
27 | width: input.border.width,
28 | }
29 |
30 | return css`
31 | --input-box-shadow: ${focusRingBorderStyle(border)};
32 |
33 | box-shadow: var(--input-box-shadow);
34 | height: 250px;
35 | min-height: 80px;
36 | overflow-y: auto;
37 | position: relative;
38 | resize: vertical;
39 | z-index: 0;
40 |
41 | & > .cm-theme {
42 | height: 100%;
43 | }
44 |
45 | &:focus-within {
46 | --input-box-shadow: ${focusRingStyle({
47 | base,
48 | border,
49 | focusRing,
50 | })};
51 | }
52 | `
53 | })
54 |
55 | /** @public */
56 | export function CodeInput(props: CodeInputProps) {
57 | const {
58 | members,
59 | elementProps,
60 | onChange,
61 | readOnly,
62 | renderField,
63 | renderInput,
64 | renderItem,
65 | renderPreview,
66 | schemaType: type,
67 | value,
68 | onPathFocus,
69 | } = props
70 |
71 | const languageFieldMember = useFieldMember(members, 'language')
72 | const filenameMember = useFieldMember(members, 'filename')
73 | const codeFieldMember = useFieldMember(members, 'code')
74 |
75 | const handleCodeFocus = useCallback(() => {
76 | onPathFocus(PATH_CODE)
77 | }, [onPathFocus])
78 |
79 | const onHighlightChange = useCallback(
80 | (lines: number[]) => onChange(set(lines, ['highlightedLines'])),
81 | [onChange],
82 | )
83 |
84 | const handleCodeChange = useCallback(
85 | (code: string) => {
86 | const path = PATH_CODE
87 | const fixedLanguage = type.options?.language
88 |
89 | onChange([
90 | setIfMissing({_type: type.name, language: fixedLanguage}),
91 | code ? set(code, path) : unset(path),
92 | ])
93 | },
94 | [onChange, type],
95 | )
96 | const {languages, language, languageMode} = useLanguageMode(props.schemaType, props.value)
97 |
98 | const CodeMirror = useCodeMirror()
99 |
100 | const renderCodeInput: RenderInputCallback = useCallback(
101 | (inputProps) => {
102 | return (
103 |
104 | {CodeMirror && (
105 |
108 | Loading code editor...
109 |
110 | }
111 | >
112 |
122 |
123 | )}
124 |
125 | )
126 | },
127 | [
128 | CodeMirror,
129 | handleCodeChange,
130 | handleCodeFocus,
131 | onHighlightChange,
132 | languageMode,
133 | elementProps.onBlur,
134 | readOnly,
135 | value,
136 | ],
137 | )
138 |
139 | return (
140 |
141 | {languageFieldMember && (
142 |
151 | )}
152 |
153 | {type.options?.withFilename && filenameMember && (
154 |
161 | )}
162 |
163 | {codeFieldMember && (
164 |
171 | )}
172 |
173 | )
174 | }
175 |
--------------------------------------------------------------------------------
/src/LanguageField.tsx:
--------------------------------------------------------------------------------
1 | import {useCallback} from 'react'
2 | import {
3 | FieldMember,
4 | type InputProps,
5 | MemberField,
6 | type MemberFieldProps,
7 | type PrimitiveInputElementProps,
8 | } from 'sanity'
9 |
10 | import {LanguageInput} from './LanguageInput'
11 | import type {CodeInputLanguage} from './types'
12 |
13 | export function LanguageField(
14 | props: MemberFieldProps & {member: FieldMember; language: string; languages: CodeInputLanguage[]},
15 | ) {
16 | const {member, languages, language, renderItem, renderField, renderPreview} = props
17 |
18 | const renderInput = useCallback(
19 | ({elementProps, onChange}: Omit) => {
20 | return (
21 |
27 | )
28 | },
29 | [languages, language],
30 | )
31 |
32 | return (
33 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/src/LanguageInput.tsx:
--------------------------------------------------------------------------------
1 | import {Select} from '@sanity/ui'
2 | import {type ChangeEvent, useCallback} from 'react'
3 | import {set, type StringInputProps, unset} from 'sanity'
4 |
5 | import {CodeInputLanguage} from './types'
6 |
7 | export interface LanguageInputProps {
8 | language: string
9 | languages: CodeInputLanguage[]
10 | onChange: StringInputProps['onChange']
11 | elementProps: StringInputProps['elementProps']
12 | }
13 |
14 | /** @internal */
15 | export function LanguageInput(props: LanguageInputProps) {
16 | const {language, languages, onChange, elementProps} = props
17 |
18 | const handleChange = useCallback(
19 | (e: ChangeEvent) => {
20 | const newValue = e.currentTarget.value
21 | onChange(newValue ? set(newValue) : unset())
22 | },
23 | [onChange],
24 | )
25 |
26 | return (
27 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/PreviewCode.tsx:
--------------------------------------------------------------------------------
1 | import {Box, Card, Flex, Label, Text} from '@sanity/ui'
2 | import {Suspense} from 'react'
3 | import {PreviewProps} from 'sanity'
4 | import styled from 'styled-components'
5 |
6 | import {useCodeMirror} from './codemirror/useCodeMirror'
7 | import {useLanguageMode} from './codemirror/useLanguageMode'
8 | import {CodeInputValue, CodeSchemaType} from './types'
9 |
10 | const PreviewContainer = styled(Box)`
11 | position: relative;
12 | `
13 |
14 | /**
15 | * @public
16 | */
17 | export interface PreviewCodeProps extends PreviewProps {
18 | selection?: CodeInputValue
19 | }
20 |
21 | /**
22 | * @public
23 | */
24 | export function PreviewCode(props: PreviewCodeProps) {
25 | const {selection, schemaType: type} = props
26 | const {languageMode} = useLanguageMode(type as CodeSchemaType, props.selection)
27 |
28 | const CodeMirror = useCodeMirror()
29 | return (
30 |
31 |
32 | {selection?.filename || selection?.language ? (
33 |
38 |
39 | {selection?.filename ? (
40 |
41 |
42 | {selection.filename}
43 |
44 |
45 | ) : null}
46 | {selection?.language ? : null}
47 |
48 |
49 | ) : null}
50 | {CodeMirror && (
51 | Loading code preview...}>
52 |
66 |
67 | )}
68 |
69 |
70 | )
71 | }
72 |
--------------------------------------------------------------------------------
/src/codemirror/CodeMirrorProxy.tsx:
--------------------------------------------------------------------------------
1 | import type {Extension} from '@codemirror/state'
2 | import {EditorView} from '@codemirror/view'
3 | import {useRootTheme} from '@sanity/ui'
4 | import CodeMirror, {type ReactCodeMirrorProps, type ReactCodeMirrorRef} from '@uiw/react-codemirror'
5 | import {forwardRef, useCallback, useContext, useEffect, useMemo, useState} from 'react'
6 |
7 | import {CodeInputConfigContext} from './CodeModeContext'
8 | import {defaultCodeModes} from './defaultCodeModes'
9 | import {
10 | highlightLine,
11 | highlightState,
12 | setHighlightedLines,
13 | } from './extensions/highlightLineExtension'
14 | import {useThemeExtension} from './extensions/theme'
15 | import {useCodeMirrorTheme} from './extensions/useCodeMirrorTheme'
16 | import {useFontSizeExtension} from './extensions/useFontSize'
17 |
18 | export interface CodeMirrorProps extends ReactCodeMirrorProps {
19 | highlightLines?: number[]
20 | languageMode?: string
21 | onHighlightChange?: (lines: number[]) => void
22 | }
23 |
24 | /**
25 | * CodeMirrorProxy is a wrapper component around CodeMirror that we lazy load to reduce initial bundle size.
26 | *
27 | * It is also responsible for integrating any CodeMirror extensions.
28 | */
29 | const CodeMirrorProxy = forwardRef(
30 | function CodeMirrorProxy(props, ref) {
31 | const {
32 | basicSetup: basicSetupProp,
33 | highlightLines,
34 | languageMode,
35 | onHighlightChange,
36 | readOnly,
37 | value,
38 | ...codeMirrorProps
39 | } = props
40 |
41 | const themeCtx = useRootTheme()
42 | const codeMirrorTheme = useCodeMirrorTheme()
43 | const [editorView, setEditorView] = useState(undefined)
44 |
45 | // Resolve extensions
46 | const themeExtension = useThemeExtension()
47 | const fontSizeExtension = useFontSizeExtension({fontSize: 1})
48 | const languageExtension = useLanguageExtension(languageMode)
49 | const highlightLineExtension = useMemo(
50 | () =>
51 | highlightLine({
52 | onHighlightChange,
53 | readOnly,
54 | theme: themeCtx,
55 | }),
56 | [onHighlightChange, readOnly, themeCtx],
57 | )
58 |
59 | const extensions = useMemo(() => {
60 | const baseExtensions = [
61 | themeExtension,
62 | fontSizeExtension,
63 | highlightLineExtension,
64 | EditorView.lineWrapping,
65 | ]
66 | if (languageExtension) {
67 | return [...baseExtensions, languageExtension]
68 | }
69 | return baseExtensions
70 | }, [fontSizeExtension, highlightLineExtension, languageExtension, themeExtension])
71 |
72 | useEffect(() => {
73 | if (editorView) {
74 | setHighlightedLines(editorView, highlightLines ?? [])
75 | }
76 | }, [editorView, highlightLines, value])
77 |
78 | const initialState = useMemo(() => {
79 | return {
80 | json: {
81 | doc: value ?? '',
82 | selection: {
83 | main: 0,
84 | ranges: [{anchor: 0, head: 0}],
85 | },
86 | highlight: highlightLines ?? [],
87 | },
88 | fields: highlightState,
89 | }
90 | // only need to calculate this on initial render
91 | // eslint-disable-next-line react-hooks/exhaustive-deps
92 | }, [])
93 |
94 | const handleCreateEditor = useCallback((view: EditorView) => {
95 | setEditorView(view)
96 | }, [])
97 |
98 | const basicSetup = useMemo(
99 | () =>
100 | basicSetupProp ?? {
101 | highlightActiveLine: false,
102 | },
103 | [basicSetupProp],
104 | )
105 |
106 | return (
107 |
117 | )
118 | },
119 | )
120 |
121 | function useLanguageExtension(mode?: string) {
122 | const codeConfig = useContext(CodeInputConfigContext)
123 |
124 | const [languageExtension, setLanguageExtension] = useState()
125 |
126 | useEffect(() => {
127 | const customModes = codeConfig?.codeModes ?? []
128 | const modes = [...customModes, ...defaultCodeModes]
129 |
130 | const codeMode = modes.find((m) => m.name === mode)
131 | if (!codeMode?.loader) {
132 | console.warn(
133 | `Found no codeMode for language mode ${mode}, syntax highlighting will be disabled.`,
134 | )
135 | }
136 | let active = true
137 | Promise.resolve(codeMode?.loader())
138 | .then((extension) => {
139 | if (active) {
140 | setLanguageExtension(extension)
141 | }
142 | })
143 | .catch((e) => {
144 | console.error(`Failed to load language mode ${mode}`, e)
145 | if (active) {
146 | setLanguageExtension(undefined)
147 | }
148 | })
149 | return () => {
150 | active = false
151 | }
152 | }, [mode, codeConfig])
153 |
154 | return languageExtension
155 | }
156 |
157 | export default CodeMirrorProxy
158 |
--------------------------------------------------------------------------------
/src/codemirror/CodeModeContext.tsx:
--------------------------------------------------------------------------------
1 | import {createContext} from 'react'
2 |
3 | import type {CodeInputConfig} from '../plugin'
4 |
5 | export const CodeInputConfigContext = createContext(undefined)
6 |
--------------------------------------------------------------------------------
/src/codemirror/defaultCodeModes.ts:
--------------------------------------------------------------------------------
1 | import {StreamLanguage} from '@codemirror/language'
2 | import type {Extension} from '@codemirror/state'
3 |
4 | export interface CodeMode {
5 | name: string
6 | loader: ModeLoader
7 | }
8 | export type ModeLoader = () => Promise | Extension | undefined
9 |
10 | export const defaultCodeModes: CodeMode[] = [
11 | {
12 | name: 'groq',
13 | loader: () =>
14 | import('@codemirror/lang-javascript').then(({javascriptLanguage}) => javascriptLanguage),
15 | },
16 | {
17 | name: 'javascript',
18 | loader: () =>
19 | import('@codemirror/lang-javascript').then(({javascript}) => javascript({jsx: false})),
20 | },
21 | {
22 | name: 'jsx',
23 | loader: () =>
24 | import('@codemirror/lang-javascript').then(({javascript}) => javascript({jsx: true})),
25 | },
26 | {
27 | name: 'typescript',
28 | loader: () =>
29 | import('@codemirror/lang-javascript').then(({javascript}) =>
30 | javascript({jsx: false, typescript: true}),
31 | ),
32 | },
33 | {
34 | name: 'tsx',
35 | loader: () =>
36 | import('@codemirror/lang-javascript').then(({javascript}) =>
37 | javascript({jsx: true, typescript: true}),
38 | ),
39 | },
40 | {name: 'php', loader: () => import('@codemirror/lang-php').then(({php}) => php())},
41 | {name: 'sql', loader: () => import('@codemirror/lang-sql').then(({sql}) => sql())},
42 | {
43 | name: 'mysql',
44 | loader: () => import('@codemirror/lang-sql').then(({sql, MySQL}) => sql({dialect: MySQL})),
45 | },
46 | {name: 'json', loader: () => import('@codemirror/lang-json').then(({json}) => json())},
47 | {
48 | name: 'markdown',
49 | loader: () => import('@codemirror/lang-markdown').then(({markdown}) => markdown()),
50 | },
51 | {name: 'java', loader: () => import('@codemirror/lang-java').then(({java}) => java())},
52 | {name: 'html', loader: () => import('@codemirror/lang-html').then(({html}) => html())},
53 | {
54 | name: 'csharp',
55 | loader: () =>
56 | import('@codemirror/legacy-modes/mode/clike').then(({csharp}) =>
57 | StreamLanguage.define(csharp),
58 | ),
59 | },
60 | {
61 | name: 'sh',
62 | loader: () =>
63 | import('@codemirror/legacy-modes/mode/shell').then(({shell}) => StreamLanguage.define(shell)),
64 | },
65 | {
66 | name: 'css',
67 | loader: () =>
68 | import('@codemirror/legacy-modes/mode/css').then(({css}) => StreamLanguage.define(css)),
69 | },
70 | {
71 | name: 'scss',
72 | loader: () =>
73 | import('@codemirror/legacy-modes/mode/css').then(({css}) => StreamLanguage.define(css)),
74 | },
75 | {
76 | name: 'sass',
77 | loader: () =>
78 | import('@codemirror/legacy-modes/mode/sass').then(({sass}) => StreamLanguage.define(sass)),
79 | },
80 | {
81 | name: 'ruby',
82 | loader: () =>
83 | import('@codemirror/legacy-modes/mode/ruby').then(({ruby}) => StreamLanguage.define(ruby)),
84 | },
85 | {
86 | name: 'python',
87 | loader: () =>
88 | import('@codemirror/legacy-modes/mode/python').then(({python}) =>
89 | StreamLanguage.define(python),
90 | ),
91 | },
92 | {
93 | name: 'xml',
94 | loader: () =>
95 | import('@codemirror/legacy-modes/mode/xml').then(({xml}) => StreamLanguage.define(xml)),
96 | },
97 | {
98 | name: 'yaml',
99 | loader: () =>
100 | import('@codemirror/legacy-modes/mode/yaml').then(({yaml}) => StreamLanguage.define(yaml)),
101 | },
102 | {
103 | name: 'golang',
104 | loader: () =>
105 | import('@codemirror/legacy-modes/mode/go').then(({go}) => StreamLanguage.define(go)),
106 | },
107 | {name: 'text', loader: () => undefined},
108 | {name: 'batch', loader: () => undefined},
109 | ]
110 |
--------------------------------------------------------------------------------
/src/codemirror/extensions/backwardsCompatibleTone.ts:
--------------------------------------------------------------------------------
1 | import type {ThemeContextValue} from '@sanity/ui'
2 |
3 | /**
4 | * `@sanity/ui@v2.9` introduced two new tones; "neutral" and "suggest",
5 | * which maps to "default" and "primary" respectively in the old theme.
6 | * This function returns the "backwards compatible" tone value.
7 | *
8 | * @returns The tone value that is backwards compatible with the old theme.
9 | * @internal
10 | */
11 | export function getBackwardsCompatibleTone(
12 | themeCtx: ThemeContextValue,
13 | ): Exclude {
14 | if (themeCtx.tone !== 'neutral' && themeCtx.tone !== 'suggest') {
15 | return themeCtx.tone
16 | }
17 |
18 | return themeCtx.tone === 'neutral' ? 'default' : 'primary'
19 | }
20 |
--------------------------------------------------------------------------------
/src/codemirror/extensions/highlightLineExtension.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 |
3 | import {type Extension, StateEffect, StateField} from '@codemirror/state'
4 | import {Decoration, type DecorationSet, EditorView, lineNumbers} from '@codemirror/view'
5 | import type {ThemeContextValue} from '@sanity/ui'
6 | import {rgba} from '@sanity/ui/theme'
7 |
8 | import {getBackwardsCompatibleTone} from './backwardsCompatibleTone'
9 |
10 | const highlightLineClass = 'cm-highlight-line'
11 |
12 | export const addLineHighlight = StateEffect.define()
13 | export const removeLineHighlight = StateEffect.define()
14 |
15 | export const lineHighlightField = StateField.define({
16 | create() {
17 | return Decoration.none
18 | },
19 | update(lines, tr) {
20 | lines = lines.map(tr.changes)
21 | for (const e of tr.effects) {
22 | if (e.is(addLineHighlight)) {
23 | lines = lines.update({add: [lineHighlightMark.range(e.value)]})
24 | }
25 | if (e.is(removeLineHighlight)) {
26 | lines = lines.update({
27 | filter: (from) => {
28 | // removeLineHighlight value is lineStart for the highlight, so keep other effects
29 | return from !== e.value
30 | },
31 | })
32 | }
33 | }
34 | return lines
35 | },
36 | toJSON(value, state) {
37 | const highlightLines: number[] = []
38 | const iter = value.iter()
39 | while (iter.value) {
40 | const lineNumber = state.doc.lineAt(iter.from).number
41 | if (!highlightLines.includes(lineNumber)) {
42 | highlightLines.push(lineNumber)
43 | }
44 | iter.next()
45 | }
46 | return highlightLines
47 | },
48 | fromJSON(value: number[], state) {
49 | const lines = state.doc.lines
50 | const highlights = value
51 | .filter((line) => line <= lines) // one-indexed
52 | .map((line) => lineHighlightMark.range(state.doc.line(line).from))
53 | highlights.sort((a, b) => a.from - b.from)
54 | try {
55 | return Decoration.none.update({
56 | add: highlights,
57 | })
58 | } catch (e) {
59 | console.error(e)
60 | return Decoration.none
61 | }
62 | },
63 | provide: (f) => EditorView.decorations.from(f),
64 | })
65 |
66 | const lineHighlightMark = Decoration.line({
67 | class: highlightLineClass,
68 | })
69 |
70 | export const highlightState: {
71 | [prop: string]: StateField
72 | } = {
73 | highlight: lineHighlightField,
74 | }
75 |
76 | export interface HighlightLineConfig {
77 | onHighlightChange?: (lines: number[]) => void
78 | readOnly?: boolean
79 | theme: ThemeContextValue
80 | }
81 |
82 | function createCodeMirrorTheme(options: {themeCtx: ThemeContextValue}) {
83 | const {themeCtx} = options
84 |
85 | const fallbackTone = getBackwardsCompatibleTone(themeCtx)
86 |
87 | const dark = {color: themeCtx.theme.color.dark[fallbackTone]}
88 | const light = {color: themeCtx.theme.color.light[fallbackTone]}
89 |
90 | return EditorView.baseTheme({
91 | '.cm-lineNumbers': {
92 | cursor: 'default',
93 | },
94 | '.cm-line.cm-line': {
95 | position: 'relative',
96 | },
97 |
98 | // need set background with pseudoelement so it does not render over selection color
99 | [`.${highlightLineClass}::before`]: {
100 | position: 'absolute',
101 | top: 0,
102 | bottom: 0,
103 | left: 0,
104 | right: 0,
105 | zIndex: -3,
106 | content: "''",
107 | boxSizing: 'border-box',
108 | },
109 | [`&dark .${highlightLineClass}::before`]: {
110 | background: rgba(dark.color.muted.caution.pressed.bg, 0.5),
111 | },
112 | [`&light .${highlightLineClass}::before`]: {
113 | background: rgba(light.color.muted.caution.pressed.bg, 0.75),
114 | },
115 | })
116 | }
117 |
118 | export const highlightLine = (config: HighlightLineConfig): Extension => {
119 | const highlightTheme = createCodeMirrorTheme({themeCtx: config.theme})
120 |
121 | return [
122 | lineHighlightField,
123 | config.readOnly
124 | ? []
125 | : lineNumbers({
126 | domEventHandlers: {
127 | mousedown: (editorView, lineInfo) => {
128 | // Determine if the line for the clicked gutter line number has highlighted state or not
129 | const line = editorView.state.doc.lineAt(lineInfo.from)
130 | let isHighlighted = false
131 | editorView.state
132 | .field(lineHighlightField)
133 | .between(line.from, line.to, (from, to, value) => {
134 | if (value) {
135 | isHighlighted = true
136 | return false // stop iteration
137 | }
138 | return undefined
139 | })
140 |
141 | if (isHighlighted) {
142 | editorView.dispatch({effects: removeLineHighlight.of(line.from)})
143 | } else {
144 | editorView.dispatch({effects: addLineHighlight.of(line.from)})
145 | }
146 | if (config?.onHighlightChange) {
147 | config.onHighlightChange(editorView.state.toJSON(highlightState).highlight)
148 | }
149 | return true
150 | },
151 | },
152 | }),
153 | highlightTheme,
154 | ]
155 | }
156 |
157 | /**
158 | * Adds and removes highlights to the provided view using highlightLines
159 | * @param view
160 | * @param highlightLines
161 | */
162 | export function setHighlightedLines(view: EditorView, highlightLines: number[]): void {
163 | const doc = view.state.doc
164 | const lines = doc.lines
165 | //1-based line numbers
166 | const allLineNumbers = Array.from({length: lines}, (x, i) => i + 1)
167 | view.dispatch({
168 | effects: allLineNumbers.map((lineNumber) => {
169 | const line = doc.line(lineNumber)
170 | if (highlightLines?.includes(lineNumber)) {
171 | return addLineHighlight.of(line.from)
172 | }
173 | return removeLineHighlight.of(line.from)
174 | }),
175 | })
176 | }
177 |
--------------------------------------------------------------------------------
/src/codemirror/extensions/theme.ts:
--------------------------------------------------------------------------------
1 | import type {Extension} from '@codemirror/state'
2 | import {EditorView} from '@codemirror/view'
3 | import {useRootTheme} from '@sanity/ui'
4 | import {rgba} from '@sanity/ui/theme'
5 | import {useMemo} from 'react'
6 |
7 | import {getBackwardsCompatibleTone} from './backwardsCompatibleTone'
8 |
9 | export function useThemeExtension(): Extension {
10 | const themeCtx = useRootTheme()
11 |
12 | return useMemo(() => {
13 | const fallbackTone = getBackwardsCompatibleTone(themeCtx)
14 | const dark = {color: themeCtx.theme.color.dark[fallbackTone]}
15 | const light = {color: themeCtx.theme.color.light[fallbackTone]}
16 |
17 | return EditorView.baseTheme({
18 | '&.cm-editor': {
19 | height: '100%',
20 | },
21 | '&.cm-editor.cm-focused': {
22 | outline: 'none',
23 | },
24 |
25 | // Matching brackets
26 | '&.cm-editor.cm-focused .cm-matchingBracket': {
27 | backgroundColor: 'transparent',
28 | },
29 | '&.cm-editor.cm-focused .cm-nonmatchingBracket': {
30 | backgroundColor: 'transparent',
31 | },
32 | '&dark.cm-editor.cm-focused .cm-matchingBracket': {
33 | outline: `1px solid ${dark.color.base.border}`,
34 | },
35 | '&dark.cm-editor.cm-focused .cm-nonmatchingBracket': {
36 | outline: `1px solid ${dark.color.base.border}`,
37 | },
38 | '&light.cm-editor.cm-focused .cm-matchingBracket': {
39 | outline: `1px solid ${light.color.base.border}`,
40 | },
41 | '&light.cm-editor.cm-focused .cm-nonmatchingBracket': {
42 | outline: `1px solid ${light.color.base.border}`,
43 | },
44 |
45 | // Size and padding of gutter
46 | '& .cm-lineNumbers .cm-gutterElement': {
47 | minWidth: `32px !important`,
48 | padding: `0 8px !important`,
49 | },
50 | '& .cm-gutter.cm-foldGutter': {
51 | width: `0px !important`,
52 | },
53 |
54 | // Color of gutter
55 | '&dark .cm-gutters': {
56 | color: `${rgba(dark.color.card.enabled.code.fg, 0.5)} !important`,
57 | borderRight: `1px solid ${rgba(dark.color.base.border, 0.5)}`,
58 | },
59 | '&light .cm-gutters': {
60 | color: `${rgba(light.color.card.enabled.code.fg, 0.5)} !important`,
61 | borderRight: `1px solid ${rgba(light.color.base.border, 0.5)}`,
62 | },
63 | })
64 | }, [themeCtx])
65 | }
66 |
--------------------------------------------------------------------------------
/src/codemirror/extensions/useCodeMirrorTheme.ts:
--------------------------------------------------------------------------------
1 | import type {Extension} from '@codemirror/state'
2 | import {tags as t} from '@lezer/highlight'
3 | import {useTheme} from '@sanity/ui'
4 | import {rgba} from '@sanity/ui/theme'
5 | import {createTheme} from '@uiw/codemirror-themes'
6 | import {useMemo} from 'react'
7 |
8 | export function useCodeMirrorTheme(): Extension {
9 | const theme = useTheme()
10 |
11 | return useMemo(() => {
12 | const {code: codeFont} = theme.sanity.fonts
13 | const {base, card, dark, syntax} = theme.sanity.color
14 |
15 | return createTheme({
16 | theme: dark ? 'dark' : 'light',
17 | settings: {
18 | background: card.enabled.bg,
19 | foreground: card.enabled.code.fg,
20 | lineHighlight: card.enabled.bg,
21 | fontFamily: codeFont.family,
22 | caret: base.focusRing,
23 | selection: rgba(base.focusRing, 0.2),
24 | selectionMatch: rgba(base.focusRing, 0.4),
25 | gutterBackground: card.disabled.bg,
26 | gutterForeground: card.disabled.code.fg,
27 | gutterActiveForeground: card.enabled.fg,
28 | },
29 | styles: [
30 | {
31 | tag: [t.heading, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6],
32 | color: card.enabled.fg,
33 | },
34 | {tag: t.angleBracket, color: card.enabled.code.fg},
35 | {tag: t.atom, color: syntax.keyword},
36 | {tag: t.attributeName, color: syntax.attrName},
37 | {tag: t.bool, color: syntax.boolean},
38 | {tag: t.bracket, color: card.enabled.code.fg},
39 | {tag: t.className, color: syntax.className},
40 | {tag: t.comment, color: syntax.comment},
41 | {tag: t.definition(t.typeName), color: syntax.function},
42 | {
43 | tag: [
44 | t.definition(t.variableName),
45 | t.function(t.variableName),
46 | t.className,
47 | t.attributeName,
48 | ],
49 | color: syntax.function,
50 | },
51 | {tag: [t.function(t.propertyName), t.propertyName], color: syntax.function},
52 | {tag: t.keyword, color: syntax.keyword},
53 | {tag: t.null, color: syntax.number},
54 | {tag: t.number, color: syntax.number},
55 | {tag: t.meta, color: card.enabled.code.fg},
56 | {tag: t.operator, color: syntax.operator},
57 | {tag: t.propertyName, color: syntax.property},
58 | {tag: [t.string, t.special(t.brace)], color: syntax.string},
59 | {tag: t.tagName, color: syntax.className},
60 | {tag: t.typeName, color: syntax.keyword},
61 | ],
62 | })
63 | }, [theme])
64 | }
65 |
--------------------------------------------------------------------------------
/src/codemirror/extensions/useFontSize.ts:
--------------------------------------------------------------------------------
1 | import type {Extension} from '@codemirror/state'
2 | import {EditorView} from '@codemirror/view'
3 | import {rem, useTheme} from '@sanity/ui'
4 | import {useMemo} from 'react'
5 |
6 | export function useFontSizeExtension(props: {fontSize: number}): Extension {
7 | const {fontSize: fontSizeProp} = props
8 | const theme = useTheme()
9 |
10 | return useMemo(() => {
11 | const {code: codeFont} = theme.sanity.fonts
12 | const {fontSize, lineHeight} = codeFont.sizes[fontSizeProp] || codeFont.sizes[2]
13 |
14 | return EditorView.baseTheme({
15 | '&': {
16 | fontSize: rem(fontSize),
17 | },
18 |
19 | '& .cm-scroller': {
20 | lineHeight: `${lineHeight / fontSize} !important`,
21 | },
22 | })
23 | }, [fontSizeProp, theme])
24 | }
25 |
--------------------------------------------------------------------------------
/src/codemirror/useCodeMirror-client.test.tsx:
--------------------------------------------------------------------------------
1 | /** @jest-environment jsdom */
2 |
3 | import {studioTheme, ThemeProvider} from '@sanity/ui'
4 | import {act, render} from '@testing-library/react'
5 | import {Suspense} from 'react'
6 |
7 | import {useCodeMirror} from './useCodeMirror'
8 |
9 | describe('useCodeMirror - client', () => {
10 | let rafMock: jest.SpyInstance
11 |
12 | beforeEach(() => {
13 | rafMock = jest
14 | .spyOn(window, 'requestAnimationFrame')
15 | .mockImplementation((callback: FrameRequestCallback): number => {
16 | try {
17 | // eslint-disable-next-line callback-return
18 | callback(0)
19 | } catch (e) {
20 | // CodeMirror does some mesurement shenanigance that json dont support
21 | // we just let it crash silently
22 | }
23 | return 0
24 | })
25 | })
26 |
27 | afterEach(() => {
28 | rafMock.mockRestore()
29 | })
30 |
31 | it('should render suspended codemirror editor', async () => {
32 | const TestComponent = () => {
33 | const CodeMirror = useCodeMirror()
34 | return (
35 |
36 | {CodeMirror && (
37 |
38 |
39 |
40 | )}
41 |
42 | )
43 | }
44 | let container: HTMLElement | undefined
45 | await act(async () => {
46 | const result = render()
47 | container = result.container
48 | })
49 | expect(container).toBeTruthy()
50 | expect(container!.querySelector('.cm-theme')).toBeTruthy()
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/src/codemirror/useCodeMirror-server.test.tsx:
--------------------------------------------------------------------------------
1 | import {renderToString} from 'react-dom/server'
2 |
3 | import {useCodeMirror} from './useCodeMirror'
4 |
5 | describe('useCodeMirror - server', () => {
6 | it('should render null to string (and not throw and Error)', () => {
7 | const TestComponent = () => {
8 | const Editor = useCodeMirror()
9 | if (!Editor) {
10 | return null
11 | }
12 | throw new Error('editor should always be null in envs without window')
13 | }
14 | const serverString = renderToString()
15 |
16 | expect(serverString).toEqual('')
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/src/codemirror/useCodeMirror.tsx:
--------------------------------------------------------------------------------
1 | import {lazy, useEffect, useState} from 'react'
2 |
3 | export const CodeMirrorProxy = lazy(() => import('./CodeMirrorProxy'))
4 |
5 | export function useCodeMirror() {
6 | const [mounted, setMounted] = useState(false)
7 | useEffect(() => {
8 | requestAnimationFrame(() => setMounted(true))
9 | }, [])
10 |
11 | return mounted ? CodeMirrorProxy : null
12 | }
13 |
--------------------------------------------------------------------------------
/src/codemirror/useLanguageMode.tsx:
--------------------------------------------------------------------------------
1 | import {useMemo} from 'react'
2 |
3 | import {LANGUAGE_ALIASES, SUPPORTED_LANGUAGES} from '../config'
4 | import type {CodeInputLanguage, CodeInputValue, CodeSchemaType} from '../types'
5 |
6 | export const defaultLanguageMode = 'text'
7 |
8 | export function useLanguageMode(
9 | schemaType: CodeSchemaType,
10 | value?: CodeInputValue,
11 | ): {
12 | language: string
13 | languageMode: string
14 | languages: CodeInputLanguage[]
15 | } {
16 | const languages = useLanguageAlternatives(schemaType)
17 | const fixedLanguage = schemaType.options?.language
18 | const language = value?.language ?? fixedLanguage ?? defaultLanguageMode
19 |
20 | // the language config from the schema
21 | const configured = languages.find((entry) => entry.value === language)
22 | const languageMode = configured?.mode ?? resolveAliasedLanguage(language) ?? defaultLanguageMode
23 |
24 | return {language, languageMode, languages}
25 | }
26 |
27 | function resolveAliasedLanguage(lang?: string) {
28 | return (lang && LANGUAGE_ALIASES[lang]) ?? lang
29 | }
30 |
31 | function useLanguageAlternatives(type: CodeSchemaType) {
32 | return useMemo((): CodeInputLanguage[] => {
33 | const languageAlternatives = type.options?.languageAlternatives
34 | if (!languageAlternatives) {
35 | return SUPPORTED_LANGUAGES
36 | }
37 |
38 | if (!Array.isArray(languageAlternatives)) {
39 | throw new Error(
40 | `'options.languageAlternatives' should be an array, got ${typeof languageAlternatives}`,
41 | )
42 | }
43 |
44 | return languageAlternatives.reduce((acc: CodeInputLanguage[], {title, value: val, mode}) => {
45 | const alias = LANGUAGE_ALIASES[val]
46 | if (alias) {
47 | // eslint-disable-next-line no-console
48 | console.warn(
49 | `'options.languageAlternatives' lists a language with value "%s", which is an alias of "%s" - please replace the value to read "%s"`,
50 | val,
51 | alias,
52 | alias,
53 | )
54 |
55 | return acc.concat({title, value: alias, mode: mode})
56 | }
57 | return acc.concat({title, value: val, mode})
58 | }, [])
59 | }, [type])
60 | }
61 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import {CodeInputLanguage} from './types'
2 |
3 | // NOTE: MAKE SURE THESE ALIGN WITH CODE MODES IN ./codemirror/defaultCodeModes.ts
4 | export const SUPPORTED_LANGUAGES: CodeInputLanguage[] = [
5 | {title: 'Batch file', value: 'batchfile'},
6 | {title: 'C#', value: 'csharp'},
7 | {title: 'CSS', value: 'css'},
8 | {title: 'Go', value: 'golang'},
9 | {title: 'GROQ', value: 'groq'},
10 | {title: 'HTML', value: 'html'},
11 | {title: 'Java', value: 'java'},
12 | {title: 'JavaScript', value: 'javascript'},
13 | {title: 'JSON', value: 'json'},
14 | {title: 'JSX', value: 'jsx'},
15 | {title: 'Markdown', value: 'markdown'},
16 | {title: 'MySQL', value: 'mysql'},
17 | {title: 'PHP', value: 'php'},
18 | {title: 'Plain text', value: 'text'},
19 | {title: 'Python', value: 'python'},
20 | {title: 'Ruby', value: 'ruby'},
21 | {title: 'SASS', value: 'sass'},
22 | {title: 'SCSS', value: 'scss'},
23 | {title: 'sh', value: 'sh'},
24 | {title: 'TSX', value: 'tsx'},
25 | {title: 'TypeScript', value: 'typescript'},
26 | {title: 'XML', value: 'xml'},
27 | {title: 'YAML', value: 'yaml'},
28 | ]
29 |
30 | export const LANGUAGE_ALIASES: Record = {js: 'javascript'}
31 |
32 | export const PATH_LANGUAGE = ['language']
33 | export const PATH_CODE = ['code']
34 | export const PATH_FILENAME = ['filename']
35 |
--------------------------------------------------------------------------------
/src/getMedia.tsx:
--------------------------------------------------------------------------------
1 | export function getMedia(language?: string) {
2 | if (language === 'jsx') {
3 | return (
4 |
10 | )
11 | }
12 |
13 | if (language === 'javascript') {
14 | return (
15 |
22 | )
23 | }
24 |
25 | if (language === 'php') {
26 | return (
27 |
36 | )
37 | }
38 |
39 | if (language === 'json') {
40 | return (
41 |
89 | )
90 | }
91 |
92 | return undefined
93 | }
94 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {CodeDefinition, codeSchema, codeTypeName} from './schema'
2 | export {type CodeInput, type CodeInputProps} from './CodeInput'
3 | export {PreviewCode, type PreviewCodeProps} from './PreviewCode'
4 | export type {CodeInputLanguage, CodeInputValue, CodeOptions, CodeSchemaType} from './types'
5 | export {codeSchema, codeTypeName}
6 | export type {CodeDefinition}
7 |
8 | export {codeInput} from './plugin'
9 |
--------------------------------------------------------------------------------
/src/plugin.tsx:
--------------------------------------------------------------------------------
1 | import {definePlugin} from 'sanity'
2 |
3 | import {CodeInputConfigContext} from './codemirror/CodeModeContext'
4 | import {CodeMode} from './codemirror/defaultCodeModes'
5 | import {codeSchema} from './schema'
6 |
7 | export interface CodeInputConfig {
8 | codeModes?: CodeMode[]
9 | }
10 |
11 | /**
12 | * @public
13 | */
14 | export const codeInput = definePlugin((config) => {
15 | const codeModes = config && config.codeModes
16 | const basePlugin = {
17 | name: '@sanity/code-input',
18 | schema: {types: [codeSchema]},
19 | }
20 | if (!codeModes) {
21 | return basePlugin
22 | }
23 | return {
24 | ...basePlugin,
25 | form: {
26 | components: {
27 | input: (props) => {
28 | if (props.id !== 'root') {
29 | return props.renderDefault(props)
30 | }
31 | return (
32 |
33 | {props.renderDefault(props)}
34 |
35 | )
36 | },
37 | },
38 | },
39 | }
40 | })
41 |
--------------------------------------------------------------------------------
/src/schema.tsx:
--------------------------------------------------------------------------------
1 | import {CodeBlockIcon} from '@sanity/icons'
2 | import {defineType, ObjectDefinition} from 'sanity'
3 |
4 | import {CodeInput} from './CodeInput'
5 | import {getMedia} from './getMedia'
6 | import {PreviewCode} from './PreviewCode'
7 | import {CodeOptions} from './types'
8 |
9 | /**
10 | * @public
11 | */
12 | export const codeTypeName = 'code' as const
13 |
14 | /**
15 | * @public
16 | */
17 | export interface CodeDefinition extends Omit {
18 | type: typeof codeTypeName
19 | options?: CodeOptions
20 | }
21 |
22 | declare module '@sanity/types' {
23 | // makes type: 'code' narrow correctly when using defineType/defineField/defineArrayMember
24 | export interface IntrinsicDefinitions {
25 | code: CodeDefinition
26 | }
27 | }
28 |
29 | /**
30 | * @public
31 | */
32 | export const codeSchema = defineType({
33 | name: 'code',
34 | type: 'object',
35 | title: 'Code',
36 | components: {input: CodeInput, preview: PreviewCode},
37 | icon: CodeBlockIcon,
38 | fields: [
39 | {
40 | name: 'language',
41 | title: 'Language',
42 | type: 'string',
43 | },
44 | {
45 | name: 'filename',
46 | title: 'Filename',
47 | type: 'string',
48 | },
49 | {
50 | title: 'Code',
51 | name: 'code',
52 | type: 'text',
53 | },
54 | {
55 | title: 'Highlighted lines',
56 | name: 'highlightedLines',
57 | type: 'array',
58 | of: [
59 | {
60 | type: 'number',
61 | title: 'Highlighted line',
62 | },
63 | ],
64 | },
65 | ],
66 | preview: {
67 | select: {
68 | language: 'language',
69 | code: 'code',
70 | filename: 'filename',
71 | highlightedLines: 'highlightedLines',
72 | },
73 | prepare: (value: {
74 | language?: string
75 | code?: string
76 | filename?: string
77 | highlightedLines?: number[]
78 | }) => {
79 | return {
80 | title: value.filename || (value.language || 'unknown').toUpperCase(),
81 | media: getMedia(value?.language),
82 | selection: value,
83 | }
84 | },
85 | },
86 | })
87 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import {ObjectSchemaType} from 'sanity'
2 |
3 | export interface CodeInputLanguage {
4 | title: string
5 | value: string
6 | mode?: string
7 | }
8 |
9 | /**
10 | * @public
11 | */
12 | export interface CodeInputValue {
13 | _type?: 'code'
14 | code?: string
15 | filename?: string
16 | language?: string
17 | highlightedLines?: number[]
18 | }
19 | /**
20 | * @public
21 | */
22 | export interface CodeOptions {
23 | theme?: string
24 | darkTheme?: string
25 | languageAlternatives?: CodeInputLanguage[]
26 | language?: string
27 | withFilename?: boolean
28 | }
29 |
30 | /**
31 | * @public
32 | */
33 | export interface CodeSchemaType extends Omit {
34 | options?: CodeOptions
35 | }
36 |
--------------------------------------------------------------------------------
/src/ui/focusRingStyle.ts:
--------------------------------------------------------------------------------
1 | /** @internal */
2 | // todo: import from @sanity/ui instead
3 | export function focusRingBorderStyle(border: {color: string; width: number}): string {
4 | return `inset 0 0 0 ${border.width}px ${border.color}`
5 | }
6 |
7 | /** @internal */
8 | // todo: import from @sanity/ui instead
9 | export function focusRingStyle(opts: {
10 | base?: {bg: string}
11 | border?: {color: string; width: number}
12 | focusRing: {offset: number; width: number}
13 | }): string {
14 | const {base, border, focusRing} = opts
15 | const focusRingOutsetWidth = focusRing.offset + focusRing.width
16 | const focusRingInsetWidth = 0 - focusRing.offset
17 | const bgColor = base ? base.bg : 'var(--card-bg-color)'
18 |
19 | return [
20 | focusRingInsetWidth > 0 && `inset 0 0 0 ${focusRingInsetWidth}px var(--card-focus-ring-color)`,
21 | border && focusRingBorderStyle(border),
22 | focusRingInsetWidth < 0 && `0 0 0 ${0 - focusRingInsetWidth}px ${bgColor}`,
23 | focusRingOutsetWidth > 0 && `0 0 0 ${focusRingOutsetWidth}px var(--card-focus-ring-color)`,
24 | ]
25 | .filter(Boolean)
26 | .join(',')
27 | }
28 |
--------------------------------------------------------------------------------
/src/useFieldMember.ts:
--------------------------------------------------------------------------------
1 | import {useMemo} from 'react'
2 | import {FieldMember, ObjectMember} from 'sanity'
3 |
4 | /** @internal */
5 | export function useFieldMember(
6 | members: ObjectMember[],
7 | fieldName: string,
8 | ): FieldMember | undefined {
9 | return useMemo(
10 | () =>
11 | members.find(
12 | (member): member is FieldMember => member.kind === 'field' && member.name === fieldName,
13 | ),
14 | [members, fieldName],
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/test/schema.ts:
--------------------------------------------------------------------------------
1 | import {defineType} from 'sanity'
2 |
3 | const testType = defineType({
4 | type: 'document',
5 | name: 'test',
6 | title: 'Test',
7 | fields: [
8 | {
9 | type: 'string',
10 | name: 'title',
11 | title: 'Title',
12 | },
13 | {
14 | type: 'code',
15 | name: 'code',
16 | title: 'Code',
17 | },
18 | {
19 | type: 'array',
20 | name: 'content',
21 | of: [{name: 'code', type: 'code', options: {withFilename: true}}],
22 | },
23 | ],
24 | })
25 |
26 | export const schema = {types: [testType]}
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "include": ["./package.config.ts", "./sanity.config.ts", "./src", "./test"],
4 | "compilerOptions": {
5 | "rootDir": ".",
6 | "jsx": "react-jsx",
7 | "noEmit": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.settings",
3 | "include": ["./src"],
4 | "exclude": [
5 | "./src/**/__fixtures__",
6 | "./src/**/__mocks__",
7 | "./src/**/*.test.ts",
8 | "./src/**/*.test.tsx"
9 | ],
10 | "compilerOptions": {
11 | "rootDir": ".",
12 | "outDir": "./lib",
13 | "jsx": "preserve",
14 | "module": "preserve",
15 | "noEmit": true,
16 | "emitDeclarationOnly": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "node",
4 | "target": "esnext",
5 | "module": "esnext",
6 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
7 | "esModuleInterop": true,
8 | "strict": true,
9 | "downlevelIteration": true,
10 | "declaration": true,
11 | "allowSyntheticDefaultImports": true,
12 | "skipLibCheck": true,
13 | "isolatedModules": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/v2-incompatible.js:
--------------------------------------------------------------------------------
1 | const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin')
2 | const {name, version, sanityExchangeUrl} = require('./package.json')
3 |
4 | export default showIncompatiblePluginDialog({
5 | name: name,
6 | versions: {
7 | v3: version,
8 | v2: '^2.30.0',
9 | },
10 | sanityExchangeUrl,
11 | })
12 |
--------------------------------------------------------------------------------