@sanity/cli 3.65.2-upgrade-vite-v5.31 → 3.66.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,522 @@
1
+ import {access, readFile, writeFile} from 'node:fs/promises'
2
+ import {join, posix, sep} from 'node:path'
3
+ import {Readable} from 'node:stream'
4
+ import {pipeline} from 'node:stream/promises'
5
+ import {type ReadableStream} from 'node:stream/web'
6
+
7
+ import {x} from 'tar'
8
+ import {parse as parseYaml} from 'yaml'
9
+
10
+ import {type CliApiClient, type PackageJson} from '../types'
11
+
12
+ const ENV_VAR = {
13
+ PROJECT_ID: /SANITY(?:_STUDIO)?_PROJECT_ID/, // Matches SANITY_PROJECT_ID and SANITY_STUDIO_PROJECT_ID
14
+ DATASET: /SANITY(?:_STUDIO)?_DATASET/, // Matches SANITY_DATASET and SANITY_STUDIO_DATASET
15
+ READ_TOKEN: 'SANITY_API_READ_TOKEN',
16
+ } as const
17
+
18
+ const ENV_FILE = {
19
+ TEMPLATE: '.env.template',
20
+ EXAMPLE: '.env.example',
21
+ LOCAL_EXAMPLE: '.env.local.example',
22
+ } as const
23
+
24
+ const ENV_TEMPLATE_FILES = [ENV_FILE.TEMPLATE, ENV_FILE.EXAMPLE, ENV_FILE.LOCAL_EXAMPLE] as const
25
+
26
+ type EnvData = {
27
+ projectId: string
28
+ dataset: string
29
+ readToken?: string
30
+ }
31
+
32
+ type GithubUrlString =
33
+ | `https://github.com/${string}/${string}`
34
+ | `https://www.github.com/${string}/${string}`
35
+
36
+ export type RepoInfo = {
37
+ username: string
38
+ name: string
39
+ branch: string
40
+ filePath: string
41
+ }
42
+
43
+ function isGithubRepoShorthand(value: string): boolean {
44
+ if (URL.canParse(value)) {
45
+ return false
46
+ }
47
+ // This supports :owner/:repo and :owner/:repo/nested/path, e.g.
48
+ // sanity-io/sanity
49
+ // sanity-io/sanity/templates/next-js
50
+ // sanity-io/templates/live-content-api
51
+ // sanity-io/sanity/packages/@sanity/cli/test/test-template
52
+ return /^[\w-]+\/[\w-.]+(\/[@\w-.]+)*$/.test(value)
53
+ }
54
+
55
+ function isGithubRepoUrl(value: string | URL): value is URL | GithubUrlString {
56
+ if (URL.canParse(value) === false) {
57
+ return false
58
+ }
59
+ const url = new URL(value)
60
+ const pathSegments = url.pathname.slice(1).split('/')
61
+
62
+ return (
63
+ url.protocol === 'https:' &&
64
+ url.hostname === 'github.com' &&
65
+ // The pathname must have at least 2 segments. If it has more than 2, the
66
+ // third must be "tree" and it must have at least 4 segments.
67
+ // https://github.com/:owner/:repo
68
+ // https://github.com/:owner/:repo/tree/:ref
69
+ pathSegments.length >= 2 &&
70
+ (pathSegments.length > 2 ? pathSegments[2] === 'tree' && pathSegments.length >= 4 : true)
71
+ )
72
+ }
73
+
74
+ async function downloadTarStream(url: string, bearerToken?: string): Promise<Readable> {
75
+ const headers: Record<string, string> = {}
76
+ if (bearerToken) {
77
+ headers.Authorization = `Bearer ${bearerToken}`
78
+ }
79
+
80
+ const res = await fetch(url, {headers})
81
+
82
+ if (!res.body) {
83
+ throw new Error(`Failed to download: ${url}`)
84
+ }
85
+
86
+ return Readable.fromWeb(res.body as ReadableStream)
87
+ }
88
+
89
+ export function checkIsRemoteTemplate(templateName?: string): boolean {
90
+ return templateName?.includes('/') ?? false
91
+ }
92
+
93
+ export async function getGitHubRepoInfo(value: string, bearerToken?: string): Promise<RepoInfo> {
94
+ let username = ''
95
+ let name = ''
96
+ let branch = ''
97
+ let filePath = ''
98
+
99
+ if (isGithubRepoShorthand(value)) {
100
+ const parts = value.split('/')
101
+ username = parts[0]
102
+ name = parts[1]
103
+ // If there are more segments after owner/repo, they form the file path
104
+ if (parts.length > 2) {
105
+ filePath = parts.slice(2).join('/')
106
+ }
107
+ }
108
+
109
+ if (isGithubRepoUrl(value)) {
110
+ const url = new URL(value)
111
+ const pathSegments = url.pathname.slice(1).split('/')
112
+ username = pathSegments[0]
113
+ name = pathSegments[1]
114
+
115
+ // If we have a "tree" segment, everything after branch is the file path
116
+ if (pathSegments[2] === 'tree') {
117
+ branch = pathSegments[3]
118
+ if (pathSegments.length > 4) {
119
+ filePath = pathSegments.slice(4).join('/')
120
+ }
121
+ }
122
+ }
123
+
124
+ if (!username || !name) {
125
+ throw new Error('Invalid GitHub repository format')
126
+ }
127
+
128
+ const tokenMessage =
129
+ 'GitHub repository not found. For private repositories, use --template-token to provide an access token.\n\n' +
130
+ 'You can generate a new token at https://github.com/settings/personal-access-tokens/new\n' +
131
+ 'Set the token to "read-only" with repository access and a short expiry (e.g. 7 days) for security.'
132
+
133
+ try {
134
+ const headers: Record<string, string> = {}
135
+ if (bearerToken) {
136
+ headers.Authorization = `Bearer ${bearerToken}`
137
+ }
138
+
139
+ const infoResponse = await fetch(`https://api.github.com/repos/${username}/${name}`, {
140
+ headers,
141
+ })
142
+
143
+ if (infoResponse.status !== 200) {
144
+ if (infoResponse.status === 404) {
145
+ throw new Error(tokenMessage)
146
+ }
147
+ throw new Error('GitHub repository not found')
148
+ }
149
+
150
+ const info = await infoResponse.json()
151
+
152
+ return {
153
+ username,
154
+ name,
155
+ branch: branch || info.default_branch,
156
+ filePath,
157
+ }
158
+ } catch {
159
+ throw new Error(tokenMessage)
160
+ }
161
+ }
162
+
163
+ export async function downloadAndExtractRepo(
164
+ root: string,
165
+ {username, name, branch, filePath}: RepoInfo,
166
+ bearerToken?: string,
167
+ ): Promise<void> {
168
+ let rootPath: string | null = null
169
+ await pipeline(
170
+ await downloadTarStream(
171
+ `https://codeload.github.com/${username}/${name}/tar.gz/${branch}`,
172
+ bearerToken,
173
+ ),
174
+ x({
175
+ cwd: root,
176
+ strip: filePath ? filePath.split('/').length + 1 : 1,
177
+ filter: (p: string) => {
178
+ const posixPath = p.split(sep).join(posix.sep)
179
+ if (rootPath === null) {
180
+ const pathSegments = posixPath.split(posix.sep)
181
+ rootPath = pathSegments.length ? pathSegments[0] : null
182
+ }
183
+ return posixPath.startsWith(`${rootPath}${filePath ? `/${filePath}/` : '/'}`)
184
+ },
185
+ }),
186
+ )
187
+ }
188
+
189
+ /**
190
+ * Checks if a GitHub repository is a monorepo by examining common monorepo configuration files.
191
+ * Supports pnpm workspaces, Lerna, Rush, and npm workspaces (package.json).
192
+ * @returns Promise that resolves to an array of package paths/names if monorepo is detected, undefined otherwise
193
+ */
194
+ export async function getMonoRepo(
195
+ repoInfo: RepoInfo,
196
+ bearerToken?: string,
197
+ ): Promise<string[] | undefined> {
198
+ const {username, name, branch, filePath} = repoInfo
199
+ const baseUrl = `https://raw.githubusercontent.com/${username}/${name}/${branch}/${filePath}`
200
+
201
+ const headers: Record<string, string> = {}
202
+ if (bearerToken) {
203
+ headers.Authorization = `Bearer ${bearerToken}`
204
+ }
205
+
206
+ type MonorepoHandler = {
207
+ check: (content: string) => string[] | undefined
208
+ }
209
+
210
+ const handlers: Record<string, MonorepoHandler> = {
211
+ 'package.json': {
212
+ check: (content) => {
213
+ try {
214
+ const pkg = JSON.parse(content)
215
+ if (!pkg.workspaces) return undefined
216
+ return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages
217
+ } catch {
218
+ return undefined
219
+ }
220
+ },
221
+ },
222
+ 'pnpm-workspace.yaml': {
223
+ check: (content) => {
224
+ try {
225
+ const config = parseYaml(content)
226
+ return config.packages
227
+ } catch {
228
+ return undefined
229
+ }
230
+ },
231
+ },
232
+ 'lerna.json': {
233
+ check: (content) => {
234
+ try {
235
+ const config = JSON.parse(content)
236
+ return config.packages
237
+ } catch {
238
+ return undefined
239
+ }
240
+ },
241
+ },
242
+ 'rush.json': {
243
+ check: (content) => {
244
+ try {
245
+ const config = JSON.parse(content)
246
+ return config.projects?.map((p: {packageName: string}) => p.packageName)
247
+ } catch {
248
+ return undefined
249
+ }
250
+ },
251
+ },
252
+ }
253
+
254
+ const fileChecks = await Promise.all(
255
+ Object.keys(handlers).map(async (file) => {
256
+ const response = await fetch(`${baseUrl}/${file}`, {headers})
257
+ return {file, exists: response.status === 200, content: await response.text()}
258
+ }),
259
+ )
260
+
261
+ for (const check of fileChecks) {
262
+ if (!check.exists) continue
263
+ const result = handlers[check.file].check(check.content)
264
+ if (result) return result
265
+ }
266
+
267
+ return undefined
268
+ }
269
+
270
+ /**
271
+ * Validates a single package within a repository against required criteria.
272
+ */
273
+ async function validatePackage(
274
+ baseUrl: string,
275
+ packagePath: string,
276
+ headers: Record<string, string>,
277
+ ): Promise<{
278
+ hasSanityConfig: boolean
279
+ hasSanityCli: boolean
280
+ hasEnvFile: boolean
281
+ hasSanityDep: boolean
282
+ }> {
283
+ const packageUrl = packagePath ? `${baseUrl}/${packagePath}` : baseUrl
284
+
285
+ const requiredFiles = [
286
+ 'package.json',
287
+ 'sanity.config.ts',
288
+ 'sanity.config.js',
289
+ 'sanity.cli.ts',
290
+ 'sanity.cli.js',
291
+ ...ENV_TEMPLATE_FILES,
292
+ ]
293
+
294
+ const fileChecks = await Promise.all(
295
+ requiredFiles.map(async (file) => {
296
+ const response = await fetch(`${packageUrl}/${file}`, {headers})
297
+ return {file, exists: response.status === 200, content: await response.text()}
298
+ }),
299
+ )
300
+
301
+ const packageJson = fileChecks.find((f) => f.file === 'package.json')
302
+ if (!packageJson?.exists) {
303
+ throw new Error(`Package at ${packagePath || 'root'} must include a package.json file`)
304
+ }
305
+
306
+ let hasSanityDep = false
307
+ try {
308
+ const pkg: PackageJson = JSON.parse(packageJson.content)
309
+ hasSanityDep = !!(pkg.dependencies?.sanity || pkg.devDependencies?.sanity)
310
+ } catch (err) {
311
+ throw new Error(`Invalid package.json file in ${packagePath || 'root'}`)
312
+ }
313
+
314
+ const hasSanityConfig = fileChecks.some(
315
+ (f) => f.exists && (f.file === 'sanity.config.ts' || f.file === 'sanity.config.js'),
316
+ )
317
+
318
+ const hasSanityCli = fileChecks.some(
319
+ (f) => f.exists && (f.file === 'sanity.cli.ts' || f.file === 'sanity.cli.js'),
320
+ )
321
+
322
+ const envFile = fileChecks.find(
323
+ (f) => f.exists && ENV_TEMPLATE_FILES.includes(f.file as (typeof ENV_TEMPLATE_FILES)[number]),
324
+ )
325
+ if (envFile) {
326
+ const envContent = envFile.content
327
+ const hasProjectId = envContent.match(ENV_VAR.PROJECT_ID)
328
+ const hasDataset = envContent.match(ENV_VAR.DATASET)
329
+
330
+ if (!hasProjectId || !hasDataset) {
331
+ const missing = []
332
+ if (!hasProjectId) missing.push('SANITY_PROJECT_ID or SANITY_STUDIO_PROJECT_ID')
333
+ if (!hasDataset) missing.push('SANITY_DATASET or SANITY_STUDIO_DATASET')
334
+ throw new Error(
335
+ `Environment template in ${
336
+ packagePath || 'repo'
337
+ } must include the following variables: ${missing.join(', ')}`,
338
+ )
339
+ }
340
+ }
341
+
342
+ return {
343
+ hasSanityConfig,
344
+ hasSanityCli,
345
+ hasEnvFile: Boolean(envFile),
346
+ hasSanityDep,
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Validates a GitHub repository template against required criteria.
352
+ * Supports both monorepo and single-package repositories.
353
+ *
354
+ * For monorepos:
355
+ * - Each package must have a valid package.json
356
+ * - At least one package must include 'sanity' in dependencies or devDependencies
357
+ * - At least one package must have sanity.config.js/ts and sanity.cli.js/ts
358
+ * - Each package must have a .env.template, .env.example, or .env.local.example
359
+ *
360
+ * For single-package repositories:
361
+ * - Must have a valid package.json with 'sanity' dependency
362
+ * - Must have sanity.config.js/ts and sanity.cli.js/ts
363
+ * - Must have .env.template, .env.example, or .env.local.example
364
+ *
365
+ * Environment files must include:
366
+ * - SANITY_PROJECT_ID or SANITY_STUDIO_PROJECT_ID variable
367
+ * - SANITY_DATASET or SANITY_STUDIO_DATASET variable
368
+ *
369
+ * @throws Error if validation fails with specific reason
370
+ */
371
+ export async function validateRemoteTemplate(
372
+ repoInfo: RepoInfo,
373
+ packages: string[] = [''],
374
+ bearerToken?: string,
375
+ ): Promise<void> {
376
+ const {username, name, branch, filePath} = repoInfo
377
+ const baseUrl = `https://raw.githubusercontent.com/${username}/${name}/${branch}/${filePath}`
378
+
379
+ const headers: Record<string, string> = {}
380
+ if (bearerToken) {
381
+ headers.Authorization = `Bearer ${bearerToken}`
382
+ }
383
+
384
+ const validations = await Promise.all(
385
+ packages.map((pkg) => validatePackage(baseUrl, pkg, headers)),
386
+ )
387
+
388
+ const hasSanityDep = validations.some((v) => v.hasSanityDep)
389
+ if (!hasSanityDep) {
390
+ throw new Error('At least one package must include "sanity" as a dependency in package.json')
391
+ }
392
+
393
+ const hasSanityConfig = validations.some((v) => v.hasSanityConfig)
394
+ if (!hasSanityConfig) {
395
+ throw new Error('At least one package must include a sanity.config.js or sanity.config.ts file')
396
+ }
397
+
398
+ const hasSanityCli = validations.some((v) => v.hasSanityCli)
399
+ if (!hasSanityCli) {
400
+ throw new Error('At least one package must include a sanity.cli.js or sanity.cli.ts file')
401
+ }
402
+
403
+ const missingEnvPackages = packages.filter((pkg, i) => !validations[i].hasEnvFile)
404
+ if (missingEnvPackages.length > 0) {
405
+ throw new Error(
406
+ `The following packages are missing .env.template, .env.example, or .env.local.example files: ${missingEnvPackages.join(
407
+ ', ',
408
+ )}`,
409
+ )
410
+ }
411
+ }
412
+
413
+ export async function isNextJsTemplate(root: string): Promise<boolean> {
414
+ try {
415
+ const packageJson = await readFile(join(root, 'package.json'), 'utf8')
416
+ const pkg = JSON.parse(packageJson)
417
+ return !!(pkg.dependencies?.next || pkg.devDependencies?.next)
418
+ } catch {
419
+ return false
420
+ }
421
+ }
422
+
423
+ export async function checkNeedsReadToken(root: string): Promise<boolean> {
424
+ try {
425
+ const templatePath = await Promise.any(
426
+ ENV_TEMPLATE_FILES.map(async (file) => {
427
+ await access(join(root, file))
428
+ return file
429
+ }),
430
+ )
431
+
432
+ const templateContent = await readFile(join(root, templatePath), 'utf8')
433
+ return templateContent.includes(ENV_VAR.READ_TOKEN)
434
+ } catch {
435
+ return false
436
+ }
437
+ }
438
+
439
+ export async function applyEnvVariables(
440
+ root: string,
441
+ envData: EnvData,
442
+ targetName = '.env',
443
+ ): Promise<void> {
444
+ const templatePath = await Promise.any(
445
+ ENV_TEMPLATE_FILES.map(async (file) => {
446
+ await access(join(root, file))
447
+ return file
448
+ }),
449
+ ).catch(() => {
450
+ throw new Error('Could not find .env.template, .env.example or .env.local.example file')
451
+ })
452
+
453
+ try {
454
+ const templateContent = await readFile(join(root, templatePath), 'utf8')
455
+ const {projectId, dataset, readToken = ''} = envData
456
+
457
+ const findAndReplaceVariable = (
458
+ content: string,
459
+ varRegex: RegExp | string,
460
+ value: string,
461
+ useQuotes: boolean,
462
+ ) => {
463
+ const pattern = varRegex instanceof RegExp ? varRegex : new RegExp(`${varRegex}=.*$`, 'm')
464
+ const match = content.match(pattern)
465
+ if (!match) return content
466
+
467
+ const varName = match[0].split('=')[0]
468
+ return content.replace(
469
+ new RegExp(`${varName}=.*$`, 'm'),
470
+ `${varName}=${useQuotes ? `"${value}"` : value}`,
471
+ )
472
+ }
473
+
474
+ let envContent = templateContent
475
+ const vars = [
476
+ {pattern: ENV_VAR.PROJECT_ID, value: projectId},
477
+ {pattern: ENV_VAR.DATASET, value: dataset},
478
+ {pattern: ENV_VAR.READ_TOKEN, value: readToken},
479
+ ]
480
+ const useQuotes = templateContent.includes('="')
481
+
482
+ for (const {pattern, value} of vars) {
483
+ envContent = findAndReplaceVariable(envContent, pattern, value, useQuotes)
484
+ }
485
+
486
+ await writeFile(join(root, targetName), envContent)
487
+ } catch (err) {
488
+ throw new Error(
489
+ 'Failed to set environment variables. This could be due to file permissions or the .env file format. See https://www.sanity.io/docs/environment-variables for details on environment variable setup.',
490
+ )
491
+ }
492
+ }
493
+
494
+ export async function tryApplyPackageName(root: string, name: string): Promise<void> {
495
+ try {
496
+ const packageJson = await readFile(join(root, 'package.json'), 'utf8')
497
+ const pkg: PackageJson = JSON.parse(packageJson)
498
+ pkg.name = name
499
+
500
+ await writeFile(join(root, 'package.json'), JSON.stringify(pkg, null, 2))
501
+ } catch (err) {
502
+ // noop
503
+ }
504
+ }
505
+
506
+ export async function generateSanityApiReadToken(
507
+ label: string,
508
+ projectId: string,
509
+ apiClient: CliApiClient,
510
+ ): Promise<string> {
511
+ const response = await apiClient({requireProject: false, requireUser: true})
512
+ .config({apiVersion: 'v2021-06-07'})
513
+ .request<{key: string}>({
514
+ uri: `/projects/${projectId}/tokens`,
515
+ method: 'POST',
516
+ body: {
517
+ label: `${label} (${Date.now()})`, // Add timestamp to ensure uniqueness
518
+ roleName: 'viewer',
519
+ },
520
+ })
521
+ return response.key
522
+ }
@@ -1,119 +0,0 @@
1
- import fs from 'node:fs/promises'
2
- import path from 'node:path'
3
-
4
- import {type InitFlags} from '../../commands/init/initCommand'
5
- import {debug} from '../../debug'
6
- import {type CliCommandArguments, type CliCommandContext, type SanityJson} from '../../types'
7
- import {bootstrapFromTemplate} from './bootstrapFromTemplate'
8
- import {pluginTemplates} from './pluginTemplates'
9
-
10
- export default async function initPlugin(
11
- args: CliCommandArguments<InitFlags>,
12
- context: CliCommandContext,
13
- ): Promise<void> {
14
- const {output, prompt} = context
15
- const [, specifiedTemplateUrl] = args.argsWithoutOptions
16
-
17
- output.print('This utility will walk you through creating a new Sanity plugin.')
18
- output.print('Press ^C at any time to quit.\n')
19
-
20
- const hasTemplateUrl = /^https?:\/\//.test(specifiedTemplateUrl || '')
21
-
22
- if (hasTemplateUrl) {
23
- debug('User provided template URL: %s', specifiedTemplateUrl)
24
- return bootstrapFromUrl(context, specifiedTemplateUrl)
25
- }
26
-
27
- let specifiedTemplate = null
28
- if (specifiedTemplateUrl) {
29
- specifiedTemplate = pluginTemplates.find((tpl) => tpl.value === specifiedTemplateUrl)
30
- }
31
-
32
- if (specifiedTemplate) {
33
- debug(
34
- 'User wanted template "%s", match found at %s',
35
- specifiedTemplateUrl,
36
- specifiedTemplate.url,
37
- )
38
-
39
- return bootstrapFromUrl(context, specifiedTemplate.url)
40
- } else if (specifiedTemplateUrl) {
41
- throw new Error(`Cannot find template with name "${specifiedTemplateUrl}"`)
42
- }
43
-
44
- const templateChoices = pluginTemplates.map(({value, name}) => ({value, name}))
45
- const selected = await prompt.single({
46
- message: 'Select template to use',
47
- type: 'list',
48
- choices: templateChoices,
49
- })
50
-
51
- specifiedTemplate = pluginTemplates.find((tpl) => tpl.value === selected)
52
- if (!specifiedTemplate) {
53
- throw new Error('No template selected')
54
- }
55
-
56
- debug('User selected template URL: %s', specifiedTemplate.url)
57
- return bootstrapFromUrl(context, specifiedTemplate.url)
58
- }
59
-
60
- async function bootstrapFromUrl(context: CliCommandContext, url: string): Promise<void> {
61
- const {output, prompt, yarn, workDir} = context
62
-
63
- debug('Bootstrapping from URL: %s', url)
64
- const {name, outputPath, inPluginsPath, dependencies} = await bootstrapFromTemplate(context, url)
65
-
66
- if (inPluginsPath) {
67
- const addIt = await prompt.single({
68
- type: 'confirm',
69
- message: 'Enable plugin in current Sanity installation?',
70
- default: true,
71
- })
72
-
73
- if (addIt) {
74
- await addPluginToManifest(workDir, name.replace(/^sanity-plugin-/, ''))
75
- }
76
- }
77
-
78
- if (dependencies) {
79
- const dependencyString = JSON.stringify(dependencies, null, 2)
80
- .split('\n')
81
- .slice(1, -1)
82
- .join('\n')
83
- .replace(/"/g, '')
84
-
85
- output.print('\nThe following dependencies are required for this template:')
86
- output.print(`${dependencyString}\n`)
87
- }
88
-
89
- if (dependencies && inPluginsPath) {
90
- const addDeps = await prompt.single({
91
- type: 'confirm',
92
- message: 'Install dependencies in current project?',
93
- default: true,
94
- })
95
-
96
- if (addDeps) {
97
- const deps = Object.keys(dependencies).map((dep) => `${dep}@${dependencies[dep]}`)
98
- await yarn(['add'].concat(deps), {...output, rootDir: workDir})
99
-
100
- output.print('Dependencies installed.')
101
- output.print('Remember to remove them from `package.json` if you no longer need them!')
102
- }
103
- }
104
-
105
- output.print(`\nSuccess! Plugin initialized at ${outputPath}`)
106
- }
107
-
108
- async function addPluginToManifest(sanityDir: string, pluginName: string): Promise<SanityJson> {
109
- const manifestPath = path.join(sanityDir, 'sanity.json')
110
- const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'))
111
-
112
- manifest.plugins = manifest.plugins || []
113
- if (manifest.plugins.indexOf(pluginName) === -1) {
114
- manifest.plugins.push(pluginName)
115
- }
116
-
117
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2))
118
- return manifest
119
- }
@@ -1,38 +0,0 @@
1
- export interface PluginTemplate {
2
- value: string
3
- name: string
4
- url: string
5
- }
6
-
7
- export const pluginTemplates: PluginTemplate[] = [
8
- {
9
- value: 'logo',
10
- name: 'Studio logo',
11
- url: 'https://github.com/sanity-io/plugin-template-logo/archive/master.zip',
12
- },
13
- {
14
- value: 'tool',
15
- name: 'Basic, empty tool',
16
- url: 'https://github.com/sanity-io/plugin-template-tool/archive/master.zip',
17
- },
18
- {
19
- value: 'toolWithRouting',
20
- name: 'Tool with basic routing',
21
- url: 'https://github.com/sanity-io/plugin-template-tool-with-routing/archive/master.zip',
22
- },
23
- {
24
- value: 'chessInput',
25
- name: 'Chess board input component w/ block preview',
26
- url: 'https://github.com/sanity-io/plugin-template-chess-input/archive/master.zip',
27
- },
28
- {
29
- value: 'dashboardWidget',
30
- name: 'A Dashboard widget with cats',
31
- url: 'https://github.com/sanity-io/plugin-template-dashboard-widget-cats/archive/master.zip',
32
- },
33
- {
34
- value: 'assetSource',
35
- name: 'Custom asset source plugin',
36
- url: 'https://github.com/sanity-io/plugin-template-asset-source/archive/master.zip',
37
- },
38
- ]