├── .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 | 
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