@sanity/cli 3.59.2-canary.19 → 3.59.2-corel-fix-presentation-perspective-switching.536

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.
Files changed (48) hide show
  1. package/lib/_chunks-cjs/cli.js +34359 -26058
  2. package/lib/_chunks-cjs/cli.js.map +1 -1
  3. package/lib/_chunks-cjs/cliWorker.js.map +1 -1
  4. package/lib/_chunks-cjs/generateAction.js.map +1 -1
  5. package/lib/_chunks-cjs/getCliConfig.js +1 -1
  6. package/lib/_chunks-cjs/getCliConfig.js.map +1 -1
  7. package/lib/_chunks-cjs/journeyConfig.js.map +1 -1
  8. package/lib/_chunks-cjs/loadEnv.js +200 -202
  9. package/lib/_chunks-cjs/loadEnv.js.map +1 -1
  10. package/lib/index.d.mts +37 -1
  11. package/lib/index.d.ts +37 -1
  12. package/lib/index.esm.js +223 -224
  13. package/lib/index.esm.js.map +1 -1
  14. package/lib/index.js.map +1 -1
  15. package/lib/index.mjs +223 -224
  16. package/lib/index.mjs.map +1 -1
  17. package/lib/workers/getCliConfig.js.map +1 -1
  18. package/lib/workers/typegenGenerate.js.map +1 -1
  19. package/package.json +17 -19
  20. package/src/CommandRunner.ts +1 -2
  21. package/src/actions/init-project/{bootstrapTemplate.ts → bootstrapLocalTemplate.ts} +8 -21
  22. package/src/actions/init-project/bootstrapRemoteTemplate.ts +118 -0
  23. package/src/actions/init-project/git.ts +2 -2
  24. package/src/actions/init-project/initProject.ts +158 -146
  25. package/src/actions/init-project/readPackageJson.ts +18 -0
  26. package/src/actions/init-project/templates/nextjs/index.ts +16 -0
  27. package/src/actions/init-project/templates/nextjs/schemaTypes/blog.ts +2 -2
  28. package/src/actions/init-project/updateInitialTemplateMetadata.ts +24 -0
  29. package/src/actions/login/login.ts +2 -3
  30. package/src/actions/versions/findSanityModuleVersions.ts +0 -1
  31. package/src/commands/index.ts +2 -2
  32. package/src/commands/init/initCommand.ts +7 -67
  33. package/src/commands/learn/learnCommand.ts +20 -0
  34. package/src/commands/logout/logoutCommand.ts +1 -1
  35. package/src/outputters/cliOutputter.ts +21 -8
  36. package/src/studioDependencies.ts +1 -1
  37. package/src/types.ts +41 -1
  38. package/src/util/frameworkPort.ts +63 -0
  39. package/src/util/generateCommandsDocumentation.ts +7 -4
  40. package/src/util/getCliConfig.ts +1 -1
  41. package/src/util/getProviderName.ts +9 -0
  42. package/src/util/remoteTemplate.ts +319 -0
  43. package/templates/get-started/plugins/sanity-plugin-tutorial/GetStartedTutorial.tsx +4 -4
  44. package/src/actions/init-plugin/initPlugin.ts +0 -119
  45. package/src/actions/init-plugin/pluginTemplates.ts +0 -38
  46. package/src/actions/init-project/reconfigureV2Project.ts +0 -446
  47. package/src/commands/upgrade/upgradeCommand.ts +0 -38
  48. package/src/commands/upgrade/upgradeDependencies.ts +0 -289
@@ -0,0 +1,319 @@
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 {ENV_TEMPLATE_FILES, REQUIRED_ENV_VAR} from '@sanity/template-validator'
8
+ import {x} from 'tar'
9
+
10
+ import {debug} from '../debug'
11
+ import {type CliApiClient, type PackageJson} from '../types'
12
+
13
+ const DISALLOWED_PATHS = [
14
+ // Prevent security risks from unknown GitHub Actions
15
+ '/.github/',
16
+ ]
17
+
18
+ const ENV_VAR = {
19
+ ...REQUIRED_ENV_VAR,
20
+ READ_TOKEN: 'SANITY_API_READ_TOKEN',
21
+ WRITE_TOKEN: 'SANITY_API_WRITE_TOKEN',
22
+ } as const
23
+
24
+ const API_READ_TOKEN_ROLE = 'viewer'
25
+ const API_WRITE_TOKEN_ROLE = 'editor'
26
+
27
+ type EnvData = {
28
+ projectId: string
29
+ dataset: string
30
+ readToken?: string
31
+ writeToken?: string
32
+ }
33
+
34
+ type GithubUrlString =
35
+ | `https://github.com/${string}/${string}`
36
+ | `https://www.github.com/${string}/${string}`
37
+
38
+ export type RepoInfo = {
39
+ username: string
40
+ name: string
41
+ branch: string
42
+ filePath: string
43
+ }
44
+
45
+ export function getGitHubRawContentUrl(repoInfo: RepoInfo): string {
46
+ const {username, name, branch, filePath} = repoInfo
47
+ return `https://raw.githubusercontent.com/${username}/${name}/${branch}/${filePath}`
48
+ }
49
+
50
+ function isGithubRepoShorthand(value: string): boolean {
51
+ if (URL.canParse(value)) {
52
+ return false
53
+ }
54
+ // This supports :owner/:repo and :owner/:repo/nested/path, e.g.
55
+ // sanity-io/sanity
56
+ // sanity-io/sanity/templates/next-js
57
+ // sanity-io/templates/live-content-api
58
+ // sanity-io/sanity/packages/@sanity/cli/test/test-template
59
+ return /^[\w-]+\/[\w-.]+(\/[@\w-.]+)*$/.test(value)
60
+ }
61
+
62
+ function isGithubRepoUrl(value: string | URL): value is URL | GithubUrlString {
63
+ if (URL.canParse(value) === false) {
64
+ return false
65
+ }
66
+ const url = new URL(value)
67
+ const pathSegments = url.pathname.slice(1).split('/')
68
+
69
+ return (
70
+ url.protocol === 'https:' &&
71
+ url.hostname === 'github.com' &&
72
+ // The pathname must have at least 2 segments. If it has more than 2, the
73
+ // third must be "tree" and it must have at least 4 segments.
74
+ // https://github.com/:owner/:repo
75
+ // https://github.com/:owner/:repo/tree/:ref
76
+ pathSegments.length >= 2 &&
77
+ (pathSegments.length > 2 ? pathSegments[2] === 'tree' && pathSegments.length >= 4 : true)
78
+ )
79
+ }
80
+
81
+ async function downloadTarStream(url: string, bearerToken?: string): Promise<Readable> {
82
+ const headers: Record<string, string> = {}
83
+ if (bearerToken) {
84
+ headers.Authorization = `Bearer ${bearerToken}`
85
+ }
86
+
87
+ const res = await fetch(url, {headers})
88
+
89
+ if (!res.body) {
90
+ throw new Error(`Failed to download: ${url}`)
91
+ }
92
+
93
+ return Readable.fromWeb(res.body as ReadableStream)
94
+ }
95
+
96
+ export function checkIsRemoteTemplate(templateName?: string): boolean {
97
+ return templateName?.includes('/') ?? false
98
+ }
99
+
100
+ export async function getGitHubRepoInfo(value: string, bearerToken?: string): Promise<RepoInfo> {
101
+ let username = ''
102
+ let name = ''
103
+ let branch = ''
104
+ let filePath = ''
105
+
106
+ if (isGithubRepoShorthand(value)) {
107
+ const parts = value.split('/')
108
+ username = parts[0]
109
+ name = parts[1]
110
+ // If there are more segments after owner/repo, they form the file path
111
+ if (parts.length > 2) {
112
+ filePath = parts.slice(2).join('/')
113
+ }
114
+ }
115
+
116
+ if (isGithubRepoUrl(value)) {
117
+ const url = new URL(value)
118
+ const pathSegments = url.pathname.slice(1).split('/')
119
+ username = pathSegments[0]
120
+ name = pathSegments[1]
121
+
122
+ // If we have a "tree" segment, everything after branch is the file path
123
+ if (pathSegments[2] === 'tree') {
124
+ branch = pathSegments[3]
125
+ if (pathSegments.length > 4) {
126
+ filePath = pathSegments.slice(4).join('/')
127
+ }
128
+ }
129
+ }
130
+
131
+ if (!username || !name) {
132
+ throw new Error('Invalid GitHub repository format')
133
+ }
134
+
135
+ const tokenMessage =
136
+ 'GitHub repository not found. For private repositories, use --template-token to provide an access token.\n\n' +
137
+ 'You can generate a new token at https://github.com/settings/personal-access-tokens/new\n' +
138
+ 'Set the token to "read-only" with repository access and a short expiry (e.g. 7 days) for security.'
139
+
140
+ try {
141
+ const headers: Record<string, string> = {}
142
+ if (bearerToken) {
143
+ headers.Authorization = `Bearer ${bearerToken}`
144
+ }
145
+
146
+ const infoResponse = await fetch(`https://api.github.com/repos/${username}/${name}`, {
147
+ headers,
148
+ })
149
+
150
+ if (infoResponse.status !== 200) {
151
+ if (infoResponse.status === 404) {
152
+ throw new Error(tokenMessage)
153
+ }
154
+ throw new Error('GitHub repository not found')
155
+ }
156
+
157
+ const info = await infoResponse.json()
158
+
159
+ return {
160
+ username,
161
+ name,
162
+ branch: branch || info.default_branch,
163
+ filePath,
164
+ }
165
+ } catch {
166
+ throw new Error(tokenMessage)
167
+ }
168
+ }
169
+
170
+ export async function downloadAndExtractRepo(
171
+ root: string,
172
+ {username, name, branch, filePath}: RepoInfo,
173
+ bearerToken?: string,
174
+ ): Promise<void> {
175
+ let rootPath: string | null = null
176
+ await pipeline(
177
+ await downloadTarStream(
178
+ `https://codeload.github.com/${username}/${name}/tar.gz/${branch}`,
179
+ bearerToken,
180
+ ),
181
+ x({
182
+ cwd: root,
183
+ strip: filePath ? filePath.split('/').length + 1 : 1,
184
+ filter: (p: string) => {
185
+ const posixPath = p.split(sep).join(posix.sep)
186
+ if (rootPath === null) {
187
+ const pathSegments = posixPath.split(posix.sep)
188
+ rootPath = pathSegments.length ? pathSegments[0] : null
189
+ }
190
+ for (const disallowedPath of DISALLOWED_PATHS) {
191
+ if (posixPath.includes(disallowedPath)) return false
192
+ }
193
+ return posixPath.startsWith(`${rootPath}${filePath ? `/${filePath}/` : '/'}`)
194
+ },
195
+ }),
196
+ )
197
+ }
198
+
199
+ export async function checkIfNeedsApiToken(root: string, type: 'read' | 'write'): Promise<boolean> {
200
+ try {
201
+ const templatePath = await Promise.any(
202
+ ENV_TEMPLATE_FILES.map(async (file) => {
203
+ await access(join(root, file))
204
+ return file
205
+ }),
206
+ )
207
+ const templateContent = await readFile(join(root, templatePath), 'utf8')
208
+ return templateContent.includes(type === 'read' ? ENV_VAR.READ_TOKEN : ENV_VAR.WRITE_TOKEN)
209
+ } catch {
210
+ return false
211
+ }
212
+ }
213
+
214
+ export async function applyEnvVariables(
215
+ root: string,
216
+ envData: EnvData,
217
+ targetName = '.env',
218
+ ): Promise<void> {
219
+ const templatePath = await Promise.any(
220
+ ENV_TEMPLATE_FILES.map(async (file) => {
221
+ await access(join(root, file))
222
+ return file
223
+ }),
224
+ ).catch(() => undefined)
225
+
226
+ if (!templatePath) {
227
+ return // No template .env file found, skip
228
+ }
229
+
230
+ try {
231
+ const templateContent = await readFile(join(root, templatePath), 'utf8')
232
+ const {projectId, dataset, readToken = ''} = envData
233
+
234
+ const findAndReplaceVariable = (
235
+ content: string,
236
+ varRegex: RegExp | string,
237
+ value: string,
238
+ useQuotes: boolean,
239
+ ) => {
240
+ const varPattern = typeof varRegex === 'string' ? varRegex : varRegex.source
241
+ const pattern = new RegExp(`.*${varPattern}=.*$`, 'gm')
242
+ const matches = content.matchAll(pattern)
243
+ return Array.from(matches).reduce((updatedContent, match) => {
244
+ if (!match[0]) return updatedContent
245
+ const varName = match[0].split('=')[0].trim()
246
+ return updatedContent.replace(
247
+ new RegExp(`${varName}=.*$`, 'gm'),
248
+ `${varName}=${useQuotes ? `"${value}"` : value}`,
249
+ )
250
+ }, content)
251
+ }
252
+
253
+ let envContent = templateContent
254
+ const vars = [
255
+ {pattern: ENV_VAR.PROJECT_ID, value: projectId},
256
+ {pattern: ENV_VAR.DATASET, value: dataset},
257
+ {pattern: ENV_VAR.READ_TOKEN, value: readToken},
258
+ ]
259
+ const useQuotes = templateContent.includes('="')
260
+
261
+ for (const {pattern, value} of vars) {
262
+ envContent = findAndReplaceVariable(envContent, pattern, value, useQuotes)
263
+ }
264
+
265
+ await writeFile(join(root, targetName), envContent)
266
+ } catch (err) {
267
+ throw new Error(
268
+ '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.',
269
+ )
270
+ }
271
+ }
272
+
273
+ export async function tryApplyPackageName(root: string, name: string): Promise<void> {
274
+ try {
275
+ const packageJson = await readFile(join(root, 'package.json'), 'utf8')
276
+ const pkg: PackageJson = JSON.parse(packageJson)
277
+ pkg.name = name
278
+
279
+ await writeFile(join(root, 'package.json'), JSON.stringify(pkg, null, 2))
280
+ } catch (err) {
281
+ // noop
282
+ }
283
+ }
284
+
285
+ export async function generateSanityApiToken(
286
+ label: string,
287
+ type: 'read' | 'write',
288
+ projectId: string,
289
+ apiClient: CliApiClient,
290
+ ): Promise<string> {
291
+ const response = await apiClient({requireProject: false, requireUser: true})
292
+ .config({apiVersion: 'v2021-06-07'})
293
+ .request<{key: string}>({
294
+ uri: `/projects/${projectId}/tokens`,
295
+ method: 'POST',
296
+ body: {
297
+ label: `${label} (${Date.now()})`,
298
+ roleName: type === 'read' ? API_READ_TOKEN_ROLE : API_WRITE_TOKEN_ROLE,
299
+ },
300
+ })
301
+ return response.key
302
+ }
303
+
304
+ export async function setCorsOrigin(
305
+ origin: string,
306
+ projectId: string,
307
+ apiClient: CliApiClient,
308
+ ): Promise<void> {
309
+ try {
310
+ await apiClient({api: {projectId}}).request({
311
+ method: 'POST',
312
+ url: '/cors',
313
+ body: {origin: origin, allowCredentials: true}, // allowCredentials is true to allow for embedded studios if needed
314
+ })
315
+ } catch (error) {
316
+ // Silent fail, it most likely means that the origin is already set
317
+ debug('Failed to set CORS origin', error)
318
+ }
319
+ }
@@ -1,4 +1,4 @@
1
- import React, {useRef, useState} from 'react'
1
+ import React, {useState} from 'react'
2
2
  import {
3
3
  Card,
4
4
  Container,
@@ -32,8 +32,8 @@ export const GetStartedTutorial = () => {
32
32
  )
33
33
 
34
34
  const {sanity} = useTheme()
35
- const rootElement = useRef(null)
36
- const rect = useElementSize(rootElement.current)
35
+ const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
36
+ const rect = useElementSize(rootElement)
37
37
  const width = rect?.content?.width
38
38
  const isSmallScreen = width ? width < sanity.media[1] : false
39
39
  const isProdEnv = process.env.NODE_ENV !== 'development'
@@ -48,7 +48,7 @@ export const GetStartedTutorial = () => {
48
48
  }
49
49
 
50
50
  return (
51
- <div ref={rootElement}>
51
+ <div ref={setRootElement}>
52
52
  <Card tone="primary" padding={isSmallScreen ? 3 : 5} paddingBottom={isSmallScreen ? 4 : 6}>
53
53
  <Flex justify={isSmallScreen ? 'space-between' : 'flex-end'} align="center">
54
54
  {isSmallScreen && (
@@ -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
- ]