├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── renovate.json └── workflows │ ├── format-if-needed.yml │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── .releaserc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── inject │ ├── LICENSE │ ├── editorconfig │ ├── prettierrc.json │ ├── renovatebot │ └── renovate.json │ ├── sanity.json │ ├── semver-workflow │ ├── .github │ │ └── workflows │ │ │ └── main.yml │ ├── .husky │ │ ├── commit-msg │ │ └── pre-commit │ ├── .releaserc.json │ ├── commitlint.template.js │ ├── lint-staged.template.js │ └── renovate.json │ ├── ui-workshop │ ├── src │ │ ├── CustomField.tsx │ │ └── __workshop__ │ │ │ ├── index.tsx │ │ │ └── props.tsx │ └── workshop.config.ts │ └── v2-incompatible.js.template ├── bin └── plugin-kit.js ├── commitlint.config.js ├── docs ├── assets │ └── semver-workflow-example.png ├── renovatebot.md ├── semver-workflow.md ├── ui-workshop.md └── ui.md ├── lint-staged.config.js ├── package-lock.json ├── package.config.ts ├── package.json ├── src ├── actions │ ├── init.ts │ ├── inject.ts │ ├── link-watch.ts │ ├── verify-package.ts │ ├── verify-studio.ts │ └── verify │ │ ├── types.ts │ │ ├── validations.ts │ │ └── verify-common.ts ├── cli.ts ├── cmds │ ├── index.ts │ ├── init.ts │ ├── inject.ts │ ├── link-watch.ts │ ├── verify-package.ts │ ├── verify-studio.ts │ └── version.ts ├── configs │ ├── banned-packages.ts │ ├── buildExtensions.ts │ ├── default-source.ts │ ├── eslint.ts │ ├── forced-package-versions.ts │ ├── git.ts │ ├── pkg-config.ts │ ├── prettier.ts │ ├── tsconfig.ts │ └── uselessFiles.ts ├── constants.ts ├── dependencies │ ├── find.ts │ └── import-linter.ts ├── index.ts ├── npm │ ├── manager.ts │ ├── package.ts │ ├── publish.ts │ └── resolveLatestVersions.ts ├── presets │ ├── presets.ts │ ├── renovatebot.ts │ ├── semver-workflow.ts │ ├── ui-workshop.ts │ └── ui.ts ├── sanity │ └── manifest.ts ├── sharedFlags.ts └── util │ ├── command-parser.ts │ ├── errorToUndefined.ts │ ├── files.ts │ ├── log.ts │ ├── prompt.ts │ ├── readme.ts │ ├── request.ts │ ├── ts.ts │ └── user.ts ├── tap-snapshots └── test │ └── verify-package.test.ts.test.cjs ├── test ├── cli.test.ts ├── fixture-utils.ts ├── fixtures │ ├── build │ │ ├── folder-sanity-json │ │ │ ├── package.json │ │ │ ├── sanity.json │ │ │ │ └── .gitkeep │ │ │ └── src │ │ │ │ └── one.js │ │ ├── plain │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── sanity.json │ │ │ └── src │ │ │ │ └── schemaType.js │ │ ├── ts │ │ │ ├── .eslintignore │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── sanity.json │ │ │ └── src │ │ │ │ ├── one.tsx │ │ │ │ ├── styles │ │ │ │ └── one.css │ │ │ │ └── two.ts │ │ └── valid-js │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── sanity.json │ │ │ └── src │ │ │ ├── index.js │ │ │ ├── styles │ │ │ └── one.css │ │ │ └── two.js │ ├── init │ │ └── empty │ │ │ └── .gitkeep │ ├── inject │ │ └── valid │ │ │ ├── .editorconfig │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── .prettierrc.json │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── sanity.json │ │ │ ├── src │ │ │ └── index.ts │ │ │ ├── tsconfig.json │ │ │ └── v2-incompatible.js │ └── verify-package │ │ ├── every-failure-possible │ │ ├── .eslintignore │ │ ├── .eslintrc │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── .gitkeep │ │ ├── .prettierrc │ │ ├── LICENSE │ │ ├── README.md │ │ ├── babel.config.js │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── sanity.json │ │ ├── src │ │ │ └── index.tsx │ │ └── tsconfig.json │ │ ├── fresh-v2-movie-studio │ │ ├── .eslintrc │ │ ├── README.md │ │ ├── config │ │ │ ├── .checksums │ │ │ └── @sanity │ │ │ │ ├── data-aspects.json │ │ │ │ ├── default-layout.json │ │ │ │ ├── default-login.json │ │ │ │ ├── form-builder.json │ │ │ │ ├── google-maps-input.json │ │ │ │ └── vision.json │ │ ├── package.json │ │ ├── plugins │ │ │ └── .gitkeep │ │ ├── sanity.json │ │ ├── schemas │ │ │ ├── blockContent.js │ │ │ ├── castMember.js │ │ │ ├── crewMember.js │ │ │ ├── movie.js │ │ │ ├── person.js │ │ │ ├── plotSummaries.js │ │ │ ├── plotSummary.js │ │ │ ├── schema.js │ │ │ └── screening.js │ │ ├── static │ │ │ ├── .gitkeep │ │ │ └── favicon.ico │ │ └── tsconfig.json │ │ ├── invalid-eslint │ │ ├── .editorconfig │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── .prettierrc.json │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── sanity.json │ │ ├── src │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── v2-incompatible.js │ │ └── valid │ │ ├── .editorconfig │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── .prettierrc.json │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.config.ts │ │ ├── package.json │ │ ├── sanity.json │ │ ├── src │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── v2-incompatible.js ├── init-verify-build.test.ts ├── init.test.ts ├── inject.test.ts ├── run-test-command.ts ├── semver-workflow.test.ts ├── verify-package.test.ts └── version.test.ts ├── tsconfig.dist.json ├── tsconfig.json └── tsconfig.settings.json /.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 | .eslintrc.js 2 | commitlint.config.js 3 | dist 4 | lint-staged.config.js 5 | node_modules 6 | test/fixtures 7 | coverage 8 | babel 9 | *.js 10 | v2-incompatible.js.template 11 | *.json 12 | assets/ 13 | tap-snapshots/ 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: false, 5 | }, 6 | extends: ['plugin:prettier/recommended'], 7 | overrides: [ 8 | { 9 | files: ['*.{ts,tsx}'], 10 | }, 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | project: './tsconfig.json', 18 | }, 19 | plugins: ['prettier'], 20 | rules: { 21 | '@typescript-eslint/explicit-function-return-type': 0, 22 | 'no-shadow': 'off', 23 | 'no-await-in-loop': 'off', 24 | 'no-console': 'off', 25 | complexity: 'off', 26 | 'id-length': 'off', 27 | 'max-depth': 'off', 28 | 'no-sync': 'off', 29 | strict: 'off', 30 | }, 31 | 32 | settings: { 33 | ignorePatterns: ['test/fixtures/**'], 34 | 'import/ignore': ['\\.css$', '.*node_modules.*', '.*:.*'], 35 | 'import/resolver': { 36 | node: { 37 | paths: ['src'], 38 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 39 | }, 40 | }, 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @sanity-io/ecosystem 2 | -------------------------------------------------------------------------------- /.github/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 | "ignorePresets": [":ignoreModulesAndTests", "github>sanity-io/renovate-config:group-non-major"], 9 | "ignorePaths": ["**/test/fixtures/**"] 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/format-if-needed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Auto format 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | permissions: 14 | contents: read # for checkout 15 | 16 | jobs: 17 | run: 18 | name: Can the code be formatted? 🤔 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-node@v4 23 | with: 24 | cache: npm 25 | node-version: lts/* 26 | - run: npm ci --ignore-scripts --only-dev 27 | - uses: actions/cache@v4 28 | with: 29 | path: node_modules/.cache/prettier/.prettier-cache 30 | key: prettier-${{ hashFiles('package-lock.json') }}-${{ hashFiles('.gitignore') }} 31 | - run: npm run format 32 | - run: git restore .github/workflows 33 | - uses: actions/create-github-app-token@v1 34 | id: generate-token 35 | with: 36 | app-id: ${{ secrets.ECOSPARK_APP_ID }} 37 | private-key: ${{ secrets.ECOSPARK_APP_PRIVATE_KEY }} 38 | - uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6 39 | with: 40 | author: github-actions <41898282+github-actions[bot]@users.noreply.github.com> 41 | body: I ran `npm run format` 🧑‍💻 42 | branch: actions/format 43 | commit-message: 'chore(format): 🤖 ✨' 44 | labels: 🤖 bot 45 | title: 'chore(format): 🤖 ✨' 46 | token: ${{ steps.generate-token.outputs.token }} 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 && 'Test ➤ Publish to NPM' || 8 | 'Test' 9 | }} 10 | 11 | on: 12 | # Build on pushes branches that have a PR (including drafts) 13 | pull_request: 14 | # Build on commits pushed to branches without a PR if it's in the allowlist 15 | push: 16 | branches: [main] 17 | # Also run as part of merge queues 18 | merge_group: 19 | # https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow 20 | workflow_dispatch: 21 | inputs: 22 | release: 23 | description: Release new version 24 | required: true 25 | default: false 26 | type: boolean 27 | 28 | concurrency: 29 | # 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 30 | # 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. 31 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 32 | cancel-in-progress: true 33 | 34 | permissions: 35 | contents: read # for checkout 36 | 37 | jobs: 38 | build: 39 | runs-on: ubuntu-latest 40 | name: Lint & Build 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: actions/setup-node@v4 44 | with: 45 | cache: npm 46 | node-version: lts/* 47 | - run: npm clean-install 48 | - run: npm run lint 49 | # But not the build script, as semantic-release will crash if this command fails so it makes sense to test it early 50 | - run: npm run prepublishOnly 51 | 52 | test: 53 | needs: build 54 | # The test matrix can be skipped, in case a new release needs to be fast-tracked and tests are already passing on main 55 | runs-on: ${{ matrix.os }} 56 | name: Node.js ${{ matrix.node }} / ${{ matrix.os }} 57 | strategy: 58 | # 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 59 | fail-fast: false 60 | matrix: 61 | # Run the testing suite on each major OS with the latest LTS release of Node.js 62 | os: [macos-latest, ubuntu-latest, windows-latest] 63 | node: [lts/*] 64 | # It makes sense to also test the oldest, and latest, versions of Node.js, on ubuntu-only since it's the fastest CI runner 65 | include: 66 | - os: ubuntu-latest 67 | # Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life 68 | node: lts/-1 69 | # disabled for now, waiting for pkg-utils fix 70 | # Test the actively developed version that will become the latest LTS release 71 | # - os: ubuntu-latest 72 | # node: current 73 | steps: 74 | # It's only necessary to do this for windows, as mac and ubuntu are sane OS's that already use LF 75 | - name: Set git to use LF 76 | if: matrix.os == 'windows-latest' 77 | run: | 78 | git config --global core.autocrlf false 79 | git config --global core.eol lf 80 | - uses: actions/checkout@v4 81 | - uses: actions/setup-node@v4 82 | with: 83 | cache: npm 84 | node-version: ${{ matrix.node }} 85 | - run: npm install 86 | - run: npm test 87 | 88 | release: 89 | permissions: 90 | id-token: write # to enable use of OIDC for npm provenance 91 | needs: [build, test] 92 | # only run if opt-in during workflow_dispatch 93 | if: always() && github.event.inputs.release == 'true' && needs.build.result != 'failure' && needs.test.result != 'failure' && needs.test.result != 'cancelled' 94 | runs-on: ubuntu-latest 95 | name: Semantic release 96 | steps: 97 | - uses: actions/create-github-app-token@v1 98 | id: app-token 99 | with: 100 | app-id: ${{ secrets.ECOSPARK_APP_ID }} 101 | private-key: ${{ secrets.ECOSPARK_APP_PRIVATE_KEY }} 102 | - uses: actions/checkout@v4 103 | with: 104 | # Need to fetch entire commit history to 105 | # analyze every commit since last release 106 | fetch-depth: 0 107 | # Uses generated token to allow pushing commits back when strict branch rules are used 108 | token: ${{ steps.app-token.outputs.token }} 109 | # Make sure the value of GITHUB_TOKEN will not be persisted in repo's config 110 | persist-credentials: false 111 | - uses: actions/setup-node@v4 112 | with: 113 | cache: npm 114 | node-version: lts/* 115 | - run: npm clean-install 116 | - run: npm audit signatures 117 | # Branches that will release new versions are defined in .releaserc.json 118 | - run: npx semantic-release 119 | # Don't allow interrupting the release step if the job is cancelled, as it can lead to an inconsistent state 120 | # e.g. git tags were pushed but it exited before `npm publish` 121 | if: always() 122 | env: 123 | NPM_CONFIG_PROVENANCE: true 124 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 125 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 126 | -------------------------------------------------------------------------------- /.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 | # Lockfiles 46 | /yarn.lock 47 | 48 | # Cache 49 | .cache 50 | 51 | ## TypeScript out 52 | dist/ 53 | 54 | # IntelliJ 55 | .idea/ 56 | *.iml 57 | 58 | # yalc 59 | yalc.lock 60 | .yalc 61 | 62 | # Fixtures for tests that output files that will be verified 63 | test/fixtures/build/lib 64 | /test/fixtures/init/empty/* 65 | !test/fixtures/init/empty/.gitkeep 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | assets 2 | build 3 | coverage 4 | tap-snapshots 5 | test/fixtures 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true, 6 | "plugins": ["prettier-plugin-packagejson"] 7 | } 8 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/semantic-release-preset", 3 | "branches": ["main"] 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 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 | -------------------------------------------------------------------------------- /assets/inject/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Espen Hovlandsdal 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 | -------------------------------------------------------------------------------- /assets/inject/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 | -------------------------------------------------------------------------------- /assets/inject/prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /assets/inject/renovatebot/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>sanity-io/renovate-presets//ecosystem/auto", 5 | "github>sanity-io/renovate-presets//ecosystem/studio-v3" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /assets/inject/sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/sanity-root", 5 | "path": "./v2-incompatible.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /assets/inject/semver-workflow/.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 && format('Build {0} ➤ Test ➤ Publish to NPM', github.ref_name) || 8 | inputs.release && !inputs.test && format('Build {0} ➤ Skip Tests ➤ Publish to NPM', github.ref_name) || 9 | github.event_name == 'workflow_dispatch' && inputs.test && format('Build {0} ➤ Test', github.ref_name) || 10 | github.event_name == 'workflow_dispatch' && !inputs.test && format('Build {0} ➤ Skip Tests', github.ref_name) || 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 | permissions: 41 | contents: read # for checkout 42 | 43 | jobs: 44 | build: 45 | runs-on: ubuntu-latest 46 | name: Lint & Build 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: actions/setup-node@v4 50 | with: 51 | cache: npm 52 | node-version: lts/* 53 | - run: npm clean-install 54 | # Linting can be skipped 55 | - run: npm run lint --if-present 56 | if: github.event.inputs.test != 'false' 57 | # But not the build script, as semantic-release will crash if this command fails so it makes sense to test it early 58 | - run: npm run prepublishOnly --if-present 59 | 60 | test: 61 | needs: build 62 | # The test matrix can be skipped, in case a new release needs to be fast-tracked and tests are already passing on main 63 | if: github.event.inputs.test != 'false' 64 | runs-on: ${{ matrix.os }} 65 | name: Node.js ${{ matrix.node }} / ${{ matrix.os }} 66 | strategy: 67 | # 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 68 | fail-fast: false 69 | matrix: 70 | # Run the testing suite on each major OS with the latest LTS release of Node.js 71 | os: [macos-latest, ubuntu-latest, windows-latest] 72 | node: [lts/*] 73 | # It makes sense to also test the oldest, and latest, versions of Node.js, on ubuntu-only since it's the fastest CI runner 74 | include: 75 | - os: ubuntu-latest 76 | # Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life 77 | node: lts/-1 78 | - os: ubuntu-latest 79 | # Test the actively developed version that will become the latest LTS release next October 80 | node: current 81 | steps: 82 | # It's only necessary to do this for windows, as mac and ubuntu are sane OS's that already use LF 83 | - name: Set git to use LF 84 | if: matrix.os == 'windows-latest' 85 | run: | 86 | git config --global core.autocrlf false 87 | git config --global core.eol lf 88 | - uses: actions/checkout@v4 89 | - uses: actions/setup-node@v4 90 | with: 91 | cache: npm 92 | node-version: ${{ matrix.node }} 93 | - run: npm install 94 | - run: npm test --if-present 95 | 96 | release: 97 | permissions: 98 | contents: write # to be able to publish a GitHub release 99 | issues: write # to be able to comment on released issues 100 | pull-requests: write # to be able to comment on released pull requests 101 | id-token: write # to enable use of OIDC for npm provenance 102 | needs: [build, test] 103 | # only run if opt-in during workflow_dispatch 104 | if: always() && github.event.inputs.release == 'true' && needs.build.result != 'failure' && needs.test.result != 'failure' && needs.test.result != 'cancelled' 105 | runs-on: ubuntu-latest 106 | name: Semantic release 107 | steps: 108 | - uses: actions/checkout@v4 109 | with: 110 | # Need to fetch entire commit history to 111 | # analyze every commit since last release 112 | fetch-depth: 0 113 | - uses: actions/setup-node@v4 114 | with: 115 | cache: npm 116 | node-version: lts/* 117 | - run: npm clean-install 118 | - run: npm audit signatures 119 | # Branches that will release new versions are defined in .releaserc.json 120 | # @TODO remove --dry-run after verifying everything is good to go 121 | - run: npx semantic-release --dry-run 122 | # Don't allow interrupting the release step if the job is cancelled, as it can lead to an inconsistent state 123 | # e.g. git tags were pushed but it exited before `npm publish` 124 | if: always() 125 | env: 126 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 127 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 128 | -------------------------------------------------------------------------------- /assets/inject/semver-workflow/.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit "" 2 | -------------------------------------------------------------------------------- /assets/inject/semver-workflow/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /assets/inject/semver-workflow/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/semantic-release-preset", 3 | "branches": ["main"] 4 | } 5 | -------------------------------------------------------------------------------- /assets/inject/semver-workflow/commitlint.template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /assets/inject/semver-workflow/lint-staged.template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '**/*.{js,jsx}': ['eslint'], 3 | '**/*.{ts,tsx}': ['eslint', () => 'tsc --build'], 4 | } 5 | -------------------------------------------------------------------------------- /assets/inject/semver-workflow/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>sanity-io/renovate-presets//ecosystem/auto", 5 | "github>sanity-io/renovate-presets//ecosystem/studio-v3" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /assets/inject/ui-workshop/src/CustomField.tsx: -------------------------------------------------------------------------------- 1 | import {Box, Stack, Text} from '@sanity/ui' 2 | import React, {ReactNode} from 'react' 3 | 4 | export function CustomField(props: {children?: ReactNode; title?: ReactNode}) { 5 | const {children, title} = props 6 | 7 | return ( 8 | 9 | 10 | {title} 11 | 12 | {children} 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /assets/inject/ui-workshop/src/__workshop__/index.tsx: -------------------------------------------------------------------------------- 1 | import {defineScope} from '@sanity/ui-workshop' 2 | import {lazy} from 'react' 3 | 4 | export default defineScope({ 5 | name: 'custom', 6 | title: 'Custom (example)', 7 | stories: [ 8 | { 9 | name: 'props', 10 | title: 'Props', 11 | component: lazy(() => import('./props')), 12 | }, 13 | ], 14 | }) 15 | -------------------------------------------------------------------------------- /assets/inject/ui-workshop/src/__workshop__/props.tsx: -------------------------------------------------------------------------------- 1 | import {Box, Card, Container, Text} from '@sanity/ui' 2 | import {useString} from '@sanity/ui-workshop' 3 | import {CustomField} from '../CustomField' 4 | import React from 'react' 5 | 6 | export default function PropsStory() { 7 | const title = useString('Title', 'My custom field') 8 | 9 | return ( 10 | 11 | 12 | 13 | 14 | This is just an example 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /assets/inject/ui-workshop/workshop.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from '@sanity/ui-workshop' 2 | 3 | export default defineConfig({ 4 | title: 'Workshop Starter', 5 | }) 6 | -------------------------------------------------------------------------------- /assets/inject/v2-incompatible.js.template: -------------------------------------------------------------------------------- 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: undefined, 9 | }, 10 | sanityExchangeUrl, 11 | }) 12 | -------------------------------------------------------------------------------- /bin/plugin-kit.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../dist/cli').cliEntry() 3 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /docs/assets/semver-workflow-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/plugin-kit/519ad2851042cd09e5d0dfe15cfc03c50c74fc5e/docs/assets/semver-workflow-example.png -------------------------------------------------------------------------------- /docs/renovatebot.md: -------------------------------------------------------------------------------- 1 | # Preset: renovatebot 2 | 3 | ## Usage 4 | 5 | ### Inject into existing package 6 | 7 | `npx @sanity/plugin-kit@latest inject --preset-only --preset renovatebot` 8 | 9 | ### Use to init plugin 10 | 11 | `npx @sanity/plugin-kit@latest init --preset renovatebot ` 12 | 13 | ## What does it do? 14 | 15 | Sets up the repo 16 | 17 | - Adds Sanity dependabot preset dependency. 18 | - Adds `renovate.json` to configure the above dependency for Renovatebot 19 | 20 | ## Manual steps after inject 21 | 22 | After injection, Renovate bot must be enabled for the repo on Github. 23 | 24 | This can be done by adding the repo to Github Renovatebot app allow-list. 25 | -------------------------------------------------------------------------------- /docs/semver-workflow.md: -------------------------------------------------------------------------------- 1 | # Preset: semver-workflow 2 | 3 | ## Usage 4 | 5 | ### Inject into existing package 6 | 7 | `npx @sanity/plugin-kit@latest inject --preset-only --preset semver-workflow` 8 | 9 | ### Use to init plugin 10 | 11 | `npx @sanity/plugin-kit@latest init --preset semver-workflow ` 12 | 13 | ## What does it do? 14 | 15 | Adds opinionated config and dependencies used by the Ecosystem team on Sanity, to develop using 16 | semantic-release driven workflow on Github. 17 | 18 | ![Github workflow](assets/semver-workflow-example.png) 19 | 20 | This preset: 21 | 22 | - adds [husky](https://github.com/typicode/husky) for pre-commit hooks to ensure that: 23 | - all commits follow [conventional-commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) format 24 | - all files in a commit pass eslint 25 | - [semantic-release](https://semantic-release.gitbook.io/semantic-release/) automation for npm publish 26 | - automates Github releases 27 | - updates package version based on conventional-commits 28 | - updates CHANGELOG.md 29 | - [GitHub workflow](https://docs.github.com/en/actions/using-workflows) (Action) that 30 | - does continuous integration 31 | - has publish-on-demand support which delegates to semantic-release 32 | - updates README.md with some standard texts, if missing 33 | 34 | Keep in mind that this setup is tailored to the needs of the Ecosystem team at Sanity. 35 | Feel free to modify any and all files injected by the preset, or use it as a basis for creating your own workflow. 36 | 37 | ## Manual steps after inject 38 | 39 | ### 1. Install new dependencies 40 | 41 | Run 42 | 43 | ```bash 44 | npm install 45 | ``` 46 | 47 | ### 2. Check README.md 48 | 49 | The preset changes README.md in a naive manner. 50 | Some text could be redundant or unnecessary depending on context and search for TODO. 51 | 52 | Move text around until it looks good. Remember to change any v2 usage examples. 53 | 54 | ### 3. Check `.github/workflows/main.yml` branches 55 | 56 | This differs from repo to repo, default is `[main]` 57 | 58 | In a plugin repo with a v2 and v3 version, it could look like this: 59 | 60 | ```yml 61 | # .github/workflows/main.yml 62 | name: CI & Release 63 | on: 64 | push: 65 | branches: [main, studio-v2] 66 | ``` 67 | 68 | ### 4. Check secrets 69 | 70 | Ensure that your repo or Github org has set the secrets used by the workflow. 71 | 72 | `secrets.GITHUB_TOKEN` should always be available by default, but 73 | `secrets.NPM_PUBLISH_TOKEN` is not. 74 | 75 | Secrets can be set using `Settings -> Secrets -> Actions -> "New repository secret"` 76 | on Github for a repository. 77 | 78 | ### 5. Check .releaserc.json 79 | 80 | This differs from repo to repo. Branches defaults to `"branches": ["main"]` 81 | 82 | In a typical plugin repo with a v2 and v3 version, it will typically look like this: 83 | 84 | ```json 85 | { 86 | "extends": "@sanity/semantic-release-preset", 87 | "branches": ["main", {"name": "studio-v2", "channel": "studio-v2", "range": "1.x.x"}] 88 | } 89 | ``` 90 | 91 | This assumes that the v2 version lives on `studio-v2` branch and the v3 version livs on `main`. 92 | The v2 version will be constrained to a version range and use `studio-v2` as npm tag. 93 | The v3 version will be release with `latest` npm tag. 94 | 95 | ### 6. Test workflow and remove `--dry-run` 96 | 97 | The injected semantic-release command in `.github/workflows/main.yml` has `--dry-run` enabled. 98 | 99 | Before removing the flag, perform a release on Github by manually triggering the `CI & Release` 100 | workflow for the V3-branch and check "Release new version". 101 | 102 | Inspect the workflow logs to see the version that will be used for the release. 103 | If it is ok, remove the `--dry-run` flag from the workflow to perform a real release. 104 | 105 | If the version is not what you expected, you might have to perform some 106 | [troubleshooting](https://semantic-release.gitbook.io/semantic-release/support/troubleshooting). 107 | 108 | #### Testing semantic-release locally 109 | 110 | Create a github token with push access and set it in your shell as GH_TOKEN. 111 | 112 | Now run: 113 | `GH_TOKEN=$GH_TOKEN npx semantic-release --no-ci --dry-run --debug` 114 | 115 | This will run semantic-release in dry-run mode (no git push or npm publish) and show everything that would 116 | go into a release. 117 | 118 | #### Note on "notable commits" 119 | 120 | As configured, semantic-release will not consider commits starting with `docs:` or `chore:` as notable. 121 | If you only have non-notable commits since the last release, semantic-release will not create a new version. 122 | 123 | Therefore, chores or doc updates that should be considered notable should use `fix(chore):` or `fix(docs):` suffix instead. 124 | -------------------------------------------------------------------------------- /docs/ui-workshop.md: -------------------------------------------------------------------------------- 1 | # Preset: ui-workshop 2 | 3 | ## Usage 4 | 5 | ### Inject into existing package 6 | 7 | `npx @sanity/plugin-kit@latest inject --preset-only --preset ui-workshop` 8 | 9 | ### Use to init plugin 10 | 11 | `npx @sanity/plugin-kit@latest init --preset ui-workshop ` 12 | 13 | ## What does it do? 14 | 15 | Sets up your package with [@sanity/ui-workshop](https://github.com/sanity-io/ui-workshop), 16 | to make component testing a breeze. 17 | 18 | - Adds [@sanity/ui-workshop](https://github.com/sanity-io/ui-workshop) dev dependency. 19 | - Adds a example files for testing components using @sanity/ui-workshop 20 | - Adds .workshop to .gitignore 21 | - 22 | 23 | ## Manual steps after inject 24 | 25 | - Run `npm i` to install dependencies. 26 | - Start the workshop with `workshop dev`. 27 | - Put your plugin/package components into workshop to test them. 28 | - Refer to @sanity/ui-workshop [README](https://github.com/sanity-io/ui-workshop#basic-usage) for more. 29 | -------------------------------------------------------------------------------- /docs/ui.md: -------------------------------------------------------------------------------- 1 | # Preset: ui 2 | 3 | ## Usage 4 | 5 | ### Inject into existing package 6 | 7 | `npx @sanity/plugin-kit@latest inject --preset-only --preset ui` 8 | 9 | ### Use to init plugin 10 | 11 | `npx @sanity/plugin-kit@latest init --preset ui ` 12 | 13 | ## What does it do? 14 | 15 | Sets up your package with [`@sanity/ui`](https://github.com/sanity-io/ui) to build plugin UIs. 16 | 17 | - Adds [`@sanity/ui`](https://github.com/sanity-io/ui) dependency. 18 | - Add required dev and peer dependencies. 19 | 20 | ## Manual steps after inject 21 | 22 | - Run `npm i` to install dependencies. 23 | - Refer to @sanity/ui [README](https://github.com/sanity-io/ui) for more. 24 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '**/*.{js,jsx}': ['eslint'], 3 | '**/*.{ts,tsx}': ['eslint', () => 'tsc --build'], 4 | } 5 | -------------------------------------------------------------------------------- /package.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from '@sanity/pkg-utils' 2 | 3 | export default defineConfig({ 4 | bundles: [{source: './src/cli.ts', require: './dist/cli.js'}], 5 | runtime: 'node', 6 | tsconfig: 'tsconfig.dist.json', 7 | }) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sanity/plugin-kit", 3 | "version": "4.0.19", 4 | "description": "Enhanced Sanity.io plugin development experience", 5 | "keywords": [ 6 | "sanity-io", 7 | "sanity", 8 | "plugin", 9 | "development", 10 | "typescript", 11 | "bootstrap" 12 | ], 13 | "homepage": "https://github.com/sanity-io/plugin-kit#readme", 14 | "bugs": { 15 | "url": "https://github.com/sanity-io/plugin-kit/issues" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+ssh://git@github.com/sanity-io/plugin-kit.git" 20 | }, 21 | "license": "MIT", 22 | "author": "Sanity.io ", 23 | "sideEffects": false, 24 | "type": "commonjs", 25 | "exports": { 26 | ".": { 27 | "source": "./src/index.ts", 28 | "require": "./dist/index.js", 29 | "default": "./dist/index.js" 30 | }, 31 | "./package.json": "./package.json" 32 | }, 33 | "main": "./dist/index.js", 34 | "types": "./dist/index.d.ts", 35 | "bin": { 36 | "plugin-kit": "./bin/plugin-kit.js" 37 | }, 38 | "files": [ 39 | "assets", 40 | "bin", 41 | "dist", 42 | "src", 43 | "v2-incompatible.js" 44 | ], 45 | "scripts": { 46 | "build": "pkg-utils build --strict --check --clean", 47 | "commit": "git-cz", 48 | "compile": "tsc --build", 49 | "format": "prettier src package.json -w", 50 | "lint": "eslint .", 51 | "prepare": "husky install", 52 | "prepublishOnly": "npm run build", 53 | "test": "tap", 54 | "test:update-snapshots": "tap --snapshot", 55 | "watch": "pkg-utils watch --strict" 56 | }, 57 | "browserslist": "extends @sanity/browserslist-config", 58 | "tap": { 59 | "browser": false, 60 | "check-coverage": false, 61 | "coverage-report": [ 62 | "html" 63 | ], 64 | "jobs": 2, 65 | "reporter": "spec", 66 | "test-ignore": "^dist/.*|.*ignore.*|.*run-test-command.*|.*fixture.utils.*", 67 | "timeout": 120, 68 | "ts": true 69 | }, 70 | "dependencies": { 71 | "@rexxars/choosealicense-list": "1.1.2", 72 | "@sanity/pkg-utils": "6.12.1", 73 | "chalk": "4.1.2", 74 | "concurrently": "8.2.2", 75 | "discover-path": "1.0.0", 76 | "email-validator": "2.0.4", 77 | "execa": "5.1.1", 78 | "get-it": "8.6.3", 79 | "get-latest-version": "5.1.0", 80 | "git-remote-origin-url": "3.1.0", 81 | "git-user-info": "2.0.3", 82 | "github-url-to-object": "4.0.6", 83 | "inquirer": "8.2.6", 84 | "meow": "9.0.0", 85 | "nodemon": "3.1.0", 86 | "npm-packlist": "8.0.2", 87 | "npm-run-path": "4.0.1", 88 | "outdent": "0.8.0", 89 | "p-any": "3.0.0", 90 | "p-props": "4.0.0", 91 | "postcss": "8.4.40", 92 | "semver": "7.5.4", 93 | "spdx-license-ids": "3.0.18", 94 | "validate-npm-package-name": "5.0.0", 95 | "xdg-basedir": "4.0.0", 96 | "yalc": "1.0.0-pre.53" 97 | }, 98 | "devDependencies": { 99 | "@commitlint/cli": "19.3.0", 100 | "@commitlint/config-conventional": "19.2.2", 101 | "@sanity/semantic-release-preset": "5.0.0", 102 | "@sanity/ui-workshop": "^2.0.16", 103 | "@types/eslint": "^8.56.11", 104 | "@types/fs-extra": "^11.0.4", 105 | "@types/inquirer": "^9.0.3", 106 | "@types/node": "^18.17.4", 107 | "@types/nodemon": "^1.19.6", 108 | "@types/tap": "^15.0.11", 109 | "@typescript-eslint/eslint-plugin": "^7.18.0", 110 | "@typescript-eslint/parser": "^7.18.0", 111 | "eslint": "^8.57.0", 112 | "eslint-config-prettier": "^9.1.0", 113 | "eslint-config-sanity": "^7.1.2", 114 | "eslint-plugin-prettier": "^5.2.1", 115 | "eslint-plugin-react-hooks": "^4.6.2", 116 | "fs-extra": "^11.2.0", 117 | "husky": "^8.0.3", 118 | "json5": "2.2.3", 119 | "lint-staged": "^13.3.0", 120 | "prettier": "^3.3.3", 121 | "prettier-plugin-packagejson": "^2.5.1", 122 | "readdirp": "^3.6.0", 123 | "rimraf": "^4.4.1", 124 | "sanity": "3.60.0", 125 | "sinon": "^17.0.2", 126 | "tap": "^16.3.10", 127 | "ts-node": "^10.9.2", 128 | "typescript": "5.5.3" 129 | }, 130 | "peerDependencies": { 131 | "eslint": ">=8.0.0" 132 | }, 133 | "engines": { 134 | "node": ">=18" 135 | }, 136 | "publishConfig": { 137 | "access": "public" 138 | }, 139 | "binname": "sanity-plugin", 140 | "overrides": { 141 | "conventional-changelog-conventionalcommits": ">= 8.0.0" 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/actions/init.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import {inject} from './inject' 3 | import {ensureDir, writeFile} from '../util/files' 4 | import sharedFlags from '../sharedFlags' 5 | import {TypedFlags} from 'meow' 6 | import {getPackage} from '../npm/package' 7 | import {defaultSourceJs, defaultSourceTs} from '../configs/default-source' 8 | import {defaultOutDir} from '../constants' 9 | 10 | export const initFlags = { 11 | ...sharedFlags, 12 | scripts: { 13 | type: 'boolean', 14 | default: true, 15 | }, 16 | eslint: { 17 | type: 'boolean', 18 | default: true, 19 | }, 20 | typescript: { 21 | type: 'boolean', 22 | default: true, 23 | }, 24 | prettier: { 25 | type: 'boolean', 26 | default: true, 27 | }, 28 | license: { 29 | type: 'string', 30 | }, 31 | editorconfig: { 32 | type: 'boolean', 33 | default: true, 34 | }, 35 | gitignore: { 36 | type: 'boolean', 37 | default: true, 38 | }, 39 | force: { 40 | type: 'boolean', 41 | default: false, 42 | }, 43 | install: { 44 | type: 'boolean', 45 | default: true, 46 | }, 47 | name: { 48 | type: 'string', 49 | }, 50 | author: { 51 | type: 'string', 52 | }, 53 | repo: { 54 | type: 'string', 55 | }, 56 | presetOnly: { 57 | type: 'boolean', 58 | default: false, 59 | }, 60 | preset: { 61 | type: 'string', 62 | isMultiple: true, 63 | }, 64 | } as const 65 | 66 | export type InitFlags = TypedFlags 67 | 68 | export interface InitOptions { 69 | basePath: string 70 | flags: InitFlags 71 | } 72 | 73 | export async function init(options: InitOptions) { 74 | let dependencies = {} 75 | let devDependencies = {} 76 | let peerDependencies = {} 77 | 78 | await inject({ 79 | ...options, 80 | outDir: defaultOutDir, 81 | requireUserConfirmation: !options.flags.force, 82 | dependencies, 83 | devDependencies, 84 | peerDependencies, 85 | validate: false, 86 | }) 87 | 88 | const packageJson = await getPackage({basePath: options.basePath, validate: false}) 89 | const typescript = options.flags.typescript 90 | const source = typescript ? defaultSourceTs(packageJson) : defaultSourceJs(packageJson) 91 | const filename = typescript ? 'index.ts' : 'index.js' 92 | const srcDir = path.resolve(options.basePath, 'src') 93 | await ensureDir(srcDir) 94 | await writeFile(path.join(srcDir, filename), source, {encoding: 'utf8'}) 95 | } 96 | -------------------------------------------------------------------------------- /src/actions/link-watch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ISC License (ISC) 3 | Copyright 2019 Johan Otterud 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, 6 | provided that the above copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 9 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 11 | WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH 12 | THE USE OR PERFORMANCE OF THIS SOFTWARE. 13 | */ 14 | 15 | /* 16 | This code is a modified version of https://github.com/johot/yalc-watch, 17 | and the ISC License has been added for this file only, in accordance with the package.json license field in that package 18 | */ 19 | 20 | import nodemon from 'nodemon' 21 | import concurrently from 'concurrently' 22 | import chalk from 'chalk' 23 | import fs from 'fs' 24 | import path from 'path' 25 | import log from '../util/log' 26 | import {getPackage} from '../npm/package' 27 | import outdent from 'outdent' 28 | import {fileExists, mkdir} from '../util/files' 29 | import {loadConfig as loadPackageConfig} from '@sanity/pkg-utils' 30 | import {defaultOutDir} from '../constants' 31 | 32 | interface YalcWatchConfig { 33 | command?: string 34 | extensions?: string 35 | } 36 | 37 | interface PackageJson { 38 | sanityPlugin?: {linkWatch?: YalcWatchConfig} 39 | } 40 | 41 | export async function linkWatch({basePath}: {basePath: string}) { 42 | const packageJson: PackageJson = JSON.parse( 43 | fs.readFileSync(path.join(basePath, 'package.json'), 'utf8'), 44 | ) 45 | 46 | const packageConfig = await loadPackageConfig({cwd: basePath}) 47 | const outDir = packageConfig?.dist ?? defaultOutDir 48 | 49 | const watch: Required = { 50 | command: 'npm run watch', 51 | extensions: 'ts,js,png,svg,gif,jpeg,css', 52 | ...packageJson.sanityPlugin?.linkWatch, 53 | } 54 | 55 | nodemon({ 56 | watch: [outDir], 57 | ext: watch.extensions, 58 | exec: 'yalc push --changed', 59 | //delay: 1000 60 | }) 61 | 62 | // ensure the folder exits so it can be watched 63 | const folder = path.join(basePath, outDir) 64 | if (!(await fileExists(folder))) { 65 | await mkdir(folder) 66 | } 67 | 68 | const pkg = await getPackage({basePath, validate: false}) 69 | 70 | concurrently([watch.command]) 71 | 72 | nodemon 73 | .on('start', function () { 74 | log.info( 75 | outdent` 76 | Watching ${outDir} for changes to files with extensions: ${watch.extensions} 77 | 78 | To test this package in Sanity Studio or another package, in a separate shell run: 79 | cd /path/to/sanity/studio-or-package 80 | 81 | Then, run one of the below commands, based on the package manager used in studio-or-package: 82 | 83 | ## yarn 84 | ${chalk.greenBright(`yalc add --link ${pkg.name} && yarn install`)} 85 | 86 | ## npm 87 | ${chalk.greenBright(`npx yalc add ${pkg.name} && npx yalc link ${pkg.name} && npm install`)} 88 | `.trimStart(), 89 | ) 90 | }) 91 | .on('quit', function () { 92 | process.exit() 93 | }) 94 | .on('restart', function (files: any) { 95 | log.info('Found changes in files:', chalk.magentaBright(files)) 96 | log.info('Pushing new yalc package...') 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /src/actions/verify-package.ts: -------------------------------------------------------------------------------- 1 | import {loadConfig as loadPackageConfig} from '@sanity/pkg-utils' 2 | import {getPackage} from '../npm/package' 3 | import log from '../util/log' 4 | import {cliName, defaultOutDir, urls} from '../constants' 5 | import {validateImports} from '../dependencies/import-linter' 6 | import outdent from 'outdent' 7 | import { 8 | createValidator, 9 | runTscMaybe, 10 | VerifyFlags, 11 | VerifyPackageConfig, 12 | } from './verify/verify-common' 13 | import { 14 | validateBabelConfig, 15 | validateNodeEngine, 16 | validatePackageName, 17 | validatePkgUtilsDependency, 18 | validatePluginSanityJson, 19 | validateDeprecatedDependencies, 20 | validateScripts, 21 | validateTsConfig, 22 | validateSanityDependencies, 23 | validateSrcIndexFile, 24 | disallowDuplicateEslintConfig, 25 | disallowDuplicatePrettierConfig, 26 | } from './verify/validations' 27 | import {PackageJson} from './verify/types' 28 | import chalk from 'chalk' 29 | import {readTSConfig} from '../util/ts' 30 | 31 | export async function verifyPackage({basePath, flags}: {basePath: string; flags: VerifyFlags}) { 32 | let errors: string[] = [] 33 | 34 | const packageJson: PackageJson = await getPackage({basePath, validate: false}) 35 | const verifyConfig: VerifyPackageConfig = packageJson.sanityPlugin?.verifyPackage || {} 36 | const packageConfig = await loadPackageConfig({cwd: basePath}) 37 | const outDir = packageConfig?.dist ?? defaultOutDir 38 | const tsconfig = packageConfig?.tsconfig ?? 'tsconfig.json' 39 | 40 | const validation = createValidator(verifyConfig, flags, errors) 41 | 42 | const ts = await readTSConfig({basePath, filename: tsconfig}) 43 | 44 | await validation('packageName', async () => validatePackageName(packageJson)) 45 | await validation('pkg-utils', async () => validatePkgUtilsDependency(packageJson)) 46 | await validation('srcIndex', async () => validateSrcIndexFile(basePath)) 47 | await validation('scripts', async () => validateScripts(packageJson)) 48 | await validation('nodeEngine', async () => validateNodeEngine(packageJson)) 49 | await validation('duplicateConfig', async () => 50 | disallowDuplicateEslintConfig(basePath, packageJson), 51 | ) 52 | await validation('duplicateConfig', async () => 53 | disallowDuplicatePrettierConfig(basePath, packageJson), 54 | ) 55 | 56 | if (ts) { 57 | await validation('tsconfig', async () => validateTsConfig(ts, {basePath, outDir, tsconfig})) 58 | } 59 | 60 | await validation('sanityV2Json', async () => validatePluginSanityJson({basePath, packageJson})) 61 | 62 | await validation('babelConfig', async () => validateBabelConfig({basePath})) 63 | 64 | await validation('dependencies', async () => validateSanityDependencies(packageJson)) 65 | await validation('deprecatedDependencies', async () => 66 | validateDeprecatedDependencies(packageJson), 67 | ) 68 | await validation('eslintImports', async () => validateImports({basePath})) 69 | 70 | if (errors.length) { 71 | throw new Error( 72 | outdent` 73 | Detected validation issues! 74 | To make this package Sanity v3 compatible, fix the issues starting from the top, or disable any checks you deem unnecessary. 75 | 76 | These issues assume the package uses @sanity/plugin-kit defaults for development and building. 77 | Refer to ${urls.pluginReadme} for configuration options. 78 | 79 | More information is available here: 80 | - Studio migration guide: ${urls.migrationGuideStudio} 81 | - Plugin migration guide: ${urls.migrationGuidePlugin} 82 | - Reference documentation: ${urls.refDocs} 83 | 84 | ${chalk.grey( 85 | `To fail-fast on first detected issue run:\nnpx ${cliName} verify-package' --single`, 86 | )} 87 | `.trimStart(), 88 | ) 89 | } 90 | 91 | await runTscMaybe(verifyConfig, ts) 92 | 93 | log.success( 94 | outdent` 95 | No outstanding upgrade issues detected. 96 | 97 | Suggested next steps: 98 | - Use plugin-kit to build and develop the plugin according to ${urls.pluginReadme}. 99 | - Build the plugin and fix any compilation errors 100 | - Test the plugin using the link-watch command 101 | `.trim(), 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /src/actions/verify-studio.ts: -------------------------------------------------------------------------------- 1 | import {loadConfig as loadPackageConfig} from '@sanity/pkg-utils' 2 | import {getPackage} from '../npm/package' 3 | import log from '../util/log' 4 | import {cliName, urls} from '../constants' 5 | import {validateImports} from '../dependencies/import-linter' 6 | import outdent from 'outdent' 7 | import chalk from 'chalk' 8 | import { 9 | createValidator, 10 | runTscMaybe, 11 | VerifyFlags, 12 | VerifyPackageConfig, 13 | } from './verify/verify-common' 14 | import {PackageJson} from './verify/types' 15 | import {validateSanityDependencies, validateStudioConfig} from './verify/validations' 16 | import {readTSConfig} from '../util/ts' 17 | 18 | export async function verifyStudio({basePath, flags}: {basePath: string; flags: VerifyFlags}) { 19 | let errors: string[] = [] 20 | 21 | const packageJson: PackageJson = await getPackage({basePath, validate: false}) 22 | const verifyConfig: VerifyPackageConfig = packageJson.sanityPlugin?.verifyPackage || {} 23 | const packageConfig = await loadPackageConfig({cwd: basePath}) 24 | const tsconfig = packageConfig?.tsconfig ?? 'tsconfig.json' 25 | 26 | const validation = createValidator(verifyConfig, flags, errors) 27 | 28 | const ts = await readTSConfig({basePath, filename: tsconfig}) 29 | 30 | await validation('studioConfig', async () => validateStudioConfig({basePath})) 31 | await validation('dependencies', async () => validateSanityDependencies(packageJson)) 32 | await validation('eslintImports', async () => validateImports({basePath})) 33 | 34 | if (errors.length) { 35 | throw new Error( 36 | outdent` 37 | Detected validation issues! 38 | This Sanity Studio is not completely V3 ready. Fix the issues starting from the top, or disable any checks you deem unnecessary. 39 | 40 | More information is available here: 41 | - Migration guide: ${urls.migrationGuideStudio} 42 | - Reference documentation: ${urls.refDocs} 43 | 44 | ${chalk.grey( 45 | `To fail-fast on first detected issue run:\nnpx ${cliName} verify-studio --single`, 46 | )} 47 | `.trimStart(), 48 | ) 49 | } 50 | 51 | await runTscMaybe(verifyConfig, ts) 52 | 53 | log.success( 54 | outdent` 55 | No outstanding upgrade issues detected. Studio is V3 ready! 56 | `.trim(), 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/actions/verify/types.ts: -------------------------------------------------------------------------------- 1 | import {VerifyPackageConfig} from './verify-common' 2 | 3 | export interface SanityPlugin { 4 | verifyPackage?: VerifyPackageConfig 5 | } 6 | 7 | export interface PackageJson { 8 | name?: string 9 | version?: string 10 | description?: string 11 | author?: string 12 | license?: string 13 | source?: string 14 | exports?: { 15 | [index: string]: Record | string | undefined 16 | } 17 | main?: string 18 | module?: string 19 | types?: string 20 | browser?: string 21 | files?: string[] 22 | scripts?: Record 23 | dependencies?: Record 24 | peerDependencies?: Record 25 | devDependencies?: Record 26 | sanityPlugin?: SanityPlugin 27 | engines?: { 28 | node?: string 29 | } 30 | keywords?: string[] 31 | repository?: {url?: string} 32 | 33 | [index: string]: unknown 34 | } 35 | 36 | export interface SanityV2Json { 37 | parts?: [ 38 | { 39 | implements?: string 40 | path?: 'string' 41 | }, 42 | ] 43 | } 44 | 45 | export interface SanityStudioJson { 46 | root?: boolean 47 | project?: { 48 | name?: string 49 | } 50 | api?: { 51 | projectId?: string 52 | dataset?: string 53 | } 54 | plugins?: string[] 55 | parts?: Record[] 56 | } 57 | -------------------------------------------------------------------------------- /src/actions/verify/verify-common.ts: -------------------------------------------------------------------------------- 1 | import log from '../../util/log' 2 | import {TypedFlags} from 'meow' 3 | import fs from 'fs' 4 | import sharedFlags from '../../sharedFlags' 5 | import chalk from 'chalk' 6 | import outdent from 'outdent' 7 | import {runCommand} from '../../util/command-parser' 8 | import {ParsedCommandLine} from 'typescript' 9 | 10 | export const readFile = fs.promises.readFile 11 | const splitLine = `\n----------------------------------------------------------` 12 | 13 | export const verifyPackageConfigDefaults = { 14 | packageName: true, 15 | tsconfig: true, 16 | tsc: true, 17 | dependencies: true, 18 | deprecatedDependencies: true, 19 | babelConfig: true, 20 | sanityV2Json: true, 21 | eslintImports: true, 22 | scripts: true, 23 | 'pkg-utils': true, 24 | nodeEngine: true, 25 | studioConfig: true, 26 | srcIndex: true, 27 | duplicateConfig: true, 28 | } as const 29 | 30 | export type VerifyPackageConfig = Partial> 31 | 32 | export const verifyFlags = { 33 | ...sharedFlags, 34 | single: { 35 | default: false, 36 | type: 'boolean', 37 | }, 38 | } as const 39 | 40 | export type VerifyFlags = TypedFlags 41 | 42 | export function disableCheckText(checkKey: string) { 43 | return chalk.grey( 44 | outdent` 45 | To skip this validation add the following to your package.json: 46 | "sanityPlugin": { 47 | "verifyPackage": { 48 | "${checkKey}": false 49 | } 50 | } 51 | `.trimStart(), 52 | ) 53 | } 54 | 55 | export function createValidator( 56 | verifyConfig: VerifyPackageConfig, 57 | flags: VerifyFlags, 58 | errors: string[], 59 | ) { 60 | return async function validation( 61 | checkKey: keyof VerifyPackageConfig, 62 | task: () => Promise, 63 | ) { 64 | if (verifyConfig[checkKey] !== false) { 65 | const result = await task() 66 | if (result?.length) { 67 | result.push(disableCheckText(checkKey)) 68 | const errorMessage = result.join('\n\n') 69 | errors.push(errorMessage) 70 | log.error(`\n` + errorMessage + splitLine) 71 | } 72 | } 73 | 74 | if (flags.single && errors.length) { 75 | throw new Error( 76 | outdent`Detected outstanding upgrade issues. 77 | 78 | Fail-fast (--single) mode enabled, stopping validation here. 79 | `, 80 | ) 81 | } 82 | } 83 | } 84 | 85 | export async function runTscMaybe(verifyConfig: VerifyPackageConfig, ts?: ParsedCommandLine) { 86 | if (ts && verifyConfig.tsc !== false) { 87 | log.info('All checks ok, running Typescript compiler.') 88 | const {code} = await runCommand('tsc --build') 89 | if (code !== 0) { 90 | throw new Error('Compilation failed. See output above.\n\n' + disableCheckText('tsc')) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import meow from 'meow' 2 | import log from './util/log' 3 | import commands from './cmds' 4 | import sharedFlags from './sharedFlags' 5 | import {cliName} from './constants' 6 | 7 | /** @public */ 8 | export async function cliEntry(argv = process.argv, autoExit = true) { 9 | const cli = meow( 10 | ` 11 | Usage 12 | $ ${cliName} [--help] [--debug] [] 13 | 14 | These are common commands used in various situations: 15 | 16 | init Create a new Sanity plugin 17 | inject Inject config into an existing Sanity v3 plugin 18 | verify-package Check that a Sanity plugin package follows V3 conventions. Prints upgrade steps. 19 | verify-studio Check that a Sanity Studio follows V3 conventions. Prints upgrade steps. 20 | link-watch Recompiles plugin automatically on changes and runs yalc push --publish 21 | version Show the version of ${cliName} currently installed 22 | 23 | Options 24 | --silent Do not print info and warning messages 25 | --verbose Log everything. This option conflicts with --silent 26 | --debug Print stack trace on errors 27 | --version Output the version number 28 | --help Output usage information 29 | 30 | Examples 31 | # Init a new plugin in current directory 32 | $ ${cliName} init 33 | 34 | # Init a new plugin in my-sanity-plugin directory 35 | $ ${cliName} init my-sanity-plugin 36 | 37 | # Check that a Sanity plugin package in current directory follows V3 conventions 38 | $ ${cliName} verify-package 39 | 40 | # Check that a Sanity Studio in current directory follows V3 conventions 41 | $ ${cliName} verify-studio 42 | `, 43 | { 44 | autoHelp: false, 45 | flags: sharedFlags, 46 | argv: argv.slice(2), 47 | }, 48 | ) 49 | 50 | const commandName = cli.input[0] 51 | if (!commandName) { 52 | cli.showHelp() // Exits 53 | } 54 | 55 | if (!(commandName in commands)) { 56 | console.error(`Unknown command "${commandName}"`) 57 | cli.showHelp() // Exits 58 | } 59 | 60 | if (cli.flags.silent && cli.flags.verbose) { 61 | log.error(`--silent and --verbose are mutually exclusive`) 62 | cli.showHelp() // Exits 63 | } 64 | 65 | // Lazy-load command 66 | const cmd = commands[commandName as keyof typeof commands] 67 | 68 | try { 69 | log.setVerbosity(cli.flags) 70 | await cmd({argv: argv.slice(3)}) 71 | } catch (err: any) { 72 | log.error(err instanceof TypeError || cli.flags.debug ? err.stack : err.message) 73 | 74 | // eslint-disable-next-line no-process-exit 75 | process.exit(1) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/cmds/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | init: async (options: {argv: string[]}) => { 3 | await (await import('./init')).default(options) 4 | }, 5 | inject: async (options: {argv: string[]}) => { 6 | await (await import('./inject')).default(options) 7 | }, 8 | 'link-watch': async (options: {argv: string[]}) => { 9 | await (await import('./link-watch')).default(options) 10 | }, 11 | 'verify-package': async (options: {argv: string[]}) => { 12 | await (await import('./verify-package')).default(options) 13 | }, 14 | 'verify-studio': async (options: {argv: string[]}) => { 15 | await (await import('./verify-studio')).default(options) 16 | }, 17 | version: async (options: {argv: string[]}) => { 18 | await (await import('./version')).default(options) 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /src/cmds/init.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import meow from 'meow' 3 | import log from '../util/log' 4 | import {init, initFlags} from '../actions/init' 5 | import {isEmptyish, ensureDir} from '../util/files' 6 | import {installDependencies, promptForPackageManager} from '../npm/manager' 7 | import {findStudioV3Config, hasSanityJson} from '../sanity/manifest' 8 | import {prompt} from '../util/prompt' 9 | import {cliName} from '../constants' 10 | import {presetHelpList} from '../presets/presets' 11 | 12 | const description = `Initialize a new Sanity plugin` 13 | 14 | const help = ` 15 | Usage 16 | $ ${cliName} init [dir] [] 17 | 18 | Options 19 | --no-eslint Disables ESLint config and dependencies from being added 20 | --no-prettier Disables prettier config and dependencies from being added 21 | --no-typescript Disables typescript config and dependencies from being added 22 | --no-license Disables LICENSE + package.json license field from being added 23 | --no-editorconfig Disables .editorconfig from being added 24 | --no-gitignore Disables .gitignore from being added 25 | --no-scripts Disables scripts from being added to package.json 26 | --no-install Disables automatically running package manager install 27 | 28 | --name [package-name] Use the provided package-name 29 | --author [name] Use the provided author 30 | --repo [url] Use the provided repo url 31 | --license [spdx] Use the license with the given SPDX identifier 32 | --force No promt when overwriting files 33 | 34 | --preset [preset-name] Adds config and files from a named preset. --preset can be supplied multiple times. 35 | The following presets are available: 36 | ${presetHelpList(30)} 37 | 38 | Examples 39 | # Initialize a new plugin in the current directory 40 | $ ${cliName} init 41 | 42 | # Initialize a plugin in the directory ~/my-plugin 43 | $ ${cliName} init ~/my-plugin 44 | 45 | # Don't add eslint or prettier 46 | $ ${cliName} init --no-eslint --no-prettier 47 | ` 48 | 49 | async function run({argv}: {argv: string[]}) { 50 | const cli = meow(help, {flags: initFlags, argv, description}) 51 | const basePath = path.resolve(cli.input[0] || process.cwd()) 52 | 53 | const {exists, isRoot} = await hasSanityJson(basePath) 54 | if (exists && isRoot) { 55 | throw new Error( 56 | `sanity.json has a "root" property set to true - are you trying to init into a studio instead of a plugin?`, 57 | ) 58 | } 59 | 60 | const {v3ConfigFile} = await findStudioV3Config(basePath) 61 | if (v3ConfigFile) { 62 | throw new Error( 63 | `${v3ConfigFile} exsists - are you trying to init into a studio instead of a plugin?`, 64 | ) 65 | } 66 | 67 | log.info('Initializing new plugin in "%s"', basePath) 68 | if ( 69 | !cli.flags.force && 70 | !(await isEmptyish(basePath)) && 71 | !(await prompt('Directory is not empty, proceed?', {type: 'confirm', default: false})) 72 | ) { 73 | log.error('Directory is not empty. Cancelled.') 74 | return 75 | } 76 | 77 | await ensureDir(basePath) 78 | await init({basePath, flags: cli.flags}) 79 | if (cli.flags.install) { 80 | if (await installDependencies(await promptForPackageManager(), {cwd: basePath})) { 81 | log.info('Done!') 82 | } else { 83 | log.error('Failed to install dependencies, try manually running `npm install`') 84 | } 85 | } else { 86 | log.info('Dependency installation skipped.') 87 | } 88 | } 89 | 90 | export default run 91 | -------------------------------------------------------------------------------- /src/cmds/inject.ts: -------------------------------------------------------------------------------- 1 | import {loadConfig as loadPackageConfig} from '@sanity/pkg-utils' 2 | import path from 'path' 3 | import meow from 'meow' 4 | import log from '../util/log' 5 | import {inject} from '../actions/inject' 6 | import {findStudioV3Config} from '../sanity/manifest' 7 | import {initFlags} from '../actions/init' 8 | import {cliName, defaultOutDir} from '../constants' 9 | import {presetHelpList} from '../presets/presets' 10 | 11 | const description = `Inject configuration into a Sanity plugin` 12 | 13 | const help = ` 14 | Usage 15 | $ ${cliName} inject [dir] [] 16 | 17 | Options 18 | --no-eslint Disables ESLint config and dependencies from being added 19 | --no-prettier Disables prettier config and dependencies from being added 20 | --no-typescript Disables typescript config and dependencies from being added 21 | --no-license Disables LICENSE + package.json license field from being added 22 | --no-editorconfig Disables .editorconfig from being added 23 | --no-gitignore Disables .gitignore from being added 24 | --no-scripts Disables scripts from being added to package.json 25 | 26 | --license [spdx] Use the license with the given SPDX identifier 27 | --force No promt when overwriting files 28 | 29 | --preset [preset-name] Adds config and files from a named preset. --preset can be supplied multiple times. 30 | The following presets are available: 31 | ${presetHelpList(30)} 32 | --preset-only Skips the default inject steps. Use this to apply a preset to an otherwise complete plugin. 33 | 34 | Examples 35 | # Inject configuration into the plugin in the current directory 36 | $ ${cliName} inject 37 | 38 | # Inject configuration into the plugin in ~/my-plugin 39 | $ ${cliName} inject ~/my-plugin 40 | 41 | # Don't inject eslint or prettier 42 | $ ${cliName} inject --no-eslint --no-prettier 43 | 44 | # Inject plugin configuration and semver-workflow into the plugin in the current directory 45 | $ @sanity/plugin-kit inject --preset semver-workflow 46 | 47 | # Only inject semver-workflow and renovatebot config from presets 48 | $ ${cliName} inject --preset-only --preset semver-workflow --preset renovatebot 49 | 50 | ` 51 | 52 | async function run({argv}: {argv: string[]}) { 53 | const cli = meow(help, {flags: initFlags, argv, description}) 54 | const basePath = path.resolve(cli.input[0] || process.cwd()) 55 | const packageConfig = await loadPackageConfig({cwd: basePath}) 56 | const outDir = packageConfig?.dist ?? defaultOutDir 57 | 58 | const {v3ConfigFile} = await findStudioV3Config(basePath) 59 | if (v3ConfigFile) { 60 | throw new Error( 61 | `${v3ConfigFile} exists - are you trying to INJECT into a studio instead of a plugin?`, 62 | ) 63 | } 64 | log.info('Inject config into plugin in "%s"', basePath) 65 | 66 | await inject({basePath, outDir, flags: cli.flags, validate: false}) 67 | log.info('Done!') 68 | } 69 | 70 | export default run 71 | -------------------------------------------------------------------------------- /src/cmds/link-watch.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import meow from 'meow' 3 | import pkg from '../../package.json' 4 | import sharedFlags from '../sharedFlags' 5 | import {linkWatch} from '../actions/link-watch' 6 | 7 | const description = `Run the watch command and pushes any changes to yalc` 8 | 9 | const help = ` 10 | Usage 11 | $ ${pkg.binname} link-watch [] 12 | 13 | Options 14 | --silent Do not print info and warning messages 15 | --verbose Log everything. This option conflicts with --silent 16 | --version Output the version number 17 | --help Output usage information 18 | 19 | Configuration 20 | To override the default watch command configuration, provide an override in package.json under sanityPlugin: 21 | { 22 | "sanityPlugin": { 23 | "watchCommand": "microbundle watch --format modern,esm,cjs --jsx React.createElement --jsxImportSource react --css inline", 24 | "linkWatch": { 25 | "command": "npm run watch", 26 | "extensions": "js,png,svg,gif,jpeg,css" 27 | } 28 | } 29 | } 30 | 31 | Examples 32 | # Run the watch command and pushes any changes to yalc 33 | $ ${pkg.binname} link-watch 34 | ` 35 | 36 | const flags = { 37 | ...sharedFlags, 38 | watch: { 39 | type: 'boolean', 40 | default: false, 41 | }, 42 | } as const 43 | 44 | function run({argv}: {argv: string[]}) { 45 | const cli = meow(help, {flags, argv, description}) 46 | const basePath = path.resolve(cli.input[0] || process.cwd()) 47 | return linkWatch({basePath}) 48 | } 49 | 50 | export default run 51 | -------------------------------------------------------------------------------- /src/cmds/verify-package.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import meow from 'meow' 3 | import {verifyPackage} from '../actions/verify-package' 4 | import {cliName} from '../constants' 5 | import {verifyFlags} from '../actions/verify/verify-common' 6 | 7 | const description = `Verify that a Sanity plugin package is v3 compatible, and print upgrade steps if not.` 8 | 9 | const help = ` 10 | Usage 11 | $ ${cliName} verify-package [dir] [] 12 | 13 | Options 14 | --single Enables fail-fast mode: Will only output the first validation that fails. 15 | --silent Do not print info and warning messages 16 | --verbose Log everything. This option conflicts with --silent 17 | --version Output the version number 18 | --help Output usage information 19 | 20 | Each check will describe how they can be individually disabled. 21 | 22 | Examples 23 | # Verify Sanity plugin package in current directory 24 | $ ${cliName} verify-package 25 | 26 | # Verify Sanity plugin package in my-plugin directory in silent mode 27 | $ ${cliName} verify-package my-plugin-directory --silent 28 | ` 29 | 30 | function run({argv}: {argv: string[]}) { 31 | const cli = meow(help, {flags: verifyFlags, argv, description}) 32 | const basePath = path.resolve(cli.input[0] || process.cwd()) 33 | return verifyPackage({basePath, flags: cli.flags}) 34 | } 35 | 36 | export default run 37 | -------------------------------------------------------------------------------- /src/cmds/verify-studio.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import meow from 'meow' 3 | import {cliName} from '../constants' 4 | import {verifyFlags} from '../actions/verify/verify-common' 5 | import {verifyStudio} from '../actions/verify-studio' 6 | 7 | const description = `Verify that a Sanity Studio is configured correctly for v3, and print upgrade steps if not.` 8 | 9 | const help = ` 10 | Usage 11 | $ ${cliName} verify-studio [dir] [] 12 | 13 | Options 14 | --single Enables fail-fast mode: Will only output the first validation that fails. 15 | --silent Do not print info and warning messages 16 | --verbose Log everything. This option conflicts with --silent 17 | --version Output the version number 18 | --help Output usage information 19 | 20 | Each check will describe how they can be individually disabled. 21 | 22 | Examples 23 | # Verify Sanity Studio in current directory 24 | $ ${cliName} verify-studio 25 | 26 | # Verify Sanity Studio in my-sanity-studio directory in silent mode 27 | $ ${cliName} verify-studio my-sanity-studio --silent 28 | ` 29 | 30 | function run({argv}: {argv: string[]}) { 31 | const cli = meow(help, {flags: verifyFlags, argv, description}) 32 | const basePath = path.resolve(cli.input[0] || process.cwd()) 33 | return verifyStudio({basePath, flags: cli.flags}) 34 | } 35 | 36 | export default run 37 | -------------------------------------------------------------------------------- /src/cmds/version.ts: -------------------------------------------------------------------------------- 1 | import meow from 'meow' 2 | import pkg from '../../package.json' 3 | import log from '../util/log' 4 | import sharedFlags from '../sharedFlags' 5 | 6 | const description = `Show the installed version of ${pkg.name}` 7 | 8 | const help = ` 9 | Usage 10 | $ ${pkg.binname} version 11 | 12 | Options 13 | --major Show only the major version 14 | --minor Show only the minor version 15 | --patch Show only the patch version 16 | 17 | Examples 18 | $ ${pkg.binname} version 19 | ${pkg.name} version ${pkg.version} 20 | 21 | $ ${pkg.binname} version --major 22 | ${pkg.version.split('.')[0]} 23 | ` 24 | 25 | const flags = { 26 | ...sharedFlags, 27 | 28 | major: { 29 | type: 'boolean', 30 | default: false, 31 | }, 32 | 33 | minor: { 34 | type: 'boolean', 35 | default: false, 36 | }, 37 | 38 | patch: { 39 | type: 'boolean', 40 | default: false, 41 | }, 42 | } as const 43 | 44 | function run({argv}: {argv: string[]}) { 45 | const cli = meow(help, {flags, argv, description}) 46 | const versionParts = pkg.version.split('.') 47 | const versionNames = ['major', 'minor', 'patch'] 48 | const versionFlags = versionNames.filter((flagName) => cli.flags[flagName]) 49 | const versionFlag = versionFlags[0] 50 | const numVersionFlags = versionFlags.length 51 | 52 | if (numVersionFlags === 0) { 53 | log.msg(`${pkg.name} version ${pkg.version}`) 54 | return 55 | } 56 | 57 | if (numVersionFlags > 1) { 58 | throw new Error( 59 | `--major, --minor and --patch are mutually exclusive - only one can be used at a time`, 60 | ) 61 | } 62 | 63 | const partIndex = versionNames.indexOf(versionFlag) 64 | log.msg(versionParts[partIndex]) 65 | } 66 | 67 | export default run 68 | -------------------------------------------------------------------------------- /src/configs/banned-packages.ts: -------------------------------------------------------------------------------- 1 | export const mergedPackages = [ 2 | '@sanity/base', 3 | '@sanity/core', 4 | '@sanity/types', 5 | '@sanity/data-aspects', 6 | '@sanity/default-layout', 7 | '@sanity/default-login', 8 | '@sanity/desk-tool', 9 | '@sanity/field', 10 | '@sanity/form-builder', 11 | '@sanity/initial-value-templates', 12 | '@sanity/language-filter', 13 | '@sanity/production-preview', 14 | '@sanity/react-hooks', 15 | '@sanity/resolver', 16 | '@sanity/state-router', 17 | '@sanity/structure', 18 | '@sanity/studio-hints', 19 | ].sort() 20 | 21 | export const deprecatedDevDeps = [ 22 | 'tsdx', 23 | 'sanipack', 24 | 'parcel', 25 | '@parcel/packager-ts', 26 | '@parcel/transformer-typescript-types', 27 | ] 28 | -------------------------------------------------------------------------------- /src/configs/buildExtensions.ts: -------------------------------------------------------------------------------- 1 | export const buildExtensions = ['.js', '.jsx', '.es6', '.es', '.mjs', '.ts', '.tsx'] 2 | -------------------------------------------------------------------------------- /src/configs/default-source.ts: -------------------------------------------------------------------------------- 1 | import outdent from 'outdent' 2 | import {PackageJson} from '../actions/verify/types' 3 | 4 | export function defaultSourceJs(pkg: PackageJson) { 5 | return ( 6 | outdent` 7 | import {definePlugin} from 'sanity' 8 | 9 | /** 10 | * Usage in sanity.config.js (or .ts) 11 | * 12 | * \`\`\`js 13 | * import {defineConfig} from 'sanity' 14 | * import {myPlugin} from '${pkg.name}' 15 | * 16 | * export default defineConfig({ 17 | * // ... 18 | * plugins: [myPlugin({})], 19 | * }) 20 | * \`\`\` 21 | */ 22 | export const myPlugin = definePlugin((config = {}) => { 23 | // eslint-disable-next-line no-console 24 | console.log(\`hello from ${pkg.name}\`) 25 | return { 26 | name: '${pkg.name}', 27 | } 28 | }) 29 | `.trimStart() + '\n' 30 | ) 31 | } 32 | 33 | export function defaultSourceTs(pkg: PackageJson) { 34 | return ( 35 | outdent` 36 | import {definePlugin} from 'sanity' 37 | 38 | interface MyPluginConfig { 39 | /* nothing here yet */ 40 | } 41 | 42 | /** 43 | * Usage in \`sanity.config.ts\` (or .js) 44 | * 45 | * \`\`\`ts 46 | * import {defineConfig} from 'sanity' 47 | * import {myPlugin} from '${pkg.name}' 48 | * 49 | * export default defineConfig({ 50 | * // ... 51 | * plugins: [myPlugin()], 52 | * }) 53 | * \`\`\` 54 | */ 55 | export const myPlugin = definePlugin((config = {}) => { 56 | // eslint-disable-next-line no-console 57 | console.log('hello from ${pkg.name}') 58 | return { 59 | name: '${pkg.name}', 60 | } 61 | }) 62 | `.trimStart() + '\n' 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /src/configs/eslint.ts: -------------------------------------------------------------------------------- 1 | import {InjectTemplate} from '../actions/inject' 2 | import {InitFlags} from '../actions/init' 3 | 4 | export function eslintrcTemplate(options: {flags: InitFlags}): InjectTemplate { 5 | const {flags} = options 6 | 7 | const eslintConfig = { 8 | root: true, 9 | env: { 10 | node: true, 11 | browser: true, 12 | }, 13 | extends: [ 14 | 'sanity', 15 | flags.typescript && 'sanity/typescript', 16 | 'sanity/react', 17 | 'plugin:react-hooks/recommended', 18 | flags.prettier && 'plugin:prettier/recommended', 19 | 'plugin:react/jsx-runtime', 20 | ].filter(Boolean), 21 | } 22 | 23 | return { 24 | type: 'template', 25 | force: flags.force, 26 | to: '.eslintrc', 27 | value: JSON.stringify(eslintConfig, null, 2), 28 | } 29 | } 30 | 31 | export function eslintignoreTemplate(options: {flags: InitFlags; outDir: string}): InjectTemplate { 32 | const {flags, outDir} = options 33 | 34 | const patterns = [ 35 | '.eslintrc.js', 36 | 'commitlint.config.js', 37 | outDir, 38 | 'lint-staged.config.js', 39 | flags.typescript ? 'package.config.ts' : 'package.config.js', 40 | flags.typescript ? '*.js' : '', 41 | ].filter(Boolean) 42 | 43 | patterns.sort() 44 | 45 | return { 46 | type: 'template', 47 | force: flags.force, 48 | to: '.eslintignore', 49 | value: patterns.join('\n'), 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/configs/forced-package-versions.ts: -------------------------------------------------------------------------------- 1 | export const forcedPackageVersions = {} 2 | 3 | export const forcedDevPackageVersions = forcedPackageVersions 4 | 5 | export const forcedPeerPackageVersions = { 6 | react: '^18', 7 | 'react-dom': '^18', 8 | '@types/react': '^18', 9 | '@types/react-dom': '^18', 10 | sanity: '^3', 11 | 'styled-components': '^5.2', 12 | } 13 | -------------------------------------------------------------------------------- /src/configs/git.ts: -------------------------------------------------------------------------------- 1 | import {outdent} from 'outdent' 2 | import {InjectTemplate} from '../actions/inject' 3 | 4 | export function gitignoreTemplate(): InjectTemplate { 5 | return { 6 | type: 'template', 7 | to: '.gitignore', 8 | value: outdent` 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules 39 | jspm_packages 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | 47 | # macOS finder cache file 48 | .DS_Store 49 | 50 | # VS Code settings 51 | .vscode 52 | 53 | # IntelliJ 54 | .idea 55 | *.iml 56 | 57 | # Cache 58 | .cache 59 | 60 | # Yalc 61 | .yalc 62 | yalc.lock 63 | 64 | # npm package zips 65 | *.tgz 66 | `, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/configs/pkg-config.ts: -------------------------------------------------------------------------------- 1 | import {outdent} from 'outdent' 2 | import {InjectTemplate} from '../actions/inject' 3 | import {InitFlags} from '../actions/init' 4 | 5 | export function pkgConfigTemplate(options: {outDir: string; flags: InitFlags}): InjectTemplate { 6 | const {flags, outDir} = options 7 | 8 | return { 9 | type: 'template', 10 | force: flags.force, 11 | to: flags.typescript ? 'package.config.ts' : 'package.config.js', 12 | value: outdent` 13 | import {defineConfig} from '@sanity/pkg-utils' 14 | 15 | export default defineConfig({ 16 | dist: '${outDir}', 17 | tsconfig: 'tsconfig.${outDir}.json', 18 | 19 | // Remove this block to enable strict export validation 20 | extract: { 21 | rules: { 22 | 'ae-forgotten-export': 'off', 23 | 'ae-incompatible-release-tags': 'off', 24 | 'ae-internal-missing-underscore': 'off', 25 | 'ae-missing-release-tag': 'off', 26 | }, 27 | }, 28 | }) 29 | `, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/configs/prettier.ts: -------------------------------------------------------------------------------- 1 | import {InjectTemplate} from '../actions/inject' 2 | 3 | export function prettierignoreTemplate(options: {outDir: string}): InjectTemplate { 4 | const {outDir} = options 5 | 6 | return { 7 | type: 'template', 8 | to: '.prettierignore', 9 | value: [outDir, 'pnpm-lock.yaml', 'yarn.lock', 'package-lock.json'].join('\n'), 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/configs/tsconfig.ts: -------------------------------------------------------------------------------- 1 | import {outdent} from 'outdent' 2 | import {InjectTemplate} from '../actions/inject' 3 | import {InitFlags} from '../actions/init' 4 | 5 | export function tsconfigTemplate(options: {flags: InitFlags}): InjectTemplate { 6 | const {flags} = options 7 | 8 | return { 9 | type: 'template', 10 | force: flags.force, 11 | to: 'tsconfig.json', 12 | value: outdent` 13 | { 14 | "extends": "./tsconfig.settings", 15 | "include": ["./src", "./package.config.ts"] 16 | } 17 | `, 18 | } 19 | } 20 | 21 | export function tsconfigTemplateDist(options: {outDir: string; flags: InitFlags}): InjectTemplate { 22 | const {flags, outDir} = options 23 | 24 | return { 25 | type: 'template', 26 | force: flags.force, 27 | to: `tsconfig.${outDir}.json`, 28 | value: outdent` 29 | { 30 | "extends": "./tsconfig.settings", 31 | "include": ["./src"], 32 | "exclude": [ 33 | "./src/**/__fixtures__", 34 | "./src/**/__mocks__", 35 | "./src/**/*.test.ts", 36 | "./src/**/*.test.tsx" 37 | ] 38 | } 39 | `, 40 | } 41 | } 42 | 43 | export function tsconfigTemplateSettings(options: { 44 | outDir: string 45 | flags: InitFlags 46 | }): InjectTemplate { 47 | const {flags, outDir} = options 48 | 49 | return { 50 | type: 'template', 51 | force: flags.force, 52 | to: `tsconfig.settings.json`, 53 | value: outdent` 54 | { 55 | "compilerOptions": { 56 | "rootDir": ".", 57 | "outDir": "./${outDir}", 58 | 59 | "target": "esnext", 60 | "jsx": "preserve", 61 | "module": "preserve", 62 | "moduleResolution": "bundler", 63 | "esModuleInterop": true, 64 | "resolveJsonModule": true, 65 | "moduleDetection": "force", 66 | "strict": true, 67 | "allowSyntheticDefaultImports": true, 68 | "skipLibCheck": true, 69 | "forceConsistentCasingInFileNames": true, 70 | "isolatedModules": true, 71 | 72 | // Don't emit by default, pkg-utils will ignore this when generating .d.ts files 73 | "noEmit": true 74 | } 75 | } 76 | `, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/configs/uselessFiles.ts: -------------------------------------------------------------------------------- 1 | export const uselessFiles = [ 2 | '.babel.config.js', 3 | '.babelrc', 4 | '.drone.yml', 5 | '.editorconfig', 6 | '.eslintignore', 7 | '.eslintrc-ts.js', 8 | '.eslintrc-ts', 9 | '.eslintrc', 10 | '.gitignore', 11 | '.github', 12 | '.nyc_output', 13 | '.prettierrc', 14 | '.stylelintignore', 15 | '.stylelintrc.json', 16 | '.stylelintrc', 17 | '.travis.yaml', 18 | '.travis.yml', 19 | 'babel.config.js', 20 | 'coverage', 21 | 'gulpfile.js', 22 | 'lcov-report', 23 | 'lerna.json', 24 | 'now.json', 25 | 'vercel.json', 26 | 'netlify.toml', 27 | 'postcss.config.js', 28 | 'tsconfig.json', 29 | ] 30 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const cliName = '@sanity/plugin-kit' 2 | 3 | export const urls = { 4 | refDocs: 'https://beta.sanity.io/docs/reference', 5 | migrationGuideStudio: 'https://beta.sanity.io/docs/platform/v2-to-v3', 6 | migrationGuidePlugin: 'https://beta.sanity.io/docs/platform/v2-to-v3/plugins', 7 | pluginReadme: 'https://github.com/sanity-io/plugin-kit', 8 | incompatiblePlugin: 'https://github.com/sanity-io/incompatible-plugin', 9 | sanityExchange: 'https://www.sanity.io/exchange', 10 | linterPackage: 'https://github.com/sanity-io/eslint-config-no-v2-imports', 11 | } 12 | 13 | export const incompatiblePluginPackage = '@sanity/incompatible-plugin' 14 | 15 | export const defaultOutDir = 'dist' 16 | -------------------------------------------------------------------------------- /src/dependencies/find.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import fs from 'fs' 3 | import path from 'path' 4 | import postcss from 'postcss' 5 | import {discoverPathSync} from 'discover-path' 6 | import traverse from '@babel/traverse' 7 | import {parseSync} from '@babel/core' 8 | 9 | export default {findDependencies} 10 | 11 | const partReg = /^(all:part|part|config|sanity):/ 12 | const importReg = /^(?:"([^"]+)"|'([^']+)')$/ 13 | const composesReg = /^(.+?)\s+from\s+(?:"([^"]+)"|'([^']+)'|(global))$/ 14 | 15 | function findDependenciesFromFiles(files, seen = new Set()): string[] { 16 | const dependencies = new Set() 17 | files.forEach((file) => findDependencies(file, seen).forEach((dep) => dependencies.add(dep))) 18 | return Array.from(dependencies) 19 | } 20 | 21 | function findDependenciesInCss(css, entryPath, processDependency) { 22 | let ast 23 | try { 24 | ast = postcss.parse(css) 25 | } catch (err: any) { 26 | throw new Error(`Error parsing file (${entryPath}): ${err.message}`) 27 | } 28 | 29 | ast.walkDecls(/^composes/, (decl) => { 30 | const matches = decl.value.match(composesReg) 31 | if (!matches) { 32 | return 33 | } 34 | 35 | const [, , doubleQuotePath, singleQuotePath] = matches 36 | const importPath = doubleQuotePath || singleQuotePath 37 | if (importPath) { 38 | processDependency(importPath) 39 | } 40 | }) 41 | 42 | ast.walkAtRules('import', (rule) => { 43 | const matches = rule.params.match(importReg) 44 | if (!matches) { 45 | return 46 | } 47 | 48 | const [, doubleQuotePath, singleQuotePath] = matches 49 | const importPath = doubleQuotePath || singleQuotePath 50 | if (importPath) { 51 | processDependency(importPath) 52 | } 53 | }) 54 | } 55 | 56 | interface Traverse { 57 | node: {callee: {name: string}; source: {value: string}; arguments: {value: string}[]} 58 | } 59 | 60 | function findDependenciesInJs( 61 | js: string, 62 | entryPath: string, 63 | processDependency: (val: string) => void 64 | ) { 65 | let ast 66 | try { 67 | ast = parseSync(js, {babelrc: false}) 68 | } catch (err: any) { 69 | throw new Error(`Error parsing file (${entryPath}): ${err.message}`) 70 | } 71 | 72 | traverse(ast, { 73 | ImportDeclaration({node}: Traverse) { 74 | processDependency(node.source.value) 75 | }, 76 | 77 | CallExpression({node}: Traverse) { 78 | if (node.callee.name === 'require') { 79 | processDependency(node.arguments[0].value) 80 | } 81 | }, 82 | }) 83 | } 84 | 85 | export function findDependencies(entryPath: string, seen = new Set()): string[] { 86 | if (Array.isArray(entryPath)) { 87 | return findDependenciesFromFiles(entryPath, seen) 88 | } 89 | 90 | seen.add(entryPath) 91 | 92 | let content 93 | try { 94 | content = fs.readFileSync(entryPath, 'utf8') 95 | } catch (err: any) { 96 | throw new Error(`Error reading file (${entryPath}): ${err.message}`) 97 | } 98 | 99 | const dir = path.dirname(entryPath) 100 | const dependencies = new Set() 101 | 102 | if (entryPath.endsWith('.css')) { 103 | findDependenciesInCss(content, entryPath, processDependency) 104 | } else { 105 | findDependenciesInJs(content, entryPath, processDependency) 106 | } 107 | 108 | function processDependency(requirePath: string) { 109 | if (typeof requirePath !== 'string') { 110 | return 111 | } 112 | 113 | // Don't allow absolute requires 114 | if (path.isAbsolute(requirePath)) { 115 | throw new Error( 116 | `Absolute paths cannot be used in require/import statements: ${entryPath} references path "${requirePath}"` 117 | ) 118 | } 119 | 120 | const isRelative = requirePath.startsWith('.') 121 | const depPath = isRelative && resolveDependency(dir, requirePath, entryPath) 122 | 123 | if ( 124 | depPath && 125 | ['.js', '.css', '.esm', '.mjs', '.jsx'].includes(path.extname(depPath)) && 126 | !seen.has(depPath) 127 | ) { 128 | // For relative javascript/css requires, recurse to find all depdendencies 129 | findDependencies(depPath, seen).forEach((dep) => dependencies.add(dep)) 130 | return 131 | } 132 | 133 | if (isRelative) { 134 | // Not JS? Skip it 135 | return 136 | } 137 | 138 | // For parts, we want the entire path, as we might want to validate them 139 | if (partReg.test(requirePath)) { 140 | dependencies.add(requirePath) 141 | return 142 | } 143 | 144 | // For modules, resolve the base module name, then add them 145 | // eg: `codemirror/mode/javascript` => `codemirror` 146 | // eg: `@sanity/base/foo/bar.js` => `@sanity/base` 147 | const dep = requirePath.startsWith('@') 148 | ? requirePath.replace(/^(@[^/]+\/[^/]+)(\/.*|$)/, '$1') 149 | : requirePath.replace(/^([^/]+)(\/.*|$)/, '$1') 150 | 151 | dependencies.add(dep) 152 | } 153 | 154 | return Array.from(dependencies) 155 | } 156 | 157 | function resolveDependency(fromDir: string, toPath: string, entryPath: string) { 158 | const [querylessPath] = toPath.split('?', 1) 159 | 160 | let depPath 161 | try { 162 | depPath = require.resolve(path.resolve(fromDir, querylessPath)) 163 | } catch (err) { 164 | throw new Error(`Unable to resolve "${querylessPath}" from ${entryPath}`) 165 | } 166 | 167 | let actualPath 168 | try { 169 | actualPath = discoverPathSync(depPath) 170 | } catch (err: any) { 171 | const paths = (err.suggestions || []).map((suggested: string) => 172 | getDidYouMeanPath(querylessPath, suggested) 173 | ) 174 | const didYouMean = paths ? `Did you mean:\n${paths.join('\n- ')}` : '' 175 | throw new Error(`Unable to resolve "${querylessPath}" from ${entryPath}. ${didYouMean}`) 176 | } 177 | 178 | if (actualPath !== depPath) { 179 | const didYouMean = getDidYouMeanPath(querylessPath, actualPath) 180 | throw new Error( 181 | `Unable to resolve "${querylessPath} from ${entryPath}. Did you mean "${didYouMean}"?` 182 | ) 183 | } 184 | 185 | return actualPath 186 | } 187 | 188 | function getDidYouMeanPath(wanted: string, suggested: string) { 189 | const end = wanted.replace(/[./]+/, '') 190 | const start = wanted.slice(0, 0 - end.length) 191 | return `${start}${suggested.slice(0 - end.length)}` 192 | } 193 | */ 194 | -------------------------------------------------------------------------------- /src/dependencies/import-linter.ts: -------------------------------------------------------------------------------- 1 | import log from '../util/log' 2 | import {mergedPackages} from '../configs/banned-packages' 3 | import {urls} from '../constants' 4 | import {ESLint} from 'eslint' 5 | import path from 'path' 6 | import outdent from 'outdent' 7 | 8 | const removedImportSuffix = `imports where removed in Sanity v3. Please refer to the migration guide: ${urls.migrationGuideStudio}, or new API-reference docs: ${urls.refDocs}` 9 | 10 | export async function validateImports({basePath}: {basePath: string}): Promise { 11 | log.debug('Running ESLint with Sanity Studio import hints...') 12 | const eslint = new ESLint({ 13 | cwd: basePath, 14 | overrideConfig: { 15 | ignorePatterns: ['node_modules'], 16 | rules: { 17 | 'no-restricted-imports': [ 18 | 'error', 19 | { 20 | patterns: [ 21 | ...mergedPackages.map((packageName) => ({ 22 | group: [`${packageName}*`], 23 | message: `Use sanity instead of ${packageName}.`, 24 | })), 25 | { 26 | group: ['config:*'], 27 | message: `config: imports are no longer supported. Please see the new plugin API for alternatives: ${urls.migrationGuideStudio}`, 28 | }, 29 | { 30 | group: ['part:*'], 31 | message: `part: ${removedImportSuffix}`, 32 | }, 33 | { 34 | group: ['all:part:*'], 35 | message: `all:part: ${removedImportSuffix}`, 36 | }, 37 | { 38 | group: ['sanity:*'], 39 | message: `sanity: ${removedImportSuffix}`, 40 | }, 41 | ], 42 | }, 43 | ], 44 | }, 45 | }, 46 | }) 47 | 48 | try { 49 | const results = await eslint.lintFiles([path.join(basePath, '**/*.{js,jsx,ts,tsx}')]) 50 | 51 | const onlyImportErrors = results 52 | .map((r) => { 53 | const limitErrors = r.messages.filter((m) => m.ruleId === 'no-restricted-imports') 54 | return { 55 | ...r, 56 | messages: limitErrors, 57 | errorCount: limitErrors.length, 58 | } 59 | }) 60 | .filter((r) => r.errorCount) 61 | 62 | if (onlyImportErrors.length) { 63 | const formatter = await eslint.loadFormatter('stylish') 64 | const resultText = await formatter.format(onlyImportErrors) 65 | 66 | const addtionalInfo = outdent` 67 | ESLint detected Studio V2 imports that are no longer available. 68 | It is recommended configure @sanity/eslint-config-no-v2-imports for ESLint. 69 | 70 | Run: 71 | npm install --save-dev @sanity/eslint-config-no-v2-imports 72 | 73 | In .eslintrc add: 74 | "extends": ["@sanity/no-v2-imports"] 75 | 76 | This way, V2-imports can be identified directly in the IDE, or using eslint CLI. 77 | For more, see ${urls.linterPackage} 78 | 79 | If the plugin package does not use eslint, disable this check. 80 | ` 81 | return [resultText + addtionalInfo] 82 | } 83 | } catch (e) { 84 | log.error('Failed to run eslint check', e) 85 | return [ 86 | outdent` 87 | Failed to run ESLint. Is ESLint configured? 88 | 89 | If the package does not use eslint, disable this check. 90 | `, 91 | ] 92 | } 93 | 94 | return [] 95 | } 96 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /src/npm/manager.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import {prompt} from '../util/prompt' 3 | 4 | export function npmIsAvailable() { 5 | return execa('npm', ['-v']) 6 | .then(() => true) 7 | .catch(() => false) 8 | } 9 | 10 | export function yarnIsAvailable() { 11 | return execa('yarn', ['-v']) 12 | .then(() => true) 13 | .catch(() => false) 14 | } 15 | 16 | export function pnpmAvailable() { 17 | return execa('pnpm', ['-v']) 18 | .then(() => true) 19 | .catch(() => false) 20 | } 21 | 22 | export async function promptForPackageManager() { 23 | const [npm, yarn, pnpm] = await Promise.all([ 24 | npmIsAvailable(), 25 | yarnIsAvailable(), 26 | pnpmAvailable(), 27 | ]) 28 | 29 | const choices = [npm && 'npm', yarn && 'yarn', pnpm && 'pnpm'].filter(Boolean) 30 | if (choices.length < 2) { 31 | return choices[0] || 'npm' 32 | } 33 | 34 | return prompt('Which package manager do you prefer?', { 35 | choices: choices.map((value) => ({value, name: value})), 36 | default: choices[0], 37 | }) 38 | } 39 | 40 | export async function installDependencies(pm: string, {cwd}: {cwd?: string}) { 41 | const proc = execa(pm, ['install'], {cwd, stdio: 'inherit'}) 42 | const {exitCode} = await proc 43 | return exitCode <= 0 44 | } 45 | -------------------------------------------------------------------------------- /src/npm/publish.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | // @ts-expect-error missing types 3 | import npmPacklist from 'npm-packlist' 4 | 5 | export function getPublishableFiles(basePath: string) { 6 | return npmPacklist({basePath}).then((files: string[]) => 7 | files.map((file) => path.normalize(file)), 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/npm/resolveLatestVersions.ts: -------------------------------------------------------------------------------- 1 | import pProps from 'p-props' 2 | import getLatestVersion from 'get-latest-version' 3 | 4 | // We may want to lock certain dependencies to specific versions 5 | const lockedDependencies: Record = { 6 | 'styled-components': '^6.1', 7 | eslint: '^8.57.0', 8 | } 9 | 10 | export function resolveLatestVersions(packages: string[]) { 11 | const versions: Record = {} 12 | for (const pkgName of packages) { 13 | versions[pkgName] = pkgName in lockedDependencies ? lockedDependencies[pkgName] : 'latest' 14 | } 15 | 16 | return pProps( 17 | versions, 18 | async (range, pkgName) => { 19 | const version = await getLatestVersion(pkgName, {range}) 20 | if (!version) { 21 | throw new Error(`Found no version for ${pkgName}`) 22 | } 23 | return rangeify(version) 24 | }, 25 | {concurrency: 8}, 26 | ) 27 | } 28 | 29 | function rangeify(version: string) { 30 | return `^${version}` 31 | } 32 | -------------------------------------------------------------------------------- /src/presets/presets.ts: -------------------------------------------------------------------------------- 1 | import {InjectOptions} from '../actions/inject' 2 | import {semverWorkflowPreset} from './semver-workflow' 3 | import {renovatePreset} from './renovatebot' 4 | import {ui} from './ui' 5 | import {uiWorkshop} from './ui-workshop' 6 | 7 | export interface Preset { 8 | name: string 9 | description: string 10 | apply: (options: InjectOptions) => Promise 11 | } 12 | 13 | const presets: Preset[] = [semverWorkflowPreset, renovatePreset, ui, uiWorkshop] 14 | const presetNames = presets.map((p) => p?.name) 15 | 16 | export function presetHelpList(padStart: number) { 17 | return presets 18 | .map((p) => `${''.padStart(padStart)}${p.name.padEnd(20)}${p.description}`) 19 | .join('\n') 20 | } 21 | 22 | export async function injectPresets(options: InjectOptions) { 23 | if (options.flags.presetOnly && !options.flags.preset?.length) { 24 | throw new Error('--preset-only, but no --preset [preset-name] was provided.') 25 | } 26 | 27 | const applyPresets = presetsFromInput(options.flags.preset) 28 | for (const preset of applyPresets) { 29 | await preset.apply(options) 30 | } 31 | } 32 | 33 | function presetsFromInput(inputPresets: string[] | undefined): Preset[] { 34 | if (!inputPresets) { 35 | return [] 36 | } 37 | const unknownPresets = inputPresets.filter((p) => !presetNames.includes(p)) 38 | if (unknownPresets.length) { 39 | throw new Error( 40 | `Unknown --preset(s): [${unknownPresets.join(', ')}]. Must be one of: [${presetNames.join( 41 | ', ', 42 | )}]`, 43 | ) 44 | } 45 | 46 | return inputPresets 47 | .filter(onlyUnique) 48 | .map((presetName) => presets.find((p) => p.name === presetName)) 49 | .filter((p): p is Preset => !!p) 50 | } 51 | 52 | function onlyUnique(value: string, index: number, arr: string[]) { 53 | return arr.indexOf(value) === index 54 | } 55 | -------------------------------------------------------------------------------- /src/presets/renovatebot.ts: -------------------------------------------------------------------------------- 1 | import {Preset} from './presets' 2 | import {InjectOptions, writeAssets} from '../actions/inject' 3 | 4 | export const renovatePreset: Preset = { 5 | name: 'renovatebot', 6 | description: 'Files to enable renovatebot.', 7 | apply: applyPreset, 8 | } 9 | 10 | async function applyPreset(options: InjectOptions) { 11 | await writeAssets( 12 | [ 13 | { 14 | type: 'copy', 15 | from: ['renovatebot', 'renovate.json'], 16 | to: 'renovate.json', 17 | }, 18 | ], 19 | options, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/presets/semver-workflow.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, InjectOptions, writeAssets} from '../actions/inject' 2 | import {resolveLatestVersions} from '../npm/resolveLatestVersions' 3 | import {Preset} from './presets' 4 | import { 5 | addPackageJsonScripts, 6 | addScript, 7 | getPackage, 8 | sortKeys, 9 | writePackageJsonDirect, 10 | } from '../npm/package' 11 | import log from '../util/log' 12 | import outdent from 'outdent' 13 | import chalk from 'chalk' 14 | import path from 'path' 15 | import {readFile, writeFile} from '../util/files' 16 | import {errorToUndefined} from '../util/errorToUndefined' 17 | import {PackageJson} from '../actions/verify/types' 18 | import { 19 | developTestSnippet, 20 | getLicenseText, 21 | installationSnippet, 22 | v3BannerNotice, 23 | } from '../util/readme' 24 | import {getUserInfo} from '../util/user' 25 | 26 | export const semverWorkflowPreset: Preset = { 27 | name: 'semver-workflow', 28 | description: 29 | 'Files and dependencies for conventional-commits, github workflow and semantic-release.', 30 | apply: applyPreset, 31 | } 32 | 33 | const info = (write: boolean, msg: string, ...args: string[]) => write && log.info(msg, ...args) 34 | 35 | async function applyPreset(options: InjectOptions) { 36 | await writeAssets(semverWorkflowFiles(), options) 37 | await addPrepareScript(options) 38 | await addDevDependencies(options) 39 | await updateReadme(options) 40 | } 41 | 42 | async function addPrepareScript(options: InjectOptions) { 43 | const pkg = await getPackage(options) 44 | const didWrite = await addPackageJsonScripts(pkg, options, (scripts) => { 45 | scripts.prepare = addScript(`husky`, scripts.prepare) 46 | return scripts 47 | }) 48 | info(didWrite, 'Added prepare script to package.json') 49 | } 50 | 51 | async function addDevDependencies(options: InjectOptions) { 52 | const pkg = await getPackage(options) 53 | const devDeps = sortKeys({ 54 | ...pkg.devDependencies, 55 | ...(await semverWorkflowDependencies()), 56 | }) 57 | const newPkg = {...pkg} 58 | newPkg.devDependencies = devDeps 59 | await writePackageJsonDirect(newPkg, options) 60 | log.info('Updated devDependencies.') 61 | 62 | log.info( 63 | chalk.green( 64 | outdent` 65 | semantic-release preset injected. 66 | 67 | Please confer 68 | https://github.com/sanity-io/plugin-kit/blob/main/docs/semver-workflow.md#manual-steps-after-inject 69 | to finalize configuration for this preset. 70 | `.trim(), 71 | ), 72 | ) 73 | } 74 | 75 | async function updateReadme(options: InjectOptions) { 76 | const {basePath} = options 77 | 78 | const readmePath = path.join(basePath, 'README.md') 79 | const readme = (await readFile(readmePath, 'utf8').catch(errorToUndefined)) ?? '' 80 | 81 | const {v3Banner, install, usage, developTest, license, releaseSnippet} = 82 | await readmeSnippets(options) 83 | 84 | const prependSections = missingSections(readme, [v3Banner, install, usage]) 85 | const appendSections = missingSections(readme, [license, developTest, releaseSnippet]) 86 | 87 | if (prependSections.length || appendSections.length) { 88 | const updatedReadme = [...prependSections, readme, ...appendSections] 89 | .filter(Boolean) 90 | .join('\n\n') 91 | await writeFile(readmePath, updatedReadme, {encoding: 'utf8'}) 92 | log.info('Updated README. Please review the changes.') 93 | } 94 | } 95 | 96 | async function readmeSnippets(options: InjectOptions) { 97 | const pkg = await getPackage(options) 98 | const user = await getUserInfo(options, pkg) 99 | 100 | const bestEffortUrl = readmeBaseurl(pkg) 101 | 102 | const install = installationSnippet(pkg.name ?? 'unknown') 103 | 104 | const usage = outdent` 105 | ## Usage 106 | ` 107 | 108 | const license = getLicenseText(typeof pkg.license === 'string' ? pkg.license : undefined, user) 109 | 110 | const releaseSnippet = outdent` 111 | ### Release new version 112 | 113 | Run ["CI & Release" workflow](${bestEffortUrl}/actions/workflows/main.yml). 114 | Make sure to select the main branch and check "Release new version". 115 | 116 | Semantic release will only release on configured branches, so it is safe to run release on any branch. 117 | ` 118 | 119 | return { 120 | v3Banner: v3BannerNotice(), 121 | install, 122 | usage, 123 | license, 124 | developTest: developTestSnippet(), 125 | releaseSnippet, 126 | } 127 | } 128 | 129 | /** 130 | * Returns sections that does not exists "close enough" in readme 131 | */ 132 | export function missingSections(readme: string, sections: string[]) { 133 | return sections.filter((section) => !closeEnough(section, readme)) 134 | } 135 | 136 | /** 137 | * a and b are considered "close enough" if > 50% of a lines exist in b lines 138 | * @param a 139 | * @param b 140 | */ 141 | function closeEnough(a: string, b: string) { 142 | const aLines = a.split('\n') 143 | const bLines = b.split('\n') 144 | 145 | const matchingLines = aLines.filter((line) => bLines.find((bLine) => bLine === line)).length 146 | const isCloseEnough = matchingLines >= aLines.length * 0.5 147 | return isCloseEnough 148 | } 149 | 150 | function semverWorkflowFiles(): Injectable[] { 151 | const base: Injectable[] = [ 152 | { 153 | type: 'copy', 154 | from: ['.github', 'workflows', 'main.yml'], 155 | to: ['.github', 'workflows', 'main.yml'], 156 | }, 157 | {type: 'copy', from: ['.husky', 'commit-msg'], to: ['.husky', 'commit-msg']}, 158 | {type: 'copy', from: ['.husky', 'pre-commit'], to: ['.husky', 'pre-commit']}, 159 | {type: 'copy', from: ['.releaserc.json'], to: '.releaserc.json'}, 160 | {type: 'copy', from: ['commitlint.template.js'], to: 'commitlint.config.js'}, 161 | {type: 'copy', from: ['lint-staged.template.js'], to: 'lint-staged.config.js'}, 162 | ] 163 | 164 | return base.map((fromTo) => { 165 | if (fromTo.type === 'copy') { 166 | return { 167 | ...fromTo, 168 | from: ['semver-workflow', ...fromTo.from], 169 | } 170 | } 171 | 172 | return fromTo 173 | }) 174 | } 175 | 176 | async function semverWorkflowDependencies(): Promise> { 177 | return resolveLatestVersions([ 178 | '@commitlint/cli', 179 | '@commitlint/config-conventional', 180 | '@sanity/semantic-release-preset', 181 | 'husky', 182 | 'lint-staged', 183 | ]) 184 | } 185 | 186 | export function readmeBaseurl(pkg: PackageJson) { 187 | return ((pkg.repository?.url ?? pkg.homepage ?? 'TODO') as string) 188 | .replace(/.+:\/\//g, 'https://') 189 | .replace(/\.git/g, '') 190 | .replace(/git@github.com\//g, 'github.com/') 191 | .replace(/git@github.com:/g, 'https://github.com/') 192 | .replace(/#.+/g, '') 193 | } 194 | -------------------------------------------------------------------------------- /src/presets/ui-workshop.ts: -------------------------------------------------------------------------------- 1 | import {Preset} from './presets' 2 | import {Injectable, InjectOptions, writeAssets} from '../actions/inject' 3 | import {getPackage, sortKeys, writePackageJsonDirect} from '../npm/package' 4 | import log from '../util/log' 5 | import chalk from 'chalk' 6 | import outdent from 'outdent' 7 | import {resolveLatestVersions} from '../npm/resolveLatestVersions' 8 | import path from 'path' 9 | import {readFile, writeFile} from '../util/files' 10 | import {errorToUndefined} from '../util/errorToUndefined' 11 | 12 | export const uiWorkshop: Preset = { 13 | name: 'ui-workshop', 14 | description: 'Files for testing custom components with @sanity/ui-workshop', 15 | apply: applyPreset, 16 | } 17 | 18 | async function applyPreset(options: InjectOptions) { 19 | await writeAssets(files(), options) 20 | await addDevDependencies(options) 21 | await updateGitIgnore(options) 22 | log.info( 23 | chalk.green( 24 | outdent` 25 | ui-workshop preset injected. 26 | 27 | Please confer 28 | https://github.com/sanity-io/plugin-kit/blob/main/docs/ui-workshop.md#manual-steps-after-inject 29 | to finalize configuration for this preset. 30 | `.trim(), 31 | ), 32 | ) 33 | } 34 | 35 | function files(): Injectable[] { 36 | const base: Injectable[] = [ 37 | {type: 'copy', from: ['workshop.config.ts'], to: ['workshop.config.ts']}, 38 | {type: 'copy', from: ['src', 'CustomField.tsx'], to: ['src', 'CustomField.tsx']}, 39 | { 40 | type: 'copy', 41 | from: ['src', '__workshop__', 'index.tsx'], 42 | to: ['src', '__workshop__', 'index.tsx'], 43 | }, 44 | { 45 | type: 'copy', 46 | from: ['src', '__workshop__', 'props.tsx'], 47 | to: ['src', '__workshop__', 'props.tsx'], 48 | }, 49 | ] 50 | 51 | return base.map((fromTo) => { 52 | if (fromTo.type === 'copy') { 53 | return { 54 | ...fromTo, 55 | from: ['ui-workshop', ...fromTo.from], 56 | } 57 | } 58 | 59 | return fromTo 60 | }) 61 | } 62 | 63 | async function updateGitIgnore(options: InjectOptions) { 64 | const {basePath} = options 65 | const gitignorePath = path.join(basePath, '.gitignore') 66 | let gitignore = (await readFile(gitignorePath, 'utf8').catch(errorToUndefined)) ?? '' 67 | const value = '.workshop' 68 | if (gitignore.includes(value)) { 69 | return 70 | } 71 | 72 | gitignore += `\n\n${value}` 73 | await writeFile(gitignorePath, gitignore, {encoding: 'utf8'}) 74 | } 75 | 76 | async function addDevDependencies(options: InjectOptions) { 77 | const pkg = await getPackage(options) 78 | const devDeps = sortKeys({ 79 | ...pkg.devDependencies, 80 | ...(await devDependencies()), 81 | }) 82 | const newPkg = {...pkg} 83 | newPkg.devDependencies = devDeps 84 | await writePackageJsonDirect(newPkg, options) 85 | log.info('Updated devDependencies.') 86 | } 87 | 88 | async function devDependencies(): Promise> { 89 | return resolveLatestVersions([ 90 | '@sanity/ui-workshop', 91 | '@sanity/icons', 92 | '@sanity/ui', 93 | 'react', 94 | 'react-dom', 95 | 'styled-components', 96 | ]) 97 | } 98 | -------------------------------------------------------------------------------- /src/presets/ui.ts: -------------------------------------------------------------------------------- 1 | import {Preset} from './presets' 2 | import {InjectOptions} from '../actions/inject' 3 | import {forceDependencyVersions, getPackage, sortKeys, writePackageJsonDirect} from '../npm/package' 4 | import log from '../util/log' 5 | import chalk from 'chalk' 6 | import {resolveLatestVersions} from '../npm/resolveLatestVersions' 7 | import {forcedDevPackageVersions, forcedPackageVersions} from '../configs/forced-package-versions' 8 | 9 | export const ui: Preset = { 10 | name: 'ui', 11 | description: '`@sanity/ui` and dependencies', 12 | apply: applyPreset, 13 | } 14 | 15 | async function applyPreset(options: InjectOptions) { 16 | await addDependencies(options) 17 | await addDevDependencies(options) 18 | 19 | log.info(chalk.green('ui preset injected')) 20 | } 21 | 22 | async function addDependencies(options: InjectOptions) { 23 | const pkg = await getPackage(options) 24 | const newDeps = sortKeys( 25 | forceDependencyVersions( 26 | { 27 | ...pkg.dependencies, 28 | ...(await resolveDependencyList()), 29 | }, 30 | forcedPackageVersions, 31 | ), 32 | ) 33 | const newPkg = {...pkg} 34 | newPkg.dependencies = newDeps 35 | await writePackageJsonDirect(newPkg, options) 36 | log.info('Updated dependencies.') 37 | } 38 | 39 | async function addDevDependencies(options: InjectOptions) { 40 | const pkg = await getPackage(options) 41 | const newDeps = sortKeys( 42 | forceDependencyVersions( 43 | { 44 | ...pkg.devDependencies, 45 | ...(await resolveDevDependencyList()), 46 | }, 47 | forcedDevPackageVersions, 48 | ), 49 | ) 50 | const newPkg = {...pkg} 51 | newPkg.devDependencies = newDeps 52 | await writePackageJsonDirect(newPkg, options) 53 | log.info('Updated devDependencies.') 54 | } 55 | 56 | async function resolveDependencyList(): Promise> { 57 | return resolveLatestVersions(['@sanity/icons', '@sanity/ui']) 58 | } 59 | 60 | async function resolveDevDependencyList(): Promise> { 61 | return resolveLatestVersions([ 62 | // install the peer dependencies of `@sanity/ui` as dev dependencies 63 | 'react', 64 | 'react-dom', 65 | 'styled-components', 66 | ]) 67 | } 68 | -------------------------------------------------------------------------------- /src/sharedFlags.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | debug: { 3 | default: false, 4 | type: 'boolean', 5 | }, 6 | silent: { 7 | type: 'boolean', 8 | default: false, 9 | }, 10 | verbose: { 11 | type: 'boolean', 12 | default: false, 13 | }, 14 | } as const 15 | -------------------------------------------------------------------------------- /src/util/command-parser.ts: -------------------------------------------------------------------------------- 1 | import childProcess from 'child_process' 2 | import npmRunPath from 'npm-run-path' 3 | import log from './log' 4 | 5 | interface Command { 6 | command: string 7 | args: string[] 8 | } 9 | 10 | export function parseCommand(commandString: string): Command { 11 | const normalized = commandString.replace(/ +/g, ' ') 12 | const commandAndArg = normalized.split(' ') 13 | return { 14 | command: commandAndArg[0], 15 | args: commandAndArg.length > 1 ? commandAndArg.slice(1) : [], 16 | } 17 | } 18 | 19 | export async function runCommand(commandString: string): Promise<{code: number}> { 20 | log.info(`Running command: ${commandString}`) 21 | const {command, args} = parseCommand(commandString) 22 | 23 | let options: any = {stdio: 'inherit', env: npmRunPath.env()} 24 | 25 | // ref: https://stackoverflow.com/questions/37459717/error-spawn-enoent-on-windows/37487465 26 | options = process.platform === 'win32' ? {...options, shell: true} : options 27 | 28 | return new Promise((resolve, reject) => { 29 | childProcess 30 | .spawn(command, args, options) 31 | .on('error', reject) 32 | .on('close', (exitCode) => { 33 | resolve({code: exitCode ?? 0}) 34 | }) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/util/errorToUndefined.ts: -------------------------------------------------------------------------------- 1 | export function errorToUndefined(err: any) { 2 | if (err instanceof TypeError) { 3 | throw err 4 | } 5 | 6 | return undefined 7 | } 8 | -------------------------------------------------------------------------------- /src/util/files.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import util from 'util' 4 | import pAny from 'p-any' 5 | import crypto from 'crypto' 6 | import {buildExtensions} from '../configs/buildExtensions' 7 | import {prompt} from './prompt' 8 | import {InitFlags} from '../actions/init' 9 | import log from './log' 10 | import json5 from 'json5' 11 | import {ManifestPaths} from '../sanity/manifest' 12 | 13 | export const stat = util.promisify(fs.stat) 14 | export const mkdir = util.promisify(fs.mkdir) 15 | export const readdir = util.promisify(fs.readdir) 16 | export const copyFile = util.promisify(fs.copyFile) 17 | export const readFile = util.promisify(fs.readFile) 18 | export const writeFile = util.promisify(fs.writeFile) 19 | 20 | export function hasSourceEquivalent(compiledFile: string, paths: ManifestPaths) { 21 | if (!paths.source) { 22 | return fileExists( 23 | path.isAbsolute(compiledFile) ? compiledFile : path.resolve(paths.basePath, compiledFile), 24 | ) 25 | } 26 | 27 | // /plugin/dist/MyComponent.js => /plugin/src 28 | const baseDir = path.dirname(compiledFile.replace(paths.compiled as string, paths.source)) 29 | 30 | // /plugin/dist/MyComponent.js => MyComponent 31 | const baseName = path.basename(compiledFile, path.extname(compiledFile)) 32 | 33 | // MyComponent => /plugin/src/MyComponent 34 | const pathStub = path.join(baseDir, baseName) 35 | 36 | /* 37 | * /plugin/src/MyComponent => [ 38 | * /plugin/src/MyComponent.jsx, 39 | * /plugin/src/MyComponent.mjs, 40 | * ... 41 | * ] 42 | */ 43 | return buildCandidateExists(pathStub) 44 | } 45 | 46 | // Generally used for parts resolving 47 | export async function hasSourceFile(filePath: string, paths?: ManifestPaths) { 48 | if (!paths?.source) { 49 | return fileExists( 50 | path.isAbsolute(filePath) ? filePath : path.resolve(paths?.basePath ?? '', filePath), 51 | ) 52 | } 53 | 54 | // filePath: components/SomeInput 55 | // paths: {source: '/plugin/src'} 56 | // MyComponent => /plugin/src/MyComponent 57 | const pathStub = path.isAbsolute(filePath) ? filePath : path.resolve(paths.source, filePath) 58 | 59 | if (await fileExists(pathStub)) { 60 | return true 61 | } 62 | 63 | return buildCandidateExists(pathStub) 64 | } 65 | 66 | // Generally used for parts resolving 67 | export function hasCompiledFile(filePath: string, paths?: ManifestPaths) { 68 | if (!paths?.compiled) { 69 | return fileExists( 70 | path.isAbsolute(filePath) ? filePath : path.resolve(paths?.basePath ?? '', filePath), 71 | ) 72 | } 73 | 74 | // filePath: components/SomeInput 75 | // paths: {compiled: '/plugin/dist'} 76 | 77 | // components/SomeInput => /plugin/dist/components/SomeInput 78 | const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(paths.compiled, filePath) 79 | 80 | // /plugin/dist/components/SomeInput => /plugin/dist/components/SomeInput.js 81 | // /plugin/dist/components/SomeInput.js => /plugin/dist/components/SomeInput.js 82 | // /plugin/dist/components/SomeInput.css => /plugin/dist/components/SomeInput.css 83 | const fileExt = path.extname(absPath) 84 | const withExt = fileExt === '' ? `${absPath}.js` : absPath 85 | 86 | return fileExists(withExt) 87 | } 88 | 89 | export function buildCandidateExists(pathStub: string) { 90 | const candidates = buildExtensions.map((extCandidate) => `${pathStub}${extCandidate}`) 91 | 92 | return pAny(candidates.map((candidate) => stat(candidate))) 93 | .then(() => true) 94 | .catch(() => false) 95 | } 96 | 97 | export function fileExists(filePath: string) { 98 | return stat(filePath) 99 | .then(() => true) 100 | .catch(() => false) 101 | } 102 | 103 | export async function readJsonFile(filePath: string) { 104 | const content = await readFile(filePath, 'utf8') 105 | return JSON.parse(content) as T 106 | } 107 | 108 | export function writeJsonFile(filePath: string, content: Record) { 109 | const data = JSON.stringify(content, null, 2) + '\n' 110 | return writeFile(filePath, data, {encoding: 'utf8'}) 111 | } 112 | 113 | export async function writeFileWithOverwritePrompt( 114 | filePath: string, 115 | content: string, 116 | options: {default?: any; force?: boolean} & fs.ObjectEncodingOptions, 117 | ) { 118 | const {default: defaultVal, force = false, ...writeOptions} = options 119 | const withinCwd = filePath.startsWith(process.cwd()) 120 | const printablePath = withinCwd ? path.relative(process.cwd(), filePath) : filePath 121 | 122 | if (await fileEqualsData(filePath, content)) { 123 | return false 124 | } 125 | 126 | if ( 127 | !force && 128 | (await fileExists(filePath)) && 129 | !(await prompt(`File "${printablePath}" already exists. Overwrite?`, { 130 | type: 'confirm', 131 | default: defaultVal, 132 | })) 133 | ) { 134 | return false 135 | } 136 | 137 | await writeFile(filePath, content, writeOptions) 138 | return true 139 | } 140 | 141 | export async function copyFileWithOverwritePrompt(from: string, to: string, flags: InitFlags) { 142 | const withinCwd = to.startsWith(process.cwd()) 143 | const printablePath = withinCwd ? path.relative(process.cwd(), to) : to 144 | 145 | if (await filesAreEqual(from, to)) { 146 | return false 147 | } 148 | 149 | await ensureDirectoryExists(to) 150 | 151 | if ( 152 | !flags.force && 153 | (await fileExists(to)) && 154 | !(await prompt(`File "${printablePath}" already exists. Overwrite?`, { 155 | type: 'confirm', 156 | default: false, 157 | })) 158 | ) { 159 | return false 160 | } 161 | 162 | await copyFile(from, to) 163 | return true 164 | } 165 | 166 | async function ensureDirectoryExists(filePath: string) { 167 | const dirname = path.dirname(filePath) 168 | if (await fileExists(dirname)) { 169 | return true 170 | } 171 | await ensureDirectoryExists(dirname) 172 | await mkdir(dirname) 173 | } 174 | 175 | export async function fileEqualsData(filePath: string, content: string) { 176 | const contentHash = crypto.createHash('sha1').update(content).digest('hex') 177 | const remoteHash = await getFileHash(filePath) 178 | return contentHash === remoteHash 179 | } 180 | 181 | export async function filesAreEqual(file1: string, file2: string) { 182 | const [hash1, hash2] = await Promise.all([getFileHash(file1, false), getFileHash(file2)]) 183 | return hash1 === hash2 184 | } 185 | 186 | export function getFileHash(filePath: string, allowMissing = true) { 187 | return new Promise((resolve, reject) => { 188 | const hash = crypto.createHash('sha1') 189 | const stream = fs.createReadStream(filePath) 190 | stream.on('error', (err) => { 191 | if ((err as unknown as {code?: string}).code === 'ENOENT' && allowMissing) { 192 | resolve(null) 193 | } else { 194 | reject(err) 195 | } 196 | }) 197 | 198 | stream.on('end', () => resolve(hash.digest('hex'))) 199 | stream.on('data', (chunk) => hash.update(chunk)) 200 | }) 201 | } 202 | 203 | export async function ensureDir(dirPath: string) { 204 | try { 205 | await mkdir(dirPath) 206 | } catch (err) { 207 | if ((err as unknown as {code?: string}).code !== 'EEXIST') { 208 | throw err 209 | } 210 | } 211 | } 212 | 213 | export async function isEmptyish(dirPath: string) { 214 | const ignoredFiles = ['.git', '.gitignore', 'license', 'readme.md'] 215 | const allFiles = await readdir(dirPath).catch(() => []) 216 | const files = allFiles.filter((file) => !ignoredFiles.includes(file.toLowerCase())) 217 | return files.length === 0 218 | } 219 | 220 | export async function readFileContent({ 221 | filename, 222 | basePath, 223 | }: { 224 | filename: string 225 | basePath: string 226 | }): Promise { 227 | const filepath = path.normalize(path.join(basePath, filename)) 228 | try { 229 | return await readFile(filepath, 'utf8') 230 | } catch (err: any) { 231 | if (err.code === 'ENOENT') { 232 | log.debug(`No ${filename} file found.`) 233 | return undefined 234 | } 235 | throw new Error(`Failed to read "${filepath}": ${err.message}`) 236 | } 237 | } 238 | 239 | export async function readJson5File({ 240 | filename, 241 | basePath, 242 | }: { 243 | filename: string 244 | basePath: string 245 | }): Promise { 246 | const content = await readFileContent({filename, basePath}) 247 | if (!content) { 248 | return undefined 249 | } 250 | 251 | return parseJson5(content, filename) 252 | } 253 | 254 | export function parseJson5(content: string, errorKey: string): T { 255 | try { 256 | return json5.parse(content) 257 | } catch (err: any) { 258 | throw new Error(`Error parsing "${errorKey}": ${err.message}`) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/util/log.ts: -------------------------------------------------------------------------------- 1 | // Note: This is _specifically_ meant for CLI usage, 2 | // I realize that "singletons" are bad. 3 | 4 | import chalk from 'chalk' 5 | 6 | let beQuiet = false 7 | let beVerbose = false 8 | 9 | function setVerbosity({verbose, silent}: {verbose: boolean; silent: boolean}) { 10 | if (silent) { 11 | beVerbose = false 12 | beQuiet = true 13 | } else if (verbose) { 14 | beVerbose = true 15 | beQuiet = false 16 | } 17 | } 18 | 19 | export default { 20 | setVerbosity: setVerbosity, 21 | 22 | // Bypasses any checks, prints regardless (only use for things like `cli --version`) 23 | msg: (msg: any, ...args: any[]) => !beQuiet && console.log(msg, ...args), 24 | 25 | // Debug only printed on --verbose 26 | debug: (msg: any, ...args: any[]) => 27 | !beQuiet && beVerbose && console.debug(`${chalk.bgBlack.white('[debug]')} ${msg}`, ...args), 28 | 29 | // Success messages only printed if not --silent 30 | success: (msg: any, ...args: any[]) => 31 | !beQuiet && console.info(`${chalk.bgBlack.greenBright('[success]')} ${msg}`, ...args), 32 | 33 | // Info only printed if not --silent ("standard" level) 34 | info: (msg: any, ...args: any[]) => 35 | !beQuiet && console.info(`${chalk.bgBlack.cyanBright('[info]')} ${msg}`, ...args), 36 | 37 | // Warning only printed if not --silent 38 | warn: (msg: any, ...args: any[]) => 39 | !beQuiet && console.warn(`${chalk.bgBlack.yellowBright('[warn]')} ${msg}`, ...args), 40 | 41 | // Errors are always printed 42 | error: (msg: any, ...args: any[]) => 43 | console.error(`${chalk.bgBlack.redBright('[error]')} ${msg}`, ...args), 44 | } 45 | -------------------------------------------------------------------------------- /src/util/prompt.ts: -------------------------------------------------------------------------------- 1 | import {URL} from 'url' 2 | import path from 'path' 3 | import inquirer from 'inquirer' 4 | // @ts-expect-error missing types 5 | import validNpmName from 'validate-npm-package-name' 6 | // @ts-expect-error missing types 7 | import githubUrlToObject from 'github-url-to-object' 8 | import {InjectOptions} from '../actions/inject' 9 | 10 | export async function prompt( 11 | message: string, 12 | options: { 13 | choices?: any 14 | type?: string 15 | default?: any 16 | filter?: (val: any) => any 17 | validate?: (val: any) => boolean | string 18 | }, 19 | ) { 20 | const type = options.choices ? 'list' : options.type 21 | const result = await inquirer.prompt([{...options, type, message, name: 'single'}]) 22 | return result && result.single 23 | } 24 | 25 | prompt.separator = () => new inquirer.Separator() 26 | 27 | export function promptForPackageName({basePath}: InjectOptions, defaultVal?: string) { 28 | return prompt('Plugin name (sanity-plugin-...)', { 29 | default: defaultVal || path.basename(basePath), 30 | filter: (name) => { 31 | const prefixless = name.trim().replace(/^sanity-plugin-/, '') 32 | return name[0] === '@' ? name : `sanity-plugin-${prefixless}` 33 | }, 34 | validate: (name) => { 35 | const valid: {errors?: string[]} = validNpmName(name) 36 | if (valid.errors) { 37 | return valid.errors[0] 38 | } 39 | 40 | if (name[0] !== '@' && name.endsWith('plugin')) { 41 | return `Name shouldn't include "plugin" multiple times (${name})` 42 | } 43 | 44 | return true 45 | }, 46 | }) 47 | } 48 | 49 | export function promptForRepoOrigin(options: InjectOptions, defaultVal?: string) { 50 | return prompt('Git repository URL', { 51 | default: defaultVal, 52 | filter: (raw) => { 53 | const url = (raw || '').trim() 54 | const gh: {user: string; repo: string} | undefined = githubUrlToObject(url) 55 | return gh ? `git+ssh://git@github.com/${gh.user}/${gh.repo}.git` : url 56 | }, 57 | validate: (url) => { 58 | if (!url) { 59 | return true 60 | } 61 | 62 | try { 63 | const parsed = new URL(url) 64 | return parsed ? true : 'Invalid URL' 65 | } catch (err) { 66 | return 'Invalid URL' 67 | } 68 | }, 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /src/util/readme.ts: -------------------------------------------------------------------------------- 1 | import outdent from 'outdent' 2 | // @ts-expect-error missing types 3 | import licenses from '@rexxars/choosealicense-list' 4 | import {PackageData} from '../actions/inject' 5 | import {User} from './user' 6 | 7 | export function generateReadme(data: PackageData) { 8 | const {user, pluginName, license, description} = data 9 | 10 | return ( 11 | outdent` 12 | # ${pluginName} 13 | 14 | ${v3BannerNotice()} 15 | 16 | ${installationSnippet(pluginName ?? 'unknown')} 17 | 18 | ## Usage 19 | 20 | Add it as a plugin in \`sanity.config.ts\` (or .js): 21 | 22 | \`\`\`ts 23 | import {defineConfig} from 'sanity' 24 | import {myPlugin} from '${pluginName}' 25 | 26 | export default defineConfig({ 27 | //... 28 | plugins: [myPlugin({})], 29 | }) 30 | \`\`\` 31 | 32 | ${getLicenseText(license?.id, user?.name ? (user as User) : undefined)} 33 | ${developTestSnippet()} 34 | ` + '\n' 35 | ) 36 | } 37 | 38 | export function v3BannerNotice() { 39 | return `> This is a **Sanity Studio v3** plugin.` 40 | } 41 | 42 | export function installationSnippet(packageName: string) { 43 | return outdent` 44 | ## Installation 45 | 46 | \`\`\`sh 47 | npm install ${packageName} 48 | \`\`\` 49 | ` 50 | } 51 | 52 | export function developTestSnippet() { 53 | return outdent` 54 | ## Develop & test 55 | 56 | This plugin uses [@sanity/plugin-kit](https://github.com/sanity-io/plugin-kit) 57 | with default configuration for build & watch scripts. 58 | 59 | See [Testing a plugin in Sanity Studio](https://github.com/sanity-io/plugin-kit#testing-a-plugin-in-sanity-studio) 60 | on how to run this plugin with hotreload in the studio. 61 | ` 62 | } 63 | 64 | export function getLicenseText(licenseId?: string, user?: User) { 65 | if (!licenseId) { 66 | return '' 67 | } 68 | 69 | let licenseName: string | undefined = licenses.find(licenseId).title 70 | licenseName = licenseName?.replace(/\s+license$/i, '') 71 | 72 | let licenseText = '## License\n' 73 | if (licenseName && user?.name) { 74 | licenseText = `${licenseText}\n[${licenseName}](LICENSE) © ${user?.name}\n` 75 | } else if (licenseName) { 76 | licenseText = `${licenseText}\n[${licenseName}](LICENSE)\n` 77 | } else { 78 | licenseText = `${licenseText}\nSee [LICENSE](LICENSE)` 79 | } 80 | 81 | return licenseText 82 | } 83 | 84 | export function isDefaultGitHubReadme(readme: string) { 85 | if (!readme) { 86 | return false 87 | } 88 | 89 | const lines = readme.split('\n', 20).filter(Boolean) 90 | 91 | // title + _optional_ description 92 | return lines.length <= 2 && lines[0].startsWith('#') 93 | } 94 | -------------------------------------------------------------------------------- /src/util/request.ts: -------------------------------------------------------------------------------- 1 | import {getIt} from 'get-it' 2 | import {jsonRequest, jsonResponse, httpErrors, headers, promise} from 'get-it/middleware' 3 | import pkg from '../../package.json' 4 | 5 | export const request = getIt([ 6 | promise({onlyBody: true}), 7 | jsonRequest(), 8 | jsonResponse(), 9 | httpErrors(), 10 | headers({'User-Agent': `${pkg.name}@${pkg.version}`}), 11 | ]) 12 | -------------------------------------------------------------------------------- /src/util/ts.ts: -------------------------------------------------------------------------------- 1 | import {loadTSConfig} from '@sanity/pkg-utils' 2 | import path from 'path' 3 | import {fileExists} from './files' 4 | 5 | export async function readTSConfig(options: {basePath: string; filename: string}) { 6 | const {basePath, filename} = options 7 | const filePath = path.resolve(basePath, filename) 8 | const exists = await fileExists(filePath) 9 | 10 | if (!exists) return undefined 11 | 12 | return await loadTSConfig({cwd: basePath, tsconfigPath: filename}) 13 | } 14 | -------------------------------------------------------------------------------- /src/util/user.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import xdgBasedir from 'xdg-basedir' 3 | import {getGitUserInfo as _getGitUserInfo} from 'git-user-info' 4 | import {validate as isValidEmail} from 'email-validator' 5 | import {readJsonFile} from './files' 6 | import {request} from './request' 7 | import {prompt} from './prompt' 8 | import {InjectOptions} from '../actions/inject' 9 | import {PackageJson} from '../actions/verify/types' 10 | 11 | export interface User { 12 | name: string 13 | email?: string 14 | } 15 | 16 | export async function getUserInfo( 17 | {requireUserConfirmation, flags}: InjectOptions, 18 | pkg?: PackageJson, 19 | ): Promise { 20 | const userInfo = 21 | getPackageUserInfo({author: flags.author ?? pkg?.author}) || 22 | (await getSanityUserInfo()) || 23 | ((await getGitUserInfo()) as User | undefined) 24 | if (requireUserConfirmation) { 25 | return promptForInfo(userInfo) 26 | } 27 | 28 | return userInfo 29 | } 30 | 31 | export function getPackageUserInfo(pkg?: { 32 | author?: 33 | | string 34 | | { 35 | name: string 36 | email?: string 37 | } 38 | }): User | undefined { 39 | let author = pkg?.author 40 | if (!author) { 41 | return undefined 42 | } 43 | 44 | if (author && typeof author !== 'string') { 45 | return author 46 | } else if (!author.includes('@')) { 47 | return {name: author} 48 | } 49 | 50 | const [pre, ...post] = author.replace(/[<>[\]]/g, '').split(/@/) 51 | const nameParts = pre.split(/\s+/) 52 | const email = [nameParts[nameParts.length - 1], ...post].join('@') 53 | const name = nameParts.slice(0, -1).join(' ') 54 | return {name, email} 55 | } 56 | 57 | async function promptForInfo(defValue?: User) { 58 | const name = await prompt('Author name', { 59 | filter: filterString, 60 | default: defValue && defValue.name, 61 | validate: requiredString, 62 | }) 63 | 64 | const email = await prompt('Author email', { 65 | filter: filterString, 66 | default: defValue && defValue.email, 67 | validate: validOrEmptyEmail, 68 | }) 69 | 70 | return {name, email} 71 | } 72 | 73 | async function getSanityUserInfo(): Promise { 74 | try { 75 | const data = await readJsonFile<{authToken?: string}>( 76 | path.join(xdgBasedir.config ?? '', 'sanity', 'config.json'), 77 | ) 78 | const token = data?.authToken 79 | 80 | if (!token) { 81 | return undefined 82 | } 83 | 84 | const user = await request({ 85 | url: 'https://api.sanity.io/v1/users/me', 86 | headers: {Authorization: `Bearer ${token}`}, 87 | }) 88 | 89 | if (!user) { 90 | return undefined 91 | } 92 | 93 | const {name, email} = user 94 | return {name, email} 95 | } catch (err) { 96 | return undefined 97 | } 98 | } 99 | 100 | async function getGitUserInfo(): Promise { 101 | const user = await _getGitUserInfo() 102 | return user ? {name: user.name, email: user.email} : undefined 103 | } 104 | 105 | function filterString(val: string) { 106 | return (val || '').trim() 107 | } 108 | 109 | function requiredString(value: string) { 110 | return value.length > 1 ? true : 'Required' 111 | } 112 | 113 | function validOrEmptyEmail(value: string): true | string { 114 | if (!value) { 115 | return true 116 | } 117 | 118 | return isValidEmail(value) ? true : 'Must either be a valid email or empty' 119 | } 120 | -------------------------------------------------------------------------------- /test/cli.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import tap from 'tap' 3 | import {runCliCommand} from './fixture-utils' 4 | import {cliName} from '../src/constants' 5 | 6 | const normalize = (dirPath: string) => dirPath.replace(/\//g, path.sep) 7 | const helpString = 'These are common commands used in various situations' 8 | 9 | tap.test('shows help if no command is given', async (t) => { 10 | const {stdout, stderr, exitCode} = await runCliCommand('', []) 11 | t.equal(exitCode, 2, 'should have exit code 2') 12 | t.equal(stderr, '') 13 | t.match(stdout, helpString) 14 | t.match(stdout, cliName) 15 | }) 16 | 17 | tap.test('shows error + help on unknown commands', async (t) => { 18 | const {stdout, stderr, exitCode} = await runCliCommand('does-not-exist', []) 19 | t.equal(exitCode, 2, 'should have exit code 2') 20 | t.equal(stderr, 'Unknown command "does-not-exist"') 21 | t.match(stdout, helpString) 22 | t.match(stdout, cliName) 23 | }) 24 | 25 | tap.test('shows error + help when using both --silent and --verbose', async (t) => { 26 | const {stdout, stderr, exitCode} = await runCliCommand('version', ['--silent', '--verbose'], { 27 | reject: false, 28 | }) 29 | t.equal(exitCode, 2, 'should have exit code 2') 30 | t.match(stderr, '--silent and --verbose are mutually exclusive') 31 | t.match(stdout, helpString) 32 | t.match(stdout, cliName) 33 | }) 34 | 35 | tap.test('shows no stack trace without --debug', async (t) => { 36 | const {stdout, stderr, exitCode} = await runCliCommand('version', ['--major', '--minor'], { 37 | reject: false, 38 | }) 39 | t.equal(exitCode, 1, 'should have exit code 1') 40 | t.equal(stdout, '', 'should have empty stdout') 41 | t.match(stderr, 'only one can be used at a time') 42 | t.notMatch(stderr, normalize('/cmds/version.js:')) 43 | }) 44 | 45 | tap.test('shows stack trace with --debug', async (t) => { 46 | const {stdout, stderr, exitCode} = await runCliCommand('version', [ 47 | '--debug', 48 | '--major', 49 | '--minor', 50 | ]) 51 | t.equal(exitCode, 1, 'should have exit code 1') 52 | t.equal(stdout, '', 'should have empty stdout') 53 | t.match(stderr, 'only one can be used at a time') 54 | t.match(stderr, path.normalize('/cmds/version.ts')) 55 | }) 56 | -------------------------------------------------------------------------------- /test/fixture-utils.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import path from 'path' 3 | import fs from 'fs/promises' 4 | import readdirp, {EntryInfo} from 'readdirp' 5 | import rimraf from 'rimraf' 6 | 7 | const baseFixturesDir = path.join(__dirname, 'fixtures') 8 | 9 | export const readFile = (file: string) => fs.readFile(file, 'utf8') 10 | export const onlyPaths = (files: EntryInfo[]) => files.map((file) => file.path) 11 | export const contents = (dir: string) => readdirp.promise(dir).then(onlyPaths) 12 | export const normalize = (dirPath: string) => dirPath.replace(/\//g, path.sep) 13 | 14 | export const pluginTestName = 'sanity-plugin-test-plugin' 15 | 16 | export const initTestArgs = [ 17 | '--force', 18 | '--no-install', 19 | '--name', 20 | pluginTestName, 21 | '--license', 22 | 'mit', 23 | '--author', 24 | 'Test Person ', 25 | '--repo', 26 | 'https://github.com/sanity-io/sanity', 27 | ] 28 | 29 | export async function testFixture({ 30 | fixturePath, 31 | relativeOutPath = 'dist', 32 | command, 33 | assert, 34 | }: { 35 | fixturePath: string 36 | relativeOutPath?: string 37 | command: (args: {fixtureDir: string; outputDir: string}) => Promise 38 | assert: (args: {result: execa.ExecaReturnValue; outputDir: string}) => Promise 39 | }) { 40 | const fixtureDir = path.join(baseFixturesDir, normalize(fixturePath)) 41 | const outputDir = path.join(fixtureDir, normalize(relativeOutPath)) 42 | 43 | await rimraf(outputDir) 44 | 45 | const result = await command({fixtureDir, outputDir}) 46 | await assert({result, outputDir}) 47 | 48 | await rimraf(outputDir) 49 | } 50 | 51 | export function fileContainsValidator( 52 | t: any /* tap types cannot be imported? :shrug: */, 53 | outputDir: string, 54 | ) { 55 | return async (file: string, ...contains: string[]) => { 56 | const fileString = await readFile(path.join(outputDir, normalize(file))) 57 | contains.forEach((content) => t.match(fileString, content, `${file} contains ${content}`)) 58 | } 59 | } 60 | 61 | export function runCliCommand(command: string, args: string[] = [], options?: execa.Options) { 62 | return execa('ts-node', ['run-test-command.ts', command, ...args], { 63 | cwd: __dirname, 64 | reject: false, 65 | ...options, 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /test/fixtures/build/folder-sanity-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-plugin-folder-sanity-json", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Just an invalid thing", 6 | "keywords": [ 7 | "sanity", 8 | "sanity-plugin" 9 | ], 10 | "license": "MIT", 11 | "author": "Some person", 12 | "main": "./src/one.js", 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/build/folder-sanity-json/sanity.json/.gitkeep: -------------------------------------------------------------------------------- 1 | Keep me 2 | -------------------------------------------------------------------------------- /test/fixtures/build/folder-sanity-json/src/one.js: -------------------------------------------------------------------------------- 1 | export default () => 'one' 2 | -------------------------------------------------------------------------------- /test/fixtures/build/plain/LICENSE: -------------------------------------------------------------------------------- 1 | some license 2 | -------------------------------------------------------------------------------- /test/fixtures/build/plain/README.md: -------------------------------------------------------------------------------- 1 | # some cool 2 | -------------------------------------------------------------------------------- /test/fixtures/build/plain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-plugin-plain", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Just a fixture", 6 | "keywords": [ 7 | "sanity", 8 | "sanity-plugin" 9 | ], 10 | "license": "MIT", 11 | "author": "Some person", 12 | "main": "./src/schemaType.js" 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/build/plain/sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/schema-type", 5 | "path": "./src/schemaType.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/build/plain/src/schemaType.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'markdown', 3 | title: 'Markdown', 4 | type: 'string', 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/build/ts/.eslintignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /test/fixtures/build/ts/README.md: -------------------------------------------------------------------------------- 1 | # just a readme 2 | -------------------------------------------------------------------------------- /test/fixtures/build/ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-plugin-ts", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Just a valid typescript Sanity plugin fixture. Do not publish.", 6 | "keywords": [ 7 | "sanity", 8 | "sanity-plugin" 9 | ], 10 | "license": "MIT", 11 | "author": "Some person", 12 | "main": "./dist/one.js", 13 | "types": "types.d.ts", 14 | "files": [ 15 | "dist", 16 | ".eslintignore" 17 | ], 18 | "scripts": { 19 | "test": "echo \"Error: no test specified\" && exit 1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/build/ts/sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "paths": { 3 | "source": "./src", 4 | "compiled": "./dist" 5 | }, 6 | 7 | "parts": [ 8 | { 9 | "name": "part:ts/some/thing", 10 | "path": "one" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/build/ts/src/one.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import two from './two' 3 | import styles from './styles/one.css' 4 | 5 | export default class One extends React.PureComponent { 6 | state = {i: 0} 7 | 8 | handleClick = () => { 9 | this.setState((prev) => ({i: prev.i + 1})) 10 | } 11 | 12 | componentDidMount() { 13 | two() 14 | } 15 | 16 | render() { 17 | const {i} = this.state 18 | return ( 19 | 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/build/ts/src/styles/one.css: -------------------------------------------------------------------------------- 1 | .button { 2 | background: #bf1942; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/build/ts/src/two.ts: -------------------------------------------------------------------------------- 1 | export default function two() { 2 | // do something important 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/build/valid-js/LICENSE: -------------------------------------------------------------------------------- 1 | some license 2 | -------------------------------------------------------------------------------- /test/fixtures/build/valid-js/README.md: -------------------------------------------------------------------------------- 1 | # some cool 2 | -------------------------------------------------------------------------------- /test/fixtures/build/valid-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-plugin-valid", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Just a valid Sanity plugin fixture. Do not publish.", 6 | "keywords": [ 7 | "sanity", 8 | "sanity-plugin" 9 | ], 10 | "license": "MIT", 11 | "author": "Some person", 12 | "exports": { 13 | ".": { 14 | "require": "./dist/cjs/index.js", 15 | "default": "./dist/esm/index.js" 16 | } 17 | }, 18 | "main": "./dist/cjs/index.js", 19 | "module": "./dist/esm/index.js", 20 | "source": "./src/index.js", 21 | "files": [ 22 | "dist", 23 | "src" 24 | ], 25 | "scripts": { 26 | "test": "echo \"Error: no test specified\" && exit 1" 27 | }, 28 | "peerDependencies": { 29 | "react": "^18" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/fixtures/build/valid-js/sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "paths": { 3 | "source": "./src", 4 | "compiled": "./dist" 5 | }, 6 | 7 | "parts": [ 8 | { 9 | "name": "part:valid/some/thing", 10 | "path": "one.js" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/build/valid-js/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import two from './two' 3 | import styles from './styles/one.css' 4 | 5 | export default class One extends React.PureComponent { 6 | // plugin-proposal-class-properties 7 | state = {i: 0} 8 | 9 | // plugin-proposal-class-properties 10 | handleClick = () => { 11 | this.setState((prev) => ({i: prev.i + 1})) 12 | } 13 | 14 | componentDidMount() { 15 | two() 16 | } 17 | 18 | render() { 19 | const {i} = this.state 20 | return ( 21 | 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/build/valid-js/src/styles/one.css: -------------------------------------------------------------------------------- 1 | .button { 2 | background: #bf1942; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/build/valid-js/src/two.js: -------------------------------------------------------------------------------- 1 | export default function two() { 2 | // do something important 3 | console.log('Important stuff') 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/init/empty/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/plugin-kit/519ad2851042cd09e5d0dfe15cfc03c50c74fc5e/test/fixtures/init/empty/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/inject/valid/.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 | -------------------------------------------------------------------------------- /test/fixtures/inject/valid/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": ["sanity", "sanity/typescript", "plugin:prettier/recommended"] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/inject/valid/.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 | # Compiled plugin 53 | dist 54 | -------------------------------------------------------------------------------- /test/fixtures/inject/valid/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/inject/valid/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Test Person 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 | -------------------------------------------------------------------------------- /test/fixtures/inject/valid/README.md: -------------------------------------------------------------------------------- 1 | # sanity-plugin-test-plugin 2 | 3 | ## Installation 4 | 5 | ```sh 6 | npm install sanity-plugin-test-plugin 7 | ``` 8 | 9 | ## Usage 10 | 11 | Install the plugin in your [Sanity Studio](https://sanity.io/studio) configuration 12 | `sanity.config.ts` (or `.js`): 13 | 14 | ```ts 15 | import {defineConfig} from 'sanity' 16 | import {myPlugin} from 'sanity-plugin-test-plugin' 17 | 18 | export default defineConfig({ 19 | // ... 20 | plugins: [myPlugin({})], 21 | }) 22 | ``` 23 | 24 | ## License 25 | 26 | [MIT](LICENSE) © Test Person 27 | -------------------------------------------------------------------------------- /test/fixtures/inject/valid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-plugin-test-plugin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "homepage": "https://github.com/sanity-io/sanity#readme", 6 | "bugs": { 7 | "url": "https://github.com/sanity-io/sanity/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/sanity-io/sanity" 12 | }, 13 | "license": "MIT", 14 | "author": "Test Person ", 15 | "type": "commonjs", 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.ts", 19 | "source": "./src/index.ts", 20 | "require": "./dist/index.js", 21 | "default": "./dist/index.js" 22 | } 23 | }, 24 | "main": "./dist/index.js", 25 | "types": "./dist/index.d.ts", 26 | "files": [ 27 | "dist", 28 | "sanity.json", 29 | "src", 30 | "v2-incompatible.js" 31 | ], 32 | "scripts": { 33 | "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean", 34 | "link-watch": "plugin-kit link-watch", 35 | "lint": "eslint .", 36 | "prepublishOnly": "npm run build", 37 | "watch": "pkg-utils watch --strict" 38 | }, 39 | "dependencies": { 40 | "@sanity/incompatible-plugin": "^0.0.1-studio-v3.1" 41 | }, 42 | "devDependencies": { 43 | "@sanity/pkg-utils": "^2.0.6", 44 | "@sanity/plugin-kit": "^0.0.1-studio-v3.1", 45 | "@typescript-eslint/eslint-plugin": "^5.27.1", 46 | "@typescript-eslint/parser": "^5.27.1", 47 | "eslint": "^8.17.0", 48 | "eslint-config-prettier": "^8.5.0", 49 | "eslint-config-sanity": "^6.0.0", 50 | "eslint-plugin-prettier": "^4.0.0", 51 | "eslint-plugin-react": "^7.30.0", 52 | "eslint-plugin-react-hooks": "^4.5.0", 53 | "prettier": "^2.6.2", 54 | "prettier-plugin-packagejson": "^2.3.0", 55 | "react": "^18.2.0", 56 | "sanity": "^3.0.0", 57 | "typescript": "^4.7.3" 58 | }, 59 | "peerDependencies": { 60 | "react": "^18", 61 | "sanity": "^3.0.0" 62 | }, 63 | "engines": { 64 | "node": ">=18" 65 | }, 66 | "sanityPlugin": { 67 | "verifyPackage": { 68 | "tsc": false 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/fixtures/inject/valid/sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/sanity-root", 5 | "path": "./v2-incompatible.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/inject/valid/src/index.ts: -------------------------------------------------------------------------------- 1 | import {definePlugin} from 'sanity' 2 | 3 | interface MyPluginConfig { 4 | /* nothing here yet */ 5 | } 6 | 7 | /** 8 | * Usage in `sanity.config.ts` (or .js) 9 | * 10 | * ```ts 11 | * import {defineConfig} from 'sanity' 12 | * import {myPlugin} from 'sanity-plugin-test-plugin' 13 | * 14 | * export default defineConfig({ 15 | * // ... 16 | * plugins: [myPlugin({})], 17 | * }) 18 | * ``` 19 | */ 20 | export const myPlugin = definePlugin((config = {}) => { 21 | // eslint-disable-next-line no-console 22 | console.log('hello from sanity-plugin-test-plugin') 23 | return { 24 | name: 'sanity-plugin-test-plugin', 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /test/fixtures/inject/valid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "moduleResolution": "bundler", 5 | "target": "esnext", 6 | "module": "preserve", 7 | "esModuleInterop": true, 8 | "lib": ["es2015", "es2016", "es2017", "dom"], 9 | "strict": true, 10 | "sourceMap": false, 11 | "inlineSourceMap": false, 12 | "downlevelIteration": true, 13 | "declaration": true, 14 | "allowSyntheticDefaultImports": true, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true, 17 | "outDir": "dist", 18 | "skipLibCheck": true, 19 | "isolatedModules": true, 20 | "checkJs": false, 21 | "rootDir": ".", 22 | "emitDeclarationOnly": true 23 | }, 24 | "include": ["src/**/*"] 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/inject/valid/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: undefined, 9 | }, 10 | sanityExchangeUrl, 11 | }) 12 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/plugin-kit/519ad2851042cd09e5d0dfe15cfc03c50c74fc5e/test/fixtures/verify-package/every-failure-possible/.eslintignore -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": ["sanity"] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": ["sanity"] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/.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 | # Compiled plugin 53 | dist 54 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/plugin-kit/519ad2851042cd09e5d0dfe15cfc03c50c74fc5e/test/fixtures/verify-package/every-failure-possible/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Snorre Brekke 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 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/README.md: -------------------------------------------------------------------------------- 1 | # my-plugin 2 | 3 | ## Installation 4 | 5 | ```sh 6 | npm install my-plugin 7 | ``` 8 | 9 | ## Usage 10 | 11 | Add it as a plugin in `sanity.config.ts` (or .js): 12 | 13 | ```ts 14 | import {defineConfig} from 'sanity' 15 | import {myPlugin} from 'my-plugin' 16 | 17 | export default defineConfig({ 18 | // ... 19 | plugins: [myPlugin({})], 20 | }) 21 | ``` 22 | 23 | ## License 24 | 25 | MIT © Snorre Brekke 26 | See LICENSE 27 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/babel.config.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/plugin-kit/519ad2851042cd09e5d0dfe15cfc03c50c74fc5e/test/fixtures/verify-package/every-failure-possible/babel.config.js -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "not-prefixed-with-sanity-plugin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "license": "MIT", 6 | "files": [ 7 | "dist", 8 | "sanity.json", 9 | "src" 10 | ], 11 | "prettier": { 12 | "bracketSpacing": false, 13 | "printWidth": 100, 14 | "semi": false, 15 | "singleQuote": true 16 | }, 17 | "dependencies": {}, 18 | "devDependencies": { 19 | "@sanity/base": "^2.30.1", 20 | "eslint": "^8.17.0", 21 | "eslint-config-prettier": "^8.5.0", 22 | "eslint-config-sanity": "^6.0.0", 23 | "eslint-plugin-react": "^7.30.0", 24 | "eslint-plugin-react-hooks": "^4.5.0", 25 | "parcel": "^2.7.0", 26 | "react": "^18.2.0", 27 | "typescript": "^4.7.3" 28 | }, 29 | "peerDependencies": { 30 | "@sanity/base": "^2.30.1", 31 | "react": "^18" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/rollup.config.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/plugin-kit/519ad2851042cd09e5d0dfe15cfc03c50c74fc5e/test/fixtures/verify-package/every-failure-possible/rollup.config.js -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/schema-type", 5 | "path": "./src/index.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/src/index.tsx: -------------------------------------------------------------------------------- 1 | import {somethingOrOther} from '@sanity/base' 2 | import {someForm} from '@sanity/form-builder' 3 | 4 | export const dummy = { 5 | type: 'document', 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/every-failure-possible/tsconfig.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": ["sanity", "sanity/typescript"] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/README.md: -------------------------------------------------------------------------------- 1 | # Sanity Movies Content Studio 2 | 3 | NOTE on this package: Eslint config modified to work with tests. 4 | 5 | Congratulations, you have now installed the Sanity Content Studio, an open source real-time content editing environment connected to the Sanity backend. 6 | 7 | Now you can do the following things: 8 | 9 | - [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme) 10 | - Check out one of the example frontends: [React](https://github.com/sanity-io/example-frontend-next-js) | [React Native](https://github.com/sanity-io/example-app-react-native) | [Vue](https://github.com/sanity-io/example-frontend-vue-js) | [PHP](https://github.com/sanity-io/example-frontend-silex-twig) 11 | - [Join the community Slack](https://slack.sanity.io/?utm_source=readme) 12 | - [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme) 13 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/config/.checksums: -------------------------------------------------------------------------------- 1 | { 2 | "#": "Used by Sanity to keep track of configuration file checksums, do not delete or modify!", 3 | "@sanity/default-layout": "bb034f391ba508a6ca8cd971967cbedeb131c4d19b17b28a0895f32db5d568ea", 4 | "@sanity/default-login": "e2ed4e51e97331c0699ba7cf9f67cbf76f1c6a5f806d6eabf8259b2bcb5f1002", 5 | "@sanity/form-builder": "b38478227ba5e22c91981da4b53436df22e48ff25238a55a973ed620be5068aa", 6 | "@sanity/data-aspects": "d199e2c199b3e26cd28b68dc84d7fc01c9186bf5089580f2e2446994d36b3cb6", 7 | "@sanity/google-maps-input": "43e62bbd0a78d3dfd363915959160d2dfeafbac941d5a78a0a4c4fd64d9e7638", 8 | "@sanity/vision": "da5b6ed712703ecd04bf4df560570c668aa95252c6bc1c41d6df1bda9b8b8f60" 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/config/@sanity/data-aspects.json: -------------------------------------------------------------------------------- 1 | { 2 | "listOptions": {} 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/config/@sanity/default-layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "toolSwitcher": { 3 | "order": [], 4 | "hidden": [] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/config/@sanity/default-login.json: -------------------------------------------------------------------------------- 1 | { 2 | "providers": { 3 | "mode": "append", 4 | "redirectOnSingle": false, 5 | "entries": [] 6 | }, 7 | "loginMethod": "dual" 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/config/@sanity/form-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": { 3 | "directUploads": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/config/@sanity/google-maps-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": null, 3 | "defaultZoom": 11, 4 | "defaultLocale": null, 5 | "defaultLocation": { 6 | "lat": 40.7058254, 7 | "lng": -74.1180863 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/config/@sanity/vision.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultApiVersion": "2021-10-21" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "throwaway", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "keywords": [ 7 | "sanity" 8 | ], 9 | "license": "UNLICENSED", 10 | "main": "package.json", 11 | "scripts": { 12 | "build": "sanity build", 13 | "start": "sanity start" 14 | }, 15 | "dependencies": { 16 | "@sanity/base": "^2.30.1", 17 | "@sanity/core": "^2.30.1", 18 | "@sanity/default-layout": "^2.30.1", 19 | "@sanity/default-login": "^2.30.1", 20 | "@sanity/desk-tool": "^2.30.1", 21 | "@sanity/eslint-config-studio": "^2.0.0", 22 | "@sanity/google-maps-input": "^2.30.1", 23 | "@sanity/vision": "^2.30.1", 24 | "eslint": "^8.6.0", 25 | "prop-types": "^15.7", 26 | "react": "^17.0", 27 | "react-dom": "^17.0", 28 | "react-icons": "^3.11.0", 29 | "styled-components": "^5.2.0" 30 | }, 31 | "devDependencies": {} 32 | } 33 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | User-specific packages can be placed here 2 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "project": { 4 | "name": "throwaway" 5 | }, 6 | "api": { 7 | "projectId": "q5ivv38k", 8 | "dataset": "production" 9 | }, 10 | "plugins": [ 11 | "@sanity/base", 12 | "@sanity/default-layout", 13 | "@sanity/default-login", 14 | "@sanity/desk-tool", 15 | "@sanity/google-maps-input" 16 | ], 17 | "env": { 18 | "development": { 19 | "plugins": ["@sanity/vision"] 20 | } 21 | }, 22 | "parts": [ 23 | { 24 | "name": "part:@sanity/base/schema", 25 | "path": "./schemas/schema" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/schemas/blockContent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the schema definition for the rich text fields used for 3 | * for this blog studio. When you import it in schemas.js it can be 4 | * reused in other parts of the studio with: 5 | * { 6 | * name: 'someName', 7 | * title: 'Some title', 8 | * type: 'blockContent' 9 | * } 10 | */ 11 | export default { 12 | title: 'Block Content', 13 | name: 'blockContent', 14 | type: 'array', 15 | of: [ 16 | { 17 | title: 'Block', 18 | type: 'block', 19 | // Styles let you set what your user can mark up blocks with. These 20 | // correspond with HTML tags, but you can set any title or value 21 | // you want and decide how you want to deal with it where you want to 22 | // use your content. 23 | styles: [ 24 | {title: 'Normal', value: 'normal'}, 25 | {title: 'H1', value: 'h1'}, 26 | {title: 'H2', value: 'h2'}, 27 | {title: 'H3', value: 'h3'}, 28 | {title: 'H4', value: 'h4'}, 29 | {title: 'Quote', value: 'blockquote'}, 30 | ], 31 | lists: [{title: 'Bullet', value: 'bullet'}], 32 | // Marks let you mark up inline text in the block editor. 33 | marks: { 34 | // Decorators usually describe a single property – e.g. a typographic 35 | // preference or highlighting by editors. 36 | decorators: [ 37 | {title: 'Strong', value: 'strong'}, 38 | {title: 'Emphasis', value: 'em'}, 39 | ], 40 | // Annotations can be any object structure – e.g. a link or a footnote. 41 | annotations: [ 42 | { 43 | title: 'URL', 44 | name: 'link', 45 | type: 'object', 46 | fields: [ 47 | { 48 | title: 'URL', 49 | name: 'href', 50 | type: 'url', 51 | }, 52 | ], 53 | }, 54 | ], 55 | }, 56 | }, 57 | // You can add additional types here. Note that you can't use 58 | // primitive types such as 'string' and 'number' in the same array 59 | // as a block type. 60 | { 61 | type: 'image', 62 | options: {hotspot: true}, 63 | }, 64 | ], 65 | } 66 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/schemas/castMember.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'castMember', 3 | title: 'Cast Member', 4 | type: 'object', 5 | fields: [ 6 | { 7 | name: 'characterName', 8 | title: 'Character Name', 9 | type: 'string', 10 | }, 11 | { 12 | name: 'person', 13 | title: 'Actor', 14 | type: 'reference', 15 | to: [{type: 'person'}], 16 | }, 17 | { 18 | name: 'externalId', 19 | title: 'External ID', 20 | type: 'number', 21 | }, 22 | { 23 | name: 'externalCreditId', 24 | title: 'External Credit ID', 25 | type: 'string', 26 | }, 27 | ], 28 | preview: { 29 | select: { 30 | subtitle: 'characterName', 31 | title: 'person.name', 32 | media: 'person.image', 33 | }, 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/schemas/crewMember.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'crewMember', 3 | title: 'Crew Member', 4 | type: 'object', 5 | fields: [ 6 | { 7 | name: 'department', 8 | title: 'Department', 9 | type: 'string', 10 | }, 11 | { 12 | name: 'job', 13 | title: 'Job', 14 | type: 'string', 15 | }, 16 | { 17 | name: 'person', 18 | title: 'Person', 19 | type: 'reference', 20 | to: [{type: 'person'}], 21 | }, 22 | { 23 | name: 'externalId', 24 | title: 'External ID', 25 | type: 'number', 26 | }, 27 | { 28 | name: 'externalCreditId', 29 | title: 'External Credit ID', 30 | type: 'string', 31 | }, 32 | ], 33 | preview: { 34 | select: { 35 | name: 'person.name', 36 | job: 'job', 37 | department: 'department', 38 | media: 'person.image', 39 | }, 40 | prepare(selection) { 41 | const {name, job, department, media} = selection 42 | return { 43 | title: name, 44 | subtitle: `${job} [${department}]`, 45 | media, 46 | } 47 | }, 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/schemas/movie.js: -------------------------------------------------------------------------------- 1 | import {MdLocalMovies as icon} from 'react-icons/md' 2 | 3 | export default { 4 | name: 'movie', 5 | title: 'Movie', 6 | type: 'document', 7 | icon, 8 | fields: [ 9 | { 10 | name: 'title', 11 | title: 'Title', 12 | type: 'string', 13 | }, 14 | { 15 | name: 'slug', 16 | title: 'Slug', 17 | type: 'slug', 18 | options: { 19 | source: 'title', 20 | maxLength: 100, 21 | }, 22 | }, 23 | { 24 | name: 'overview', 25 | title: 'Overview', 26 | type: 'blockContent', 27 | }, 28 | { 29 | name: 'releaseDate', 30 | title: 'Release date', 31 | type: 'datetime', 32 | }, 33 | { 34 | name: 'externalId', 35 | title: 'External ID', 36 | type: 'number', 37 | }, 38 | { 39 | name: 'popularity', 40 | title: 'Popularity', 41 | type: 'number', 42 | }, 43 | { 44 | name: 'poster', 45 | title: 'Poster Image', 46 | type: 'image', 47 | options: { 48 | hotspot: true, 49 | }, 50 | }, 51 | { 52 | name: 'castMembers', 53 | title: 'Cast Members', 54 | type: 'array', 55 | of: [{type: 'castMember'}], 56 | }, 57 | { 58 | name: 'crewMembers', 59 | title: 'Crew Members', 60 | type: 'array', 61 | of: [{type: 'crewMember'}], 62 | }, 63 | ], 64 | preview: { 65 | select: { 66 | title: 'title', 67 | date: 'releaseDate', 68 | media: 'poster', 69 | castName0: 'castMembers.0.person.name', 70 | castName1: 'castMembers.1.person.name', 71 | }, 72 | prepare(selection) { 73 | const year = selection.date && selection.date.split('-')[0] 74 | const cast = [selection.castName0, selection.castName1].filter(Boolean).join(', ') 75 | 76 | return { 77 | title: `${selection.title} ${year ? `(${year})` : ''}`, 78 | date: selection.date, 79 | subtitle: cast, 80 | media: selection.media, 81 | } 82 | }, 83 | }, 84 | } 85 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/schemas/person.js: -------------------------------------------------------------------------------- 1 | import {UserIcon} from '@sanity/icons' 2 | 3 | export default { 4 | name: 'person', 5 | title: 'Person', 6 | type: 'document', 7 | icon: UserIcon, 8 | fields: [ 9 | { 10 | name: 'name', 11 | title: 'Name', 12 | type: 'string', 13 | description: 'Please use "Firstname Lastname" format', 14 | }, 15 | { 16 | name: 'slug', 17 | title: 'Slug', 18 | type: 'slug', 19 | options: { 20 | source: 'name', 21 | maxLength: 100, 22 | }, 23 | }, 24 | { 25 | name: 'image', 26 | title: 'Image', 27 | type: 'image', 28 | options: { 29 | hotspot: true, 30 | }, 31 | }, 32 | ], 33 | preview: { 34 | select: {title: 'name', media: 'image'}, 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/schemas/plotSummaries.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Plot summaries', 3 | name: 'plotSummaries', 4 | type: 'object', 5 | fields: [ 6 | { 7 | name: 'caption', 8 | title: 'Caption', 9 | type: 'string', 10 | }, 11 | { 12 | name: 'summaries', 13 | title: 'Summaries', 14 | type: 'array', 15 | of: [{type: 'plotSummary'}], 16 | }, 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/schemas/plotSummary.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'plotSummary', 3 | title: 'Plot Summary', 4 | type: 'object', 5 | fields: [ 6 | { 7 | title: 'Summary', 8 | name: 'summary', 9 | type: 'text', 10 | }, 11 | { 12 | title: 'Author', 13 | name: 'author', 14 | type: 'string', 15 | }, 16 | { 17 | title: 'Link to author', 18 | name: 'url', 19 | type: 'url', 20 | }, 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/schemas/schema.js: -------------------------------------------------------------------------------- 1 | // First, we must import the schema creator 2 | import createSchema from 'part:@sanity/base/schema-creator' 3 | // Then import schema types from any plugins that might expose them 4 | import schemaTypes from 'all:part:@sanity/base/schema-type' 5 | 6 | // We import object and document schemas 7 | import blockContent from './blockContent' 8 | import crewMember from './crewMember' 9 | import castMember from './castMember' 10 | import movie from './movie' 11 | import person from './person' 12 | import screening from './screening' 13 | import plotSummary from './plotSummary' 14 | import plotSummaries from './plotSummaries' 15 | 16 | // Then we give our schema to the builder and provide the result to Sanity 17 | export default createSchema({ 18 | // We name our schema 19 | name: 'default', 20 | // Then proceed to concatenate our document type 21 | // to the ones provided by any plugins that are installed 22 | types: schemaTypes.concat([ 23 | // The following are document types which will appear 24 | // in the studio. 25 | movie, 26 | person, 27 | screening, 28 | // When added to this list, object types can be used as 29 | // { type: 'typename' } in other document schemas 30 | blockContent, 31 | plotSummary, 32 | plotSummaries, 33 | castMember, 34 | crewMember, 35 | ]), 36 | }) 37 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/schemas/screening.js: -------------------------------------------------------------------------------- 1 | import {MdLocalPlay as icon} from 'react-icons/md' 2 | 3 | export default { 4 | name: 'screening', 5 | title: 'Screening', 6 | type: 'document', 7 | icon, 8 | fields: [ 9 | { 10 | name: 'title', 11 | title: 'Title', 12 | type: 'string', 13 | description: 'E.g.: Our first ever screening of Gattaca', 14 | }, 15 | { 16 | name: 'movie', 17 | title: 'Movie', 18 | type: 'reference', 19 | to: [{type: 'movie'}], 20 | description: 'Which movie are we screening', 21 | }, 22 | { 23 | name: 'published', 24 | title: 'Published', 25 | type: 'boolean', 26 | description: 'Set to published when this screening should be visible on a front-end', 27 | }, 28 | { 29 | name: 'location', 30 | title: 'Location', 31 | type: 'geopoint', 32 | description: 'Where will the screening take place?', 33 | }, 34 | { 35 | name: 'beginAt', 36 | title: 'Starts at', 37 | type: 'datetime', 38 | description: 'When does the screening start?', 39 | }, 40 | { 41 | name: 'endAt', 42 | title: 'Ends at', 43 | type: 'datetime', 44 | description: 'When does the screening end?', 45 | }, 46 | { 47 | name: 'allowedGuests', 48 | title: 'Who can come?', 49 | type: 'string', 50 | options: { 51 | list: [ 52 | {title: 'Members', value: 'members'}, 53 | {title: 'Members and friends', value: 'friends'}, 54 | {title: 'Anyone', value: 'anyone'}, 55 | ], 56 | layout: 'radio', 57 | }, 58 | }, 59 | { 60 | name: 'infoUrl', 61 | title: 'More info at', 62 | type: 'url', 63 | description: 64 | 'URL to imdb.com, rottentomatoes.com or some other place with reviews, stats, etc', 65 | }, 66 | { 67 | name: 'ticket', 68 | title: 'Ticket', 69 | type: 'file', 70 | description: 'PDF for printing a physical ticket', 71 | }, 72 | ], 73 | preview: { 74 | select: { 75 | title: 'title', 76 | media: 'movie.poster', 77 | }, 78 | }, 79 | } 80 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/static/.gitkeep: -------------------------------------------------------------------------------- 1 | Files placed here will be served by the Sanity server under the `/static`-prefix 2 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/plugin-kit/519ad2851042cd09e5d0dfe15cfc03c50c74fc5e/test/fixtures/verify-package/fresh-v2-movie-studio/static/favicon.ico -------------------------------------------------------------------------------- /test/fixtures/verify-package/fresh-v2-movie-studio/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Note: This config is only used to help editors like VS Code understand/resolve 3 | // parts, the actual transpilation is done by babel. Any compiler configuration in 4 | // here will be ignored. 5 | "include": ["./node_modules/@sanity/base/types/**/*.ts", "./**/*.ts", "./**/*.tsx"] 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/invalid-eslint/.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 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/invalid-eslint/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": ["sanity", "this-does-not-exist"] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/invalid-eslint/.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 | # Compiled plugin 53 | dist 54 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/invalid-eslint/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/invalid-eslint/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Test Person 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 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/invalid-eslint/README.md: -------------------------------------------------------------------------------- 1 | # sanity-plugin-test-plugin 2 | 3 | ## Installation 4 | 5 | ```sh 6 | npm install sanity-plugin-test-plugin 7 | ``` 8 | 9 | ## Usage 10 | 11 | Add it as a plugin in `sanity.config.ts` (or .js): 12 | 13 | ```ts 14 | import {defineConfig} from 'sanity' 15 | import {myPlugin} from 'sanity-plugin-test-plugin' 16 | 17 | export default defineConfig({ 18 | // ... 19 | plugins: [myPlugin({})], 20 | }) 21 | ``` 22 | 23 | ## License 24 | 25 | MIT © Test Person 26 | See LICENSE 27 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/invalid-eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-plugin-test-plugin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "homepage": "https://github.com/sanity-io/sanity#readme", 6 | "bugs": { 7 | "url": "https://github.com/sanity-io/sanity/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/sanity-io/sanity" 12 | }, 13 | "license": "MIT", 14 | "author": "Test Person ", 15 | "exports": { 16 | ".": { 17 | "require": "./dist/cjs/index.js", 18 | "default": "./dist/esm/index.js" 19 | } 20 | }, 21 | "main": "./dist/cjs/index.js", 22 | "module": "./dist/esm/index.js", 23 | "source": "./src/index.ts", 24 | "types": "./dist/types/index.d.ts", 25 | "files": [ 26 | "dist", 27 | "sanity.json", 28 | "src", 29 | "v2-incompatible.js" 30 | ], 31 | "scripts": { 32 | "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean", 33 | "link-watch": "plugin-kit link-watch", 34 | "lint": "eslint .", 35 | "prepublishOnly": "npm run build", 36 | "watch": "pkg-utils watch --strict" 37 | }, 38 | "dependencies": { 39 | "@sanity/incompatible-plugin": "^0.0.1-studio-v3.1" 40 | }, 41 | "devDependencies": { 42 | "@sanity/plugin-kit": "^0.0.1-studio-v3.1", 43 | "@typescript-eslint/eslint-plugin": "^5.27.1", 44 | "@typescript-eslint/parser": "^5.27.1", 45 | "eslint": "^8.17.0", 46 | "eslint-config-prettier": "^8.5.0", 47 | "eslint-config-sanity": "^6.0.0", 48 | "eslint-plugin-prettier": "^4.0.0", 49 | "eslint-plugin-react": "^7.30.0", 50 | "eslint-plugin-react-hooks": "^4.5.0", 51 | "prettier": "^2.6.2", 52 | "prettier-plugin-packagejson": "^2.3.0", 53 | "react": "^18.2.0", 54 | "sanity": "^3.0.0", 55 | "typescript": "^4.7.3" 56 | }, 57 | "peerDependencies": { 58 | "react": "^18", 59 | "sanity": "^3.0.0" 60 | }, 61 | "engines": { 62 | "node": ">=18" 63 | }, 64 | "sanityPlugin": { 65 | "verifyPackage": { 66 | "eslintImports": true, 67 | "tsc": false, 68 | "packageName": false, 69 | "module": false, 70 | "tsconfig": false, 71 | "dependencies": false, 72 | "rollupConfig": false, 73 | "babelConfig": false, 74 | "sanityV2Json": false, 75 | "scripts": false, 76 | "pkg-utils": false, 77 | "nodeEngine": false, 78 | "studioConfig": false 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/invalid-eslint/sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/sanity-root", 5 | "path": "./v2-incompatible.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/invalid-eslint/src/index.ts: -------------------------------------------------------------------------------- 1 | import {definePlugin} from 'sanity' 2 | 3 | interface MyPluginConfig { 4 | /* nothing here yet */ 5 | } 6 | 7 | /** 8 | * Usage in `sanity.config.ts` (or .js) 9 | * 10 | * ```ts 11 | * import {defineConfig} from 'sanity' 12 | * import {myPlugin} from 'sanity-plugin-test-plugin' 13 | * 14 | * export default defineConfig({ 15 | * // ... 16 | * plugins: [myPlugin({})], 17 | * }) 18 | * ``` 19 | */ 20 | export const myPlugin = definePlugin((config = {}) => { 21 | // eslint-disable-next-line no-console 22 | console.log('hello from sanity-plugin-test-plugin') 23 | return { 24 | name: 'sanity-plugin-test-plugin', 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/invalid-eslint/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "moduleResolution": "bundler", 5 | "target": "esnext", 6 | "module": "preserve", 7 | "esModuleInterop": true, 8 | "lib": ["es2015", "es2016", "es2017", "dom"], 9 | "strict": true, 10 | "sourceMap": false, 11 | "inlineSourceMap": false, 12 | "downlevelIteration": true, 13 | "declaration": true, 14 | "allowSyntheticDefaultImports": true, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true, 17 | "outDir": "dist", 18 | "skipLibCheck": true, 19 | "isolatedModules": true, 20 | "checkJs": false, 21 | "emitDeclarationOnly": true 22 | }, 23 | "include": ["src/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/invalid-eslint/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: undefined, 9 | }, 10 | sanityExchangeUrl, 11 | }) 12 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/valid/.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 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/valid/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": ["sanity", "sanity/typescript", "plugin:prettier/recommended"] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/valid/.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 | # Compiled plugin 53 | dist 54 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/valid/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/valid/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Test Person 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 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/valid/README.md: -------------------------------------------------------------------------------- 1 | # sanity-plugin-test-plugin 2 | 3 | ## Installation 4 | 5 | ```sh 6 | npm install sanity-plugin-test-plugin 7 | ``` 8 | 9 | ## Usage 10 | 11 | Add it as a plugin in `sanity.config.ts` (or .js): 12 | 13 | ```ts 14 | import {defineConfig} from 'sanity' 15 | import {myPlugin} from 'sanity-plugin-test-plugin' 16 | 17 | export default defineConfig({ 18 | // ... 19 | plugins: [myPlugin({})], 20 | }) 21 | ``` 22 | 23 | ## License 24 | 25 | MIT © Test Person 26 | See LICENSE 27 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/valid/package.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from '@sanity/pkg-utils' 2 | 3 | export default defineConfig({ 4 | dist: 'dist', 5 | }) 6 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/valid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-plugin-test-plugin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "homepage": "https://github.com/sanity-io/sanity#readme", 6 | "bugs": { 7 | "url": "https://github.com/sanity-io/sanity/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/sanity-io/sanity" 12 | }, 13 | "license": "MIT", 14 | "author": "Test Person ", 15 | "type": "commonjs", 16 | "exports": { 17 | ".": { 18 | "source": "./src/index.ts", 19 | "import": "./dist/index.mjs", 20 | "default": "./dist/index.js" 21 | } 22 | }, 23 | "main": "./dist/index.js", 24 | "types": "./dist/index.d.ts", 25 | "files": [ 26 | "src", 27 | "dist", 28 | "v2-incompatible.js", 29 | "sanity.json" 30 | ], 31 | "scripts": { 32 | "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean", 33 | "link-watch": "plugin-kit link-watch", 34 | "lint": "eslint .", 35 | "prepublishOnly": "npm run build", 36 | "watch": "pkg-utils watch --strict" 37 | }, 38 | "dependencies": { 39 | "@sanity/incompatible-plugin": "^0.0.1-studio-v3.1" 40 | }, 41 | "devDependencies": { 42 | "@sanity/pkg-utils": "^6.4.1", 43 | "@sanity/plugin-kit": "^0.0.1-studio-v3.1", 44 | "@typescript-eslint/eslint-plugin": "^5.27.1", 45 | "@typescript-eslint/parser": "^5.27.1", 46 | "eslint": "^8.17.0", 47 | "eslint-config-prettier": "^8.5.0", 48 | "eslint-config-sanity": "^6.0.0", 49 | "eslint-plugin-prettier": "^4.0.0", 50 | "eslint-plugin-react": "^7.30.0", 51 | "eslint-plugin-react-hooks": "^4.5.0", 52 | "prettier": "^2.6.2", 53 | "prettier-plugin-packagejson": "^2.3.0", 54 | "react": "^18.2.0", 55 | "sanity": "^3.0.0", 56 | "typescript": "^5.4.5" 57 | }, 58 | "peerDependencies": { 59 | "react": "^18", 60 | "sanity": "^3.0.0" 61 | }, 62 | "engines": { 63 | "node": ">=18" 64 | }, 65 | "sanityPlugin": { 66 | "verifyPackage": { 67 | "tsc": false 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/valid/sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/sanity-root", 5 | "path": "./v2-incompatible.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/valid/src/index.ts: -------------------------------------------------------------------------------- 1 | import {definePlugin} from 'sanity' 2 | 3 | interface MyPluginConfig { 4 | /* nothing here yet */ 5 | } 6 | 7 | /** 8 | * Usage in `sanity.config.ts` (or .js) 9 | * 10 | * ```ts 11 | * import {defineConfig} from 'sanity' 12 | * import {myPlugin} from 'sanity-plugin-test-plugin' 13 | * 14 | * export default defineConfig({ 15 | * // ... 16 | * plugins: [myPlugin({})], 17 | * }) 18 | * ``` 19 | */ 20 | export const myPlugin = definePlugin((config = {}) => { 21 | // eslint-disable-next-line no-console 22 | console.log('hello from sanity-plugin-test-plugin') 23 | return { 24 | name: 'sanity-plugin-test-plugin', 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/valid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "moduleResolution": "bundler", 5 | "target": "esnext", 6 | "module": "preserve", 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "forceConsistentCasingInFileNames": true, 11 | "lib": ["es2015", "es2016", "es2017", "dom"], 12 | "strict": true, 13 | "sourceMap": false, 14 | "inlineSourceMap": false, 15 | "downlevelIteration": true, 16 | "declaration": true, 17 | "allowSyntheticDefaultImports": true, 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | "outDir": "dist", 21 | "skipLibCheck": true, 22 | "isolatedModules": true, 23 | "checkJs": false, 24 | "noEmit": true, 25 | "rootDir": "." 26 | }, 27 | "include": ["src/**/*"] 28 | } 29 | -------------------------------------------------------------------------------- /test/fixtures/verify-package/valid/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: undefined, 9 | }, 10 | sanityExchangeUrl, 11 | }) 12 | -------------------------------------------------------------------------------- /test/init-verify-build.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import execa from 'execa' 3 | import tap from 'tap' 4 | import {contents, initTestArgs, normalize, runCliCommand, testFixture} from './fixture-utils' 5 | import outdent from 'outdent' 6 | 7 | tap.test('plugin-kit init -> verify-package -> tsc > pkg-utils build', async (t) => { 8 | await testFixture({ 9 | fixturePath: 'init/empty', 10 | relativeOutPath: 'buildable', 11 | command: async ({outputDir}) => { 12 | // using console.error: we want these logged continuously to not surprise devs on the runtime 13 | console.error( 14 | 'Integration testing init -> verify-package -> tsc -> build.\nThis may take a while...', 15 | ) 16 | let start = new Date().getTime() 17 | function seconds() { 18 | return `${(new Date().getTime() - start) / 1000}s` 19 | } 20 | const init = await runCliCommand('init', [outputDir, ...initTestArgs]) 21 | console.error( 22 | `"plugin-kit init" done in ${seconds()}.\nRunning "plugin-kit verify-package"...`, 23 | ) 24 | 25 | start = new Date().getTime() 26 | const verify = await runCliCommand('verify-package', [outputDir]) 27 | console.error(`"plugin-kit verify-package" done in ${seconds()}.\nRunning "tsc --build"...`) 28 | 29 | start = new Date().getTime() 30 | const tsc = await execa('tsc', ['--build'], {cwd: outputDir}) 31 | console.error(`"tsc --build" done in ${seconds()}.\nRunning "pkg-utils build"...`) 32 | 33 | start = new Date().getTime() 34 | const build = await execa('pkg-utils', ['build'], {cwd: outputDir}) 35 | console.error(`"pkg-utils build" done in ${seconds()}.`) 36 | 37 | return { 38 | stdout: outdent` 39 | ${init.stdout} 40 | ${verify.stdout} 41 | ${tsc.stdout} 42 | ${build.stdout} 43 | `, 44 | stderr: outdent` 45 | ${init.stderr} 46 | ${verify.stderr} 47 | ${tsc.stderr} 48 | ${build.stderr} 49 | `, 50 | } as any 51 | }, 52 | 53 | assert: async ({result: {stdout, stderr}, outputDir}) => { 54 | const trimmedErrors = stderr 55 | .split('\n') 56 | .filter((line) => !line.trim()) // remove empty lines 57 | .join('\n') 58 | .trim() 59 | t.equal(trimmedErrors, '', 'should have empty stderr') 60 | 61 | t.strictSame( 62 | await contents(path.join(outputDir, 'dist')), 63 | ['index.d.mts', 'index.d.ts', 'index.js', 'index.js.map', 'index.mjs', 'index.mjs.map'].map( 64 | normalize, 65 | ), 66 | 'should output expected files to dist', 67 | ) 68 | }, 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /test/init.test.ts: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | import { 3 | fileContainsValidator, 4 | normalize, 5 | readFile, 6 | runCliCommand, 7 | testFixture, 8 | pluginTestName, 9 | initTestArgs, 10 | } from './fixture-utils' 11 | import path from 'path' 12 | import {fileExists} from '../src/util/files' 13 | import {incompatiblePluginPackage} from '../src/constants' 14 | import {PackageJson} from '../src/actions/verify/types' 15 | 16 | const defaultDevDependencies = [ 17 | '@sanity/pkg-utils', 18 | '@sanity/plugin-kit', 19 | '@types/react', 20 | '@typescript-eslint/eslint-plugin', 21 | '@typescript-eslint/parser', 22 | 'eslint', 23 | 'eslint-config-prettier', 24 | 'eslint-config-sanity', 25 | 'eslint-plugin-prettier', 26 | 'eslint-plugin-react', 27 | 'eslint-plugin-react-hooks', 28 | 'prettier', 29 | 'prettier-plugin-packagejson', 30 | 'react', 31 | 'react-dom', 32 | 'sanity', 33 | 'styled-components', 34 | 'typescript', 35 | ] 36 | 37 | tap.test('plugin-kit init --force in empty directory', async (t) => { 38 | await testFixture({ 39 | fixturePath: 'init/empty', 40 | relativeOutPath: 'defaults', 41 | command: ({outputDir}) => runCliCommand('init', [outputDir, ...initTestArgs]), 42 | assert: async ({result: {stdout, stderr}, outputDir}) => { 43 | t.equal(stderr, '', 'should have empty stderr') 44 | t.match(stdout, `Initializing new plugin in "${outputDir}"`) 45 | 46 | const fileContains = fileContainsValidator(t, outputDir) 47 | 48 | await fileContains('LICENSE', 'MIT') 49 | await fileContains('README.md', `# ${pluginTestName}`) 50 | await fileContains('.gitignore', 'dist') 51 | await fileContains( 52 | '.eslintrc', 53 | 'sanity', 54 | 'sanity/typescript', 55 | 'sanity/react', 56 | 'plugin:react-hooks/recommended', 57 | 'plugin:prettier/recommended', 58 | ) 59 | await fileContains( 60 | '.eslintignore', 61 | '.eslintrc.js', 62 | 'commitlint.config.js', 63 | 'dist', 64 | 'lint-staged.config.js', 65 | '*.js', 66 | ) 67 | await fileContains('.prettierrc', '"semi": false') 68 | await fileContains('sanity.json', '"path": "./v2-incompatible.js"') 69 | await fileContains('v2-incompatible.js', 'showIncompatiblePluginDialog') 70 | await fileContains('tsconfig.json', '"extends": "./tsconfig.settings"') 71 | await fileContains('tsconfig.dist.json', '"extends": "./tsconfig.settings"') 72 | await fileContains('tsconfig.settings.json', '"target": "esnext"') 73 | 74 | await fileContains('src/index.ts', `name: '${pluginTestName}'`) 75 | 76 | const pkg: PackageJson = JSON.parse(await readFile(path.join(outputDir, 'package.json'))) 77 | 78 | t.has( 79 | pkg, 80 | { 81 | name: pluginTestName, 82 | version: '1.0.0', 83 | description: '', 84 | // author: 'Omitted from validation', 85 | license: 'MIT', 86 | exports: { 87 | '.': { 88 | source: './src/index.ts', 89 | import: './dist/index.mjs', 90 | default: './dist/index.js', 91 | }, 92 | }, 93 | main: './dist/index.js', 94 | types: './dist/index.d.ts', 95 | files: ['dist', 'sanity.json', 'src', 'v2-incompatible.js'], 96 | scripts: { 97 | lint: 'eslint .', 98 | build: 'plugin-kit verify-package --silent && pkg-utils build --strict --check --clean', 99 | watch: 'pkg-utils watch --strict', 100 | 'link-watch': 'plugin-kit link-watch', 101 | prepublishOnly: 'npm run build', 102 | }, 103 | repository: { 104 | type: 'git', 105 | url: 'https://github.com/sanity-io/sanity', 106 | }, 107 | engines: { 108 | node: '>=18', 109 | }, 110 | bugs: { 111 | url: 'https://github.com/sanity-io/sanity/issues', 112 | }, 113 | homepage: 'https://github.com/sanity-io/sanity#readme', 114 | }, 115 | 'package.json has expected content', 116 | ) 117 | 118 | t.strictSame( 119 | Object.keys(pkg.dependencies ?? {}), 120 | [incompatiblePluginPackage], 121 | 'should have empty dependencies', 122 | ) 123 | t.strictSame( 124 | Object.keys(pkg.peerDependencies ?? {}), 125 | ['react', 'sanity'], 126 | 'should have expected peerDependencies', 127 | ) 128 | 129 | t.strictSame( 130 | Object.keys(pkg.devDependencies ?? {}), 131 | defaultDevDependencies, 132 | 'should have expected devDependencies', 133 | ) 134 | }, 135 | }) 136 | }) 137 | 138 | tap.test('plugin-kit init --force with all the opt-outs in empty directory', async (t) => { 139 | await testFixture({ 140 | fixturePath: 'init/empty', 141 | relativeOutPath: 'opt-out', 142 | command: ({outputDir}) => 143 | runCliCommand('init', [ 144 | outputDir, 145 | ...initTestArgs.filter((a) => a !== '--license' && a !== 'mit'), 146 | '--no-install', 147 | '--no-eslint', 148 | '--no-prettier', 149 | '--no-typescript', 150 | '--no-license', 151 | '--no-editorconfig', 152 | '--no-gitignore', 153 | '--no-scripts', 154 | ]), 155 | assert: async ({result: {stdout, stderr}, outputDir}) => { 156 | t.equal(stderr, '', 'should have empty stderr') 157 | t.match(stdout, `Initializing new plugin in "${outputDir}"`) 158 | 159 | const fileContains = fileContainsValidator(t, outputDir) 160 | 161 | const expectNotExist = async (file: string) => 162 | t.notOk(await fileExists(path.join(outputDir, normalize(file))), `${file} should not exist`) 163 | 164 | await expectNotExist('LICENSE') 165 | await expectNotExist('.eslintrc') 166 | await expectNotExist('.gitignore') 167 | await expectNotExist('.prettierrc') 168 | await expectNotExist('tsconfig.json') 169 | 170 | await fileContains('src/index.js', `name: '${pluginTestName}'`) 171 | 172 | const pkg: PackageJson = JSON.parse(await readFile(path.join(outputDir, 'package.json'))) 173 | t.same(pkg.scripts, {}, 'scripts should be an empty object') 174 | 175 | t.strictSame( 176 | Object.keys(pkg.dependencies ?? {}), 177 | [incompatiblePluginPackage], 178 | 'should have empty dependencies', 179 | ) 180 | t.strictSame( 181 | Object.keys(pkg.peerDependencies ?? {}), 182 | ['react', 'sanity'], 183 | 'should have expected peerDependencies', 184 | ) 185 | t.strictSame( 186 | Object.keys(pkg.devDependencies ?? {}), 187 | [ 188 | '@sanity/pkg-utils', 189 | '@sanity/plugin-kit', 190 | 'react', 191 | 'react-dom', 192 | 'sanity', 193 | 'styled-components', 194 | ], 195 | 'should have expected devDependencies', 196 | ) 197 | }, 198 | }) 199 | }) 200 | 201 | tap.test('plugin-kit init --force --preset semver-workflow in empty directory', async (t) => { 202 | await testFixture({ 203 | fixturePath: 'init/empty', 204 | relativeOutPath: 'defaults-semver-workflow', 205 | command: ({outputDir}) => 206 | runCliCommand('init', [outputDir, ...initTestArgs, '--preset', 'semver-workflow']), 207 | assert: async ({result: {stdout, stderr}, outputDir}) => { 208 | t.equal(stderr, '', 'should have empty stderr') 209 | 210 | const fileContains = fileContainsValidator(t, outputDir) 211 | 212 | await fileContains(path.join('.github', 'workflows', 'main.yml'), 'CI & Release') 213 | await fileContains(path.join('.husky', 'commit-msg'), 'npx --no -- commitlint') 214 | await fileContains(path.join('.husky', 'pre-commit'), 'npx lint-staged') 215 | await fileContains(path.join('.releaserc.json'), '@sanity/semantic-release-preset') 216 | await fileContains(path.join('commitlint.config.js'), '@commitlint/config-conventional') 217 | 218 | const pkg: PackageJson = JSON.parse(await readFile(path.join(outputDir, 'package.json'))) 219 | 220 | t.strictSame( 221 | Object.keys(pkg.devDependencies ?? {}), 222 | [ 223 | ...defaultDevDependencies, 224 | '@commitlint/cli', 225 | '@commitlint/config-conventional', 226 | '@sanity/semantic-release-preset', 227 | 'husky', 228 | 'lint-staged', 229 | ].sort(), 230 | 'should have expected devDependencies', 231 | ) 232 | 233 | t.strictSame(pkg.scripts?.prepare, 'husky') 234 | }, 235 | }) 236 | }) 237 | -------------------------------------------------------------------------------- /test/inject.test.ts: -------------------------------------------------------------------------------- 1 | import tap from 'tap' 2 | import {fileContainsValidator, runCliCommand, testFixture} from './fixture-utils' 3 | import path from 'path' 4 | import {copySync} from 'fs-extra' 5 | 6 | tap.test('plugin-kit inject --preset semver-workflow into existing plugin directory', async (t) => { 7 | await testFixture({ 8 | fixturePath: 'inject/valid', 9 | relativeOutPath: '../semver-workflow', 10 | command: async ({fixtureDir, outputDir}) => { 11 | copySync(fixtureDir, outputDir) 12 | return runCliCommand('inject', [outputDir, '--preset-only', '--preset', 'semver-workflow']) 13 | }, 14 | assert: async ({result: {stdout, stderr}, outputDir}) => { 15 | t.equal(stderr, '', 'should have empty stderr') 16 | t.match(stdout, `Only apply presets, skipping default inject.`) 17 | t.match(stdout, `Inject config into plugin in "${outputDir}"`) 18 | 19 | const fileContains = fileContainsValidator(t, outputDir) 20 | 21 | // only check for a single file from the preset: 22 | // rest is covered by the init tests; it uses the same codepath 23 | await fileContains(path.join('.github', 'workflows', 'main.yml'), 'CI & Release') 24 | }, 25 | }) 26 | }) 27 | 28 | tap.test('plugin-kit inject --preset renovatebot into existing plugin directory', async (t) => { 29 | await testFixture({ 30 | fixturePath: 'inject/valid', 31 | relativeOutPath: '../renovatebot', 32 | command: async ({fixtureDir, outputDir}) => { 33 | copySync(fixtureDir, outputDir) 34 | return runCliCommand('inject', [outputDir, '--preset-only', '--preset', 'renovatebot']) 35 | }, 36 | assert: async ({result: {stdout, stderr}, outputDir}) => { 37 | t.equal(stderr, '', 'should have empty stderr') 38 | 39 | const fileContains = fileContainsValidator(t, outputDir) 40 | 41 | await fileContains( 42 | path.join('renovate.json'), 43 | '"github>sanity-io/renovate-presets//ecosystem/auto"', 44 | ) 45 | }, 46 | }) 47 | }) 48 | 49 | tap.test('plugin-kit inject --preset-only requires --preset', async (t) => { 50 | await testFixture({ 51 | fixturePath: 'inject/valid', 52 | command: async ({fixtureDir}) => { 53 | return runCliCommand('inject', [fixtureDir, '--preset-only']) 54 | }, 55 | assert: async ({result: {stdout, stderr}, outputDir}) => { 56 | t.match(stderr, '--preset-only, but no --preset [preset-name] was provided.') 57 | }, 58 | }) 59 | }) 60 | 61 | tap.test('plugin-kit inject --preset-only --preset does-not-exist', async (t) => { 62 | await testFixture({ 63 | fixturePath: 'inject/valid', 64 | command: async ({fixtureDir}) => { 65 | return runCliCommand('inject', [fixtureDir, '--preset-only', '--preset', 'does-not-exist']) 66 | }, 67 | assert: async ({result: {stdout, stderr}, outputDir}) => { 68 | t.match(stderr, 'Unknown --preset(s): [does-not-exist]. Must be one of: [') 69 | }, 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/run-test-command.ts: -------------------------------------------------------------------------------- 1 | import {cliEntry} from '../src/cli' 2 | 3 | // ts-node