UNPKG

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

210 lines (177 loc) 6.35 kB
import {createHash} from 'node:crypto' import {mkdir, writeFile} from 'node:fs/promises' import {dirname, join, resolve} from 'node:path' import {Worker} from 'node:worker_threads' import {type CliCommandArguments, type CliCommandContext} from '@sanity/cli' import chalk from 'chalk' import {minutesToMilliseconds} from 'date-fns' import readPkgUp from 'read-pkg-up' import { type CreateManifest, type CreateWorkspaceManifest, type ManifestWorkspaceFile, } from '../../../manifest/manifestTypes' import {type ExtractManifestWorkerData} from '../../threads/extractManifest' import {getTimer} from '../../util/timing' import {SCHEMA_STORE_FEATURE_ENABLED} from '../schema/schemaStoreConstants' export const MANIFEST_FILENAME = 'create-manifest.json' const SCHEMA_FILENAME_SUFFIX = '.create-schema.json' const TOOLS_FILENAME_SUFFIX = '.create-tools.json' /** Escape-hatch env flags to change action behavior */ const FEATURE_ENABLED_ENV_NAME = 'SANITY_CLI_EXTRACT_MANIFEST_ENABLED' const EXTRACT_MANIFEST_ENABLED = process.env[FEATURE_ENABLED_ENV_NAME] !== 'false' const EXTRACT_MANIFEST_LOG_ERRORS = process.env.SANITY_CLI_EXTRACT_MANIFEST_LOG_ERRORS === 'true' const CREATE_TIMER = 'create-manifest' const EXTRACT_TASK_TIMEOUT_MS = minutesToMilliseconds(2) const EXTRACT_FAILURE_MESSAGE = "↳ Couldn't extract manifest file. Sanity Create will not be available for the studio.\n" + ` Disable this message with ${FEATURE_ENABLED_ENV_NAME}=false` export interface ExtractManifestFlags { path?: string } /** * This function will never throw. * @returns `undefined` if extract succeeded - caught error if it failed */ export async function extractManifestSafe( args: CliCommandArguments<ExtractManifestFlags>, context: CliCommandContext, ): Promise<Error | undefined> { if (!EXTRACT_MANIFEST_ENABLED) { return undefined } try { await extractManifest(args, context) return undefined } catch (err) { if (!SCHEMA_STORE_FEATURE_ENABLED) { // preserves current behavior while schema store is disabled context.output.print( chalk.gray( "↳ Couldn't extract manifest file. Sanity Create will not be available for the studio.\n" + ` Disable this message with ${FEATURE_ENABLED_ENV_NAME}=false`, ), ) } if (EXTRACT_MANIFEST_LOG_ERRORS) { context.output.error(err) } return err } } async function extractManifest( args: CliCommandArguments<ExtractManifestFlags>, context: CliCommandContext, ): Promise<void> { const {output, workDir} = context const flags = args.extOptions const defaultOutputDir = resolve(join(workDir, 'dist')) const outputDir = resolve(defaultOutputDir) const defaultStaticPath = join(outputDir, 'static') const staticPath = flags.path ?? defaultStaticPath const path = join(staticPath, MANIFEST_FILENAME) const rootPkgPath = readPkgUp.sync({cwd: __dirname})?.path if (!rootPkgPath) { throw new Error('Could not find root directory for `sanity` package') } const timer = getTimer() timer.start(CREATE_TIMER) const spinner = output.spinner({}).start('Extracting manifest') try { const workspaceManifests = await getWorkspaceManifests({rootPkgPath, workDir}) await mkdir(staticPath, {recursive: true}) const workspaceFiles = await writeWorkspaceFiles(workspaceManifests, staticPath) const manifest: CreateManifest = { /** * Version history: * 1: Initial release. * 2: Added tools file. */ version: 2, createdAt: new Date().toISOString(), workspaces: workspaceFiles, } await writeFile(path, JSON.stringify(manifest, null, 2)) const manifestDuration = timer.end(CREATE_TIMER) spinner.succeed(`Extracted manifest (${manifestDuration.toFixed()}ms)`) } catch (err) { spinner.fail(err.message) throw err } } async function getWorkspaceManifests({ rootPkgPath, workDir, }: { rootPkgPath: string workDir: string }): Promise<CreateWorkspaceManifest[]> { const workerPath = join( dirname(rootPkgPath), 'lib', '_internal', 'cli', 'threads', 'extractManifest.js', ) const worker = new Worker(workerPath, { workerData: {workDir} satisfies ExtractManifestWorkerData, // eslint-disable-next-line no-process-env env: process.env, }) let timeout = false const timeoutId = setTimeout(() => { timeout = true worker.terminate() }, EXTRACT_TASK_TIMEOUT_MS) try { return await new Promise<CreateWorkspaceManifest[]>((resolveWorkspaces, reject) => { const buffer: CreateWorkspaceManifest[] = [] worker.addListener('message', (message) => buffer.push(message)) worker.addListener('exit', (exitCode) => { if (exitCode === 0) { resolveWorkspaces(buffer) } else if (timeout) { reject(new Error(`Extract manifest was aborted after ${EXTRACT_TASK_TIMEOUT_MS}ms`)) } }) worker.addListener('error', reject) }) } finally { clearTimeout(timeoutId) } } function writeWorkspaceFiles( manifestWorkspaces: CreateWorkspaceManifest[], staticPath: string, ): Promise<ManifestWorkspaceFile[]> { const output = manifestWorkspaces.reduce<Promise<ManifestWorkspaceFile>[]>( (workspaces, workspace) => { return [...workspaces, writeWorkspaceFile(workspace, staticPath)] }, [], ) return Promise.all(output) } async function writeWorkspaceFile( workspace: CreateWorkspaceManifest, staticPath: string, ): Promise<ManifestWorkspaceFile> { const [schemaFilename, toolsFilename] = await Promise.all([ createFile(staticPath, workspace.schema, SCHEMA_FILENAME_SUFFIX), createFile(staticPath, workspace.tools, TOOLS_FILENAME_SUFFIX), ]) return { ...workspace, schema: schemaFilename, tools: toolsFilename, } } const createFile = async (path: string, content: any, filenameSuffix: string) => { const stringifiedContent = JSON.stringify(content, null, 2) const hash = createHash('sha1').update(stringifiedContent).digest('hex') const filename = `${hash.slice(0, 8)}${filenameSuffix}` // workspaces with identical data will overwrite each others file. This is ok, since they are identical and can be shared await writeFile(join(path, filename), stringifiedContent) return filename }