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
169 lines (144 loc) • 5.29 kB
text/typescript
import path from 'path'
import {promisify} from 'util'
import chalk from 'chalk'
import {noopLogger} from '@sanity/telemetry'
import rimrafCallback from 'rimraf'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This may not yet be built.
import type {CliCommandArguments, CliCommandContext} from '@sanity/cli'
import {buildStaticFiles, ChunkModule, ChunkStats} from '../../server'
import {checkStudioDependencyVersions} from '../../util/checkStudioDependencyVersions'
import {checkRequiredDependencies} from '../../util/checkRequiredDependencies'
import {getTimer} from '../../util/timing'
import {BuildTrace} from './build.telemetry'
const rimraf = promisify(rimrafCallback)
export interface BuildSanityStudioCommandFlags {
yes?: boolean
y?: boolean
minify?: boolean
stats?: boolean
'source-maps'?: boolean
}
export default async function buildSanityStudio(
args: CliCommandArguments<BuildSanityStudioCommandFlags>,
context: CliCommandContext,
overrides?: {basePath?: string},
): Promise<{didCompile: boolean}> {
const timer = getTimer()
const {output, prompt, workDir, cliConfig, telemetry = noopLogger} = context
const flags: BuildSanityStudioCommandFlags = {
minify: true,
stats: false,
'source-maps': false,
...args.extOptions,
}
const unattendedMode = Boolean(flags.yes || flags.y)
const defaultOutputDir = path.resolve(path.join(workDir, 'dist'))
const outputDir = path.resolve(args.argsWithoutOptions[0] || defaultOutputDir)
await checkStudioDependencyVersions(workDir)
// If the check resulted in a dependency install, the CLI command will be re-run,
// thus we want to exit early
if ((await checkRequiredDependencies(context)).didInstall) {
return {didCompile: false}
}
const envVarKeys = getSanityEnvVars()
if (envVarKeys.length > 0) {
output.print(
'\nIncluding the following environment variables as part of the JavaScript bundle:',
)
envVarKeys.forEach((key) => output.print(`- ${key}`))
output.print('')
}
let shouldClean = true
if (outputDir !== defaultOutputDir && !unattendedMode) {
shouldClean = await prompt.single({
type: 'confirm',
message: `Do you want to delete the existing directory (${outputDir}) first?`,
default: true,
})
}
// Determine base path for built studio
let basePath = '/'
const envBasePath = process.env.SANITY_STUDIO_BASEPATH
const configBasePath = cliConfig?.project?.basePath
// Allow `sanity deploy` to override base path
if (overrides?.basePath) {
basePath = overrides.basePath
} else if (envBasePath) {
// Environment variable (SANITY_STUDIO_BASEPATH)
basePath = envBasePath
} else if (configBasePath) {
// `sanity.cli.ts`
basePath = configBasePath
}
if (envBasePath && configBasePath) {
output.warn(
`Overriding configured base path (${configBasePath}) with value from environment variable (${envBasePath})`,
)
}
let spin
if (shouldClean) {
timer.start('cleanOutputFolder')
spin = output.spinner('Clean output folder').start()
await rimraf(outputDir)
const cleanDuration = timer.end('cleanOutputFolder')
spin.text = `Clean output folder (${cleanDuration.toFixed()}ms)`
spin.succeed()
}
spin = output.spinner('Build Sanity Studio').start()
const trace = telemetry.trace(BuildTrace)
trace.start()
try {
timer.start('bundleStudio')
const bundle = await buildStaticFiles({
cwd: workDir,
outputDir,
basePath,
sourceMap: Boolean(flags['source-maps']),
minify: Boolean(flags.minify),
vite: cliConfig && 'vite' in cliConfig ? cliConfig.vite : undefined,
})
trace.log({
outputSize: bundle.chunks
.flatMap((chunk) => chunk.modules.flatMap((mod) => mod.renderedLength))
.reduce((sum, n) => sum + n, 0),
})
const buildDuration = timer.end('bundleStudio')
spin.text = `Build Sanity Studio (${buildDuration.toFixed()}ms)`
spin.succeed()
trace.complete()
if (flags.stats) {
output.print('\nLargest module files:')
output.print(formatModuleSizes(sortModulesBySize(bundle.chunks).slice(0, 15)))
}
} catch (err) {
spin.fail()
trace.error(err)
throw err
}
return {didCompile: true}
}
// eslint-disable-next-line no-process-env
function getSanityEnvVars(env: Record<string, string | undefined> = process.env): string[] {
return Object.keys(env).filter((key) => key.toUpperCase().startsWith('SANITY_STUDIO_'))
}
function sortModulesBySize(chunks: ChunkStats[]): ChunkModule[] {
return chunks
.flatMap((chunk) => chunk.modules)
.sort((modA, modB) => modB.renderedLength - modA.renderedLength)
}
function formatModuleSizes(modules: ChunkModule[]): string {
const lines = []
for (const mod of modules) {
lines.push(` - ${formatModuleName(mod.name)} (${formatSize(mod.renderedLength)})`)
}
return lines.join('\n')
}
function formatModuleName(modName: string): string {
const delimiter = '/node_modules/'
const nodeIndex = modName.lastIndexOf(delimiter)
return nodeIndex === -1 ? modName : modName.slice(nodeIndex + delimiter.length)
}
function formatSize(bytes: number): string {
return chalk.cyan(`${(bytes / 1024).toFixed()} kB`)
}