sanity
Version:
Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches
192 lines (162 loc) • 5.48 kB
text/typescript
import {type CliCommandArguments, type CliCommandContext} from '@sanity/cli'
import {type SanityClient} from '@sanity/client'
import {promises as fs} from 'fs'
import path from 'path'
import tar from 'tar-fs'
import zlib from 'zlib'
import buildSanityStudio, {type BuildSanityStudioCommandFlags} from '../build/buildAction'
export interface DeployStudioActionFlags extends BuildSanityStudioCommandFlags {
build?: boolean
}
export default async function deployStudio(
args: CliCommandArguments<DeployStudioActionFlags>,
context: CliCommandContext,
): Promise<void> {
const {apiClient, workDir, chalk, output, prompt} = context
const flags = {build: true, ...args.extOptions}
const destFolder = args.argsWithoutOptions[0]
const sourceDir = path.resolve(process.cwd(), destFolder || path.join(workDir, 'dist'))
if (destFolder === 'graphql') {
throw new Error('Did you mean `sanity graphql deploy`?')
}
if (destFolder) {
let relativeOutput = path.relative(process.cwd(), sourceDir)
if (relativeOutput[0] !== '.') {
relativeOutput = `./${relativeOutput}`
}
const isEmpty = await dirIsEmptyOrNonExistent(sourceDir)
const shouldProceed =
isEmpty ||
(await prompt.single({
type: 'confirm',
message: `"${relativeOutput}" is not empty, do you want to proceed?`,
default: false,
}))
if (!shouldProceed) {
output.print('Cancelled.')
return
}
output.print(`Building to ${relativeOutput}\n`)
}
const client = apiClient({
requireUser: true,
requireProject: true,
})
// Check that the project has a studio hostname
let spinner = output.spinner('Checking project info').start()
const project = await client.projects.getById(client.config().projectId as string)
let studioHostname = project && project.studioHost
spinner.succeed()
if (!studioHostname) {
output.print('Your project has not been assigned a studio hostname.')
output.print('To deploy your Sanity Studio to our hosted Sanity.Studio service,')
output.print('you will need one. Please enter the part you want to use.')
studioHostname = await prompt.single({
type: 'input',
filter: (inp: string) => inp.replace(/\.sanity\.studio$/i, ''),
message: 'Studio hostname (<value>.sanity.studio):',
validate: (name: string) => validateHostname(name, client),
})
}
// Always build the project, unless --no-build is passed
const shouldBuild = flags.build
if (shouldBuild) {
const buildArgs = [destFolder].filter(Boolean)
const {didCompile} = await buildSanityStudio(
{...args, extOptions: flags, argsWithoutOptions: buildArgs},
context,
{basePath: '/'},
)
if (!didCompile) {
return
}
}
// Ensure that the directory exists, is a directory and seems to have valid content
spinner = output.spinner('Verifying local content').start()
try {
await checkDir(sourceDir)
spinner.succeed()
} catch (err) {
spinner.fail()
throw err
}
// Now create a tarball of the given directory
const parentDir = path.dirname(sourceDir)
const base = path.basename(sourceDir)
const tarball = tar.pack(parentDir, {entries: [base]}).pipe(zlib.createGzip())
spinner = output.spinner('Deploying to Sanity.Studio').start()
try {
const response = await client.request({
method: 'POST',
url: '/deploy',
body: tarball,
maxRedirects: 0,
})
spinner.succeed()
// And let the user know we're done
output.print(`\nSuccess! Studio deployed to ${chalk.cyan(response.location)}`)
} catch (err) {
spinner.fail()
throw err
}
}
async function dirIsEmptyOrNonExistent(sourceDir: string): Promise<boolean> {
try {
const stats = await fs.stat(sourceDir)
if (!stats.isDirectory()) {
throw new Error(`Directory ${sourceDir} is not a directory`)
}
} catch (err) {
if (err.code === 'ENOENT') {
return true
}
throw err
}
const content = await fs.readdir(sourceDir)
return content.length === 0
}
async function checkDir(sourceDir: string) {
try {
const stats = await fs.stat(sourceDir)
if (!stats.isDirectory()) {
throw new Error(`Directory ${sourceDir} is not a directory`)
}
} catch (err) {
const error = err.code === 'ENOENT' ? new Error(`Directory "${sourceDir}" does not exist`) : err
throw error
}
try {
await fs.stat(path.join(sourceDir, 'index.html'))
} catch (err) {
const error =
err.code === 'ENOENT'
? new Error(
[
`"${sourceDir}/index.html" does not exist -`,
'[SOURCE_DIR] must be a directory containing',
'a Sanity studio built using "sanity build"',
].join(' '),
)
: err
throw error
}
}
async function validateHostname(value: string, client: SanityClient): Promise<boolean | string> {
const projectId = client.config().projectId
const uri = `/projects/${projectId}`
const studioHost = value || ''
// Check that it matches allowed character range
if (!/^[a-z0-9_-]+$/i.test(studioHost)) {
return 'Hostname can contain only A-Z, 0-9, _ and -'
}
// Check that the hostname is not already taken
try {
await client.request({uri, method: 'PATCH', body: {studioHost}})
return true
} catch (error) {
if (error?.response?.body?.message) {
return error.response.body.message
}
throw error
}
}