Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): add support for remote templates with --template #7867

Merged
merged 13 commits into from
Dec 3, 2024
3 changes: 2 additions & 1 deletion packages/@sanity/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
"prettier": "^3.3.0",
"semver": "^7.3.5",
"silver-fleece": "1.1.0",
"validate-npm-package-name": "^3.0.0"
"validate-npm-package-name": "^3.0.0",
"yaml": "^2.6.1"
},
"devDependencies": {
"@repo/package.config": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import {createPackageManifest} from './createPackageManifest'
import {createStudioConfig, type GenerateConfigOptions} from './createStudioConfig'
import {type ProjectTemplate} from './initProject'
import templates from './templates'
import {updateInitialTemplateMetadata} from './updateInitialTemplateMetadata'

export interface BootstrapOptions {
export interface BootstrapLocalOptions {
packageName: string
templateName: string
/**
Expand All @@ -28,8 +29,8 @@ export interface BootstrapOptions {
variables: GenerateConfigOptions['variables']
}

export async function bootstrapTemplate(
opts: BootstrapOptions,
export async function bootstrapLocalTemplate(
opts: BootstrapLocalOptions,
context: CliCommandContext,
): Promise<ProjectTemplate> {
const {apiClient, cliRoot, output} = context
Expand Down Expand Up @@ -142,22 +143,8 @@ export async function bootstrapTemplate(
),
])

// Store template name metadata on project
try {
await apiClient({api: {projectId}}).request({
method: 'PATCH',
uri: `/projects/${projectId}`,
body: {metadata: {initialTemplate: `cli-${templateName}`}},
})
} catch (err: unknown) {
// Non-critical that we update this metadata, and user does not need to be aware
let message = typeof err === 'string' ? err : '<unknown error>'
if (err instanceof Error) {
message = err.message
}

debug('Failed to update initial template metadata for project: %s', message)
}
debug('Updating initial template metadata')
await updateInitialTemplateMetadata(apiClient, variables.projectId, `cli-${templateName}`)

// Finish up by providing init process with template-specific info
spinner.succeed()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {mkdir} from 'node:fs/promises'
import {join} from 'node:path'

import {debug} from '../../debug'
import {type CliCommandContext} from '../../types'
import {
applyEnvVariables,
downloadAndExtractRepo,
generateSanityApiReadToken,
getMonoRepo,
isNextJsTemplate,
type RepoInfo,
tryApplyPackageName,
validateRemoteTemplate,
} from '../../util/remoteTemplate'
import {type GenerateConfigOptions} from './createStudioConfig'
import {tryGitInit} from './git'
import {updateInitialTemplateMetadata} from './updateInitialTemplateMetadata'

export interface BootstrapRemoteOptions {
outputPath: string
repoInfo: RepoInfo
bearerToken?: string
packageName: string
variables: GenerateConfigOptions['variables']
}

const INITIAL_COMMIT_MESSAGE = 'Initial commit from Sanity CLI'

export async function bootstrapRemoteTemplate(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clearly seeing the pipeline of things that need to be done serially here but unsure how failure of this half-way would look. We have a pattern of showing the error messages in the cli but if this partially completes, it might be confusing to users to know how to proceed if they have the repo down but were unable to have any of the files edited. Perhaps some try/catch around groups of this where the errors are still thrown to be printed on the cli but have some surrounding context like "Unable to edit newly downloaded files in [folder]/. See documentation on updating files from a template: some-docs link" or something else could be helpful in getting people un-stuck at certain points.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the template does not pass validation, we will just abort, and you should get a message with why it did not pass. This is fine imo.

There are a new other steps it could fail in after we've downloaded files over to the users computer.

  1. Applying environment variables: For this I've gone ahead and added a more descriptive error message with a link to our environment variable docs article.
  2. Applying package name: I've renamed this function to tryApplyPackageName, and will noop on catch block, as this is not a big deal if fails.
  3. Git init also runs tryGitInit and will noop if it fails.

opts: BootstrapRemoteOptions,
context: CliCommandContext,
): Promise<void> {
const {outputPath, repoInfo, bearerToken, variables, packageName} = opts
const {output, apiClient} = context
const name = [repoInfo.username, repoInfo.name, repoInfo.filePath].filter(Boolean).join('/')
const spinner = output.spinner(`Bootstrapping files from template "${name}"`).start()

debug('Validating remote template')
const packages = await getMonoRepo(repoInfo, bearerToken)
await validateRemoteTemplate(repoInfo, packages, bearerToken)

debug('Create new directory "%s"', outputPath)
await mkdir(outputPath, {recursive: true})

debug('Downloading and extracting repo to %s', outputPath)
await downloadAndExtractRepo(outputPath, repoInfo, bearerToken)

debug('Applying environment variables')
const readToken = await generateSanityApiReadToken(variables.projectId, apiClient)
const isNext = await isNextJsTemplate(outputPath)
const envName = isNext ? '.env.local' : '.env'

for (const folder of packages ?? ['']) {
const path = join(outputPath, folder)
await applyEnvVariables(path, {...variables, readToken}, envName)
}

debug('Setting package name to %s', packageName)
await tryApplyPackageName(outputPath, packageName)

debug('Initializing git repository')
tryGitInit(outputPath, INITIAL_COMMIT_MESSAGE)

debug('Updating initial template metadata')
await updateInitialTemplateMetadata(apiClient, variables.projectId, `external-${name}`)

spinner.succeed()
}
Loading
Loading