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
133 lines (117 loc) • 4.28 kB
text/typescript
import {existsSync, mkdirSync} from 'node:fs'
import {writeFile} from 'node:fs/promises'
import path from 'node:path'
import {type CliCommandDefinition} from '@sanity/cli'
import deburr from 'lodash/deburr'
import {MIGRATIONS_DIRECTORY} from './constants'
import {minimalAdvanced} from './templates/minimalAdvanced'
import {minimalSimple} from './templates/minimalSimple'
import {renameField} from './templates/renameField'
import {renameType} from './templates/renameType'
import {stringToPTE} from './templates/stringToPTE'
const helpText = `
Examples:
# Create a new migration, prompting for title and options
sanity migration create
# Create a new migration with the provided title, prompting for options
sanity migration create "Rename field from location to address"
`
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface CreateMigrationFlags {}
const TEMPLATES = [
{name: 'Minimalistic migration to get you started', template: minimalSimple},
{name: 'Rename an object type', template: renameType},
{name: 'Rename a field', template: renameField},
{name: 'Convert string field to Portable Text', template: stringToPTE},
{
name: 'Advanced template using async iterators providing more fine grained control',
template: minimalAdvanced,
},
]
const createMigrationCommand: CliCommandDefinition<CreateMigrationFlags> = {
name: 'create',
group: 'migration',
signature: '[TITLE]',
helpText,
description: 'Create a new migration within your project',
action: async (args, context) => {
const {output, prompt, workDir, chalk} = context
let [title] = args.argsWithoutOptions
while (!title?.trim()) {
title = await prompt.single({
type: 'input',
suffix: ' (e.g. "Rename field from location to address")',
message: 'Title of migration',
})
if (!title.trim()) {
output.error(chalk.red('Name cannot be empty'))
}
}
const types = await prompt.single({
type: 'input',
suffix: ' (optional)',
message: 'Type of documents to migrate. You can add multiple types separated by comma',
})
const templatesByName = Object.fromEntries(TEMPLATES.map((t) => [t.name, t]))
const template = await prompt.single({
type: 'list',
message: 'Select a template',
choices: TEMPLATES.map((definedTemplate) => ({
name: definedTemplate.name,
value: definedTemplate.name,
})),
})
const sluggedName = deburr(title.toLowerCase())
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
const destDir = path.join(workDir, MIGRATIONS_DIRECTORY, sluggedName)
if (existsSync(destDir)) {
if (
!(await prompt.single({
type: 'confirm',
message: `Migration directory ${chalk.cyan(destDir)} already exists. Overwrite?`,
default: false,
}))
) {
return
}
}
mkdirSync(destDir, {recursive: true})
const renderedTemplate = (templatesByName[template].template || minimalSimple)({
migrationName: title,
documentTypes: types
.split(',')
.map((t) => t.trim())
.filter(Boolean),
})
const definitionFile = path.join(destDir, 'index.ts')
await writeFile(definitionFile, renderedTemplate)
// To dry run it, run \`sanity migration run ${sluggedName}\``)
output.print()
output.print(`${chalk.green('✓')} Migration created!`)
output.print()
output.print('Next steps:')
output.print(
`Open ${chalk.bold(
definitionFile,
)} in your code editor and write the code for your migration.`,
)
output.print(
`Dry run the migration with:\n\`${chalk.bold(
`sanity migration run ${sluggedName} --project=<projectId> --dataset <dataset> `,
)}\``,
)
output.print(
`Run the migration against a dataset with:\n \`${chalk.bold(
`sanity migration run ${sluggedName} --project=<projectId> --dataset <dataset> --no-dry-run`,
)}\``,
)
output.print()
output.print(
`👉 Learn more about schema and content migrations at ${chalk.bold(
'https://www.sanity.io/docs/schema-and-content-migrations',
)}`,
)
},
}
export default createMigrationCommand