@sanity/cli 8.10.0 → 8.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -26
- package/dist/actions/assets/ingestAssetFromUrlWithProgress.js +21 -0
- package/dist/actions/assets/ingestAssetFromUrlWithProgress.js.map +1 -0
- package/dist/actions/assets/withUrlIngestProgress.js +45 -0
- package/dist/actions/assets/withUrlIngestProgress.js.map +1 -0
- package/dist/actions/auth/authServer.js +5 -8
- package/dist/actions/auth/authServer.js.map +1 -1
- package/dist/actions/auth/login/getProvider.js +10 -14
- package/dist/actions/auth/login/getProvider.js.map +1 -1
- package/dist/actions/auth/login/login.js +0 -1
- package/dist/actions/auth/login/login.js.map +1 -1
- package/dist/actions/deploy/deployApp.js +5 -2
- package/dist/actions/deploy/deployApp.js.map +1 -1
- package/dist/actions/deploy/deployChecks.js +3 -2
- package/dist/actions/deploy/deployChecks.js.map +1 -1
- package/dist/actions/deploy/findUserApplication.js +3 -2
- package/dist/actions/deploy/findUserApplication.js.map +1 -1
- package/dist/actions/deploy/resolveDeployTarget.js +6 -1
- package/dist/actions/deploy/resolveDeployTarget.js.map +1 -1
- package/dist/actions/documents/validateDocuments.worker.js +1 -4
- package/dist/actions/documents/validateDocuments.worker.js.map +1 -1
- package/dist/actions/media/importMedia.js.map +1 -1
- package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js +19 -0
- package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js.map +1 -0
- package/dist/commands/assets/upload.js +78 -52
- package/dist/commands/assets/upload.js.map +1 -1
- package/dist/commands/deploy.js +33 -1
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/media/import.js +149 -13
- package/dist/commands/media/import.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/index.js +1 -4
- package/dist/exports/invokeSanityCli/commandPolicies/index.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +26 -6
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/resolve.js +112 -0
- package/dist/exports/invokeSanityCli/commandPolicies/resolve.js.map +1 -0
- package/dist/exports/invokeSanityCli/help.js +8 -1
- package/dist/exports/invokeSanityCli/help.js.map +1 -1
- package/dist/exports/invokeSanityCli/index.d.ts +1 -2
- package/dist/exports/invokeSanityCli/index.js +37 -15
- package/dist/exports/invokeSanityCli/index.js.map +1 -1
- package/dist/generated/apiRoutes.js +2 -0
- package/dist/generated/apiRoutes.js.map +1 -1
- package/dist/services/assets.js +46 -1
- package/dist/services/assets.js.map +1 -1
- package/dist/services/mediaLibraries.js +39 -1
- package/dist/services/mediaLibraries.js.map +1 -1
- package/dist/util/assetSourceValidation.js +56 -0
- package/dist/util/assetSourceValidation.js.map +1 -0
- package/dist/util/assetUploadErrors.js +64 -0
- package/dist/util/assetUploadErrors.js.map +1 -0
- package/dist/util/isRemoteAssetSource.js +19 -0
- package/dist/util/isRemoteAssetSource.js.map +1 -0
- package/dist/util/parseAspectFlags.js +34 -0
- package/dist/util/parseAspectFlags.js.map +1 -0
- package/oclif.config.js +0 -1
- package/oclif.manifest.json +2134 -2055
- package/package.json +18 -18
- package/templates/app-quickstart/AGENTS.md +18 -0
- package/templates/app-sanity-ui/AGENTS.md +18 -0
- package/templates/shopify/.gitignore +26 -0
- package/dist/exports/invokeSanityCli/commandPolicies/policy.js +0 -46
- package/dist/exports/invokeSanityCli/commandPolicies/policy.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/actions/documents/validateDocuments.worker.ts"],"sourcesContent":["import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\nimport readline from 'node:readline'\nimport {Readable} from 'node:stream'\nimport {workerData as _workerData, isMainThread, parentPort} from 'node:worker_threads'\n\nimport {getStudioWorkspaces, getWorkspace, resolveLocalPackage} from '@sanity/cli-core'\nimport {type ClientConfig, createClient, type SanityDocument} from '@sanity/client'\nimport {type ValidationContext, type ValidationMarker} from '@sanity/types'\nimport {WorkerChannelReporter} from '@sanity/worker-channels'\nimport pMap from 'p-map'\n\nimport {\n type AvailabilityResponse,\n checkDocumentAvailability,\n exportDocuments,\n getDocumentCount,\n} from '../../services/documents.js'\nimport {extractDocumentsFromNdjsonOrTarball} from '../../util/extractDocumentsFromNdjsonOrTarball.js'\nimport {\n DOCUMENT_VALIDATION_TIMEOUT,\n getReferenceIds,\n isValidId,\n levelValues,\n MAX_VALIDATION_CONCURRENCY,\n REFERENCE_INTEGRITY_BATCH_SIZE,\n shouldIncludeDocument,\n} from '../../util/validation/validateDocumentsUtils.js'\nimport {type ValidateDocumentsWorkerData, type ValidationWorkerChannel} from './types.js'\n\nconst {\n clientConfig,\n dataset,\n level,\n maxCustomValidationConcurrency,\n maxFetchConcurrency,\n ndjsonFilePath,\n projectId,\n studioHost,\n workDir,\n workspace: workspaceName,\n} = _workerData as ValidateDocumentsWorkerData\n\nif (isMainThread || !parentPort) {\n throw new Error('This module must be run as a worker thread')\n}\n\nconst report = WorkerChannelReporter.from<ValidationWorkerChannel>(parentPort)\n\n// eslint-disable-next-line n/no-unsupported-features/node-builtins\nasync function* readerToGenerator(reader: ReadableStreamDefaultReader<Uint8Array>) {\n while (true) {\n const {done, value} = await reader.read()\n if (value) yield value\n if (done) return\n }\n}\n\nawait main()\nprocess.exit()\n\nasync function loadWorkspace() {\n const workspaces = await getStudioWorkspaces(workDir)\n const workspace = getWorkspace(workspaces, workspaceName)\n\n const resolvedDataset = dataset || workspace.dataset\n const resolvedProjectId = projectId || workspace.projectId\n\n const client = createClient({\n ...clientConfig,\n dataset: resolvedDataset,\n projectId: resolvedProjectId,\n requestTagPrefix: 'sanity.cli.validate',\n }).config({apiVersion: 'v2021-03-25'})\n\n report.event.loadedWorkspace({\n basePath: workspace.basePath,\n dataset: workspace.dataset,\n name: workspace.name,\n projectId: workspace.projectId,\n })\n\n return {client, dataset: resolvedDataset, projectId: resolvedProjectId, workspace}\n}\n\ninterface DownloadFromExportOptions {\n dataset: string\n projectId: string\n}\n\nasync function downloadFromExport({\n dataset: datasetName,\n projectId: projId,\n}: DownloadFromExportOptions) {\n const documentCount = await getDocumentCount({dataset: datasetName, projectId: projId})\n report.event.loadedDocumentCount({documentCount})\n\n const response = await exportDocuments({dataset: datasetName, projectId: projId})\n\n const reader = response.body?.getReader()\n if (!reader) throw new Error('Could not get reader from response body.')\n\n let downloadedCount = 0\n const referencedIds = new Set<string>()\n const documentIds = new Set<string>()\n const lines = readline.createInterface({input: Readable.from(readerToGenerator(reader))})\n\n // Note: we stream the export to a file and then re-read from that file to\n // make this less memory intensive.\n // this is a similar pattern to the import/export CLI commands\n const slugDate = new Date()\n .toISOString()\n .replaceAll(/[^a-z0-9]/gi, '-')\n .toLowerCase()\n const tempOutputFile = path.join(os.tmpdir(), `sanity-validate-${slugDate}.ndjson`)\n const outputStream = fs.createWriteStream(tempOutputFile)\n\n for await (const line of lines) {\n const document = JSON.parse(line) as SanityDocument\n\n if (shouldIncludeDocument(document)) {\n documentIds.add(document._id)\n for (const referenceId of getReferenceIds(document)) {\n referencedIds.add(referenceId)\n }\n\n outputStream.write(`${line}\\n`)\n }\n\n downloadedCount++\n report.stream.exportProgress.emit({documentCount, downloadedCount})\n }\n\n await new Promise<void>((resolve, reject) =>\n outputStream.close((err) => (err ? reject(err) : resolve())),\n )\n\n report.stream.exportProgress.end()\n report.event.exportFinished({totalDocumentsToValidate: documentIds.size})\n\n const getDocuments = () =>\n extractDocumentsFromNdjsonOrTarball(fs.createReadStream(tempOutputFile))\n\n return {cleanup: () => fs.promises.rm(tempOutputFile), documentIds, getDocuments, referencedIds}\n}\n\nasync function downloadFromFile(filePath: string) {\n const referencedIds = new Set<string>()\n const documentIds = new Set<string>()\n const getDocuments = () => extractDocumentsFromNdjsonOrTarball(fs.createReadStream(filePath))\n\n for await (const document of getDocuments()) {\n if (shouldIncludeDocument(document)) {\n documentIds.add(document._id)\n for (const referenceId of getReferenceIds(document)) {\n referencedIds.add(referenceId)\n }\n }\n }\n\n report.event.exportFinished({totalDocumentsToValidate: documentIds.size})\n\n return {cleanup: undefined, documentIds, getDocuments, referencedIds}\n}\n\ninterface CheckReferenceExistenceOptions {\n dataset: string\n documentIds: Set<string>\n projectId: string\n referencedIds: Set<string>\n}\n\nasync function checkReferenceExistence({\n dataset: datasetName,\n documentIds,\n projectId: projId,\n referencedIds: _referencedIds,\n}: CheckReferenceExistenceOptions) {\n const existingIds = new Set(documentIds)\n const idsToCheck = [..._referencedIds]\n .filter((id) => !existingIds.has(id) && isValidId(id))\n .toSorted()\n\n const batches: string[][] = []\n for (let i = 0; i < idsToCheck.length; i += REFERENCE_INTEGRITY_BATCH_SIZE) {\n batches.push(idsToCheck.slice(i, i + REFERENCE_INTEGRITY_BATCH_SIZE))\n }\n\n for (const batch of batches) {\n const {omitted} = await checkDocumentAvailability({\n dataset: datasetName,\n documentIds: batch,\n projectId: projId,\n })\n\n const omittedIds: Record<string, AvailabilityResponse['omitted'][number]['reason']> = {}\n for (const item of omitted) {\n omittedIds[item.id] = item.reason\n }\n\n for (const id of batch) {\n // unless the document ID is in the `omitted` object explictly due to\n // the reason `'existence'`, then it should exist\n if (omittedIds[id] !== 'existence') {\n existingIds.add(id)\n }\n }\n }\n report.event.loadedReferenceIntegrity()\n\n return {existingIds}\n}\n\nasync function main() {\n let cleanupDownloadedDocuments: (() => Promise<void>) | undefined\n\n try {\n const {client, dataset: datasetName, projectId: projId, workspace} = await loadWorkspace()\n const {cleanup, documentIds, getDocuments, referencedIds} = ndjsonFilePath\n ? await downloadFromFile(ndjsonFilePath)\n : await downloadFromExport({dataset: datasetName, projectId: projId})\n cleanupDownloadedDocuments = cleanup\n const {existingIds} = await checkReferenceExistence({\n dataset: datasetName,\n documentIds,\n projectId: projId,\n referencedIds,\n })\n\n const getClient = <TOptions extends Partial<ClientConfig>>(options: TOptions) =>\n client.withConfig(options)\n\n const getDocumentExists: ValidationContext['getDocumentExists'] = ({id}) =>\n Promise.resolve(existingIds.has(id))\n\n const getLevel = (markers: ValidationMarker[]) => {\n let foundWarning = false\n for (const marker of markers) {\n if (marker.level === 'error') return 'error'\n if (marker.level === 'warning') foundWarning = true\n }\n\n if (foundWarning) return 'warning'\n return 'info'\n }\n\n let validatedCount = 0\n\n const validate = async (document: SanityDocument) => {\n let markers: ValidationMarker[]\n\n try {\n const timeout = Symbol('timeout')\n\n const {validateDocument} = await resolveLocalPackage<typeof import('sanity')>(\n 'sanity',\n workDir,\n )\n\n const result = await Promise.race([\n validateDocument({\n document,\n environment: 'cli',\n // The local `sanity` package types its client against its own\n // @sanity/client major; the runtime surface validation uses is\n // compatible across v7/v8.\n getClient: getClient as unknown as Parameters<typeof validateDocument>[0]['getClient'],\n getDocumentExists,\n maxCustomValidationConcurrency,\n maxFetchConcurrency,\n workspace,\n }),\n new Promise<typeof timeout>((resolve) =>\n setTimeout(() => resolve(timeout), DOCUMENT_VALIDATION_TIMEOUT),\n ),\n ])\n\n if (result === timeout) {\n throw new Error(\n `Document '${document._id}' failed to validate within ${DOCUMENT_VALIDATION_TIMEOUT}ms.`,\n )\n }\n\n markers = result\n // filter out unwanted levels\n .filter((marker) => {\n const markerValue = levelValues[marker.level]\n const flagLevelValue =\n levelValues[level as keyof typeof levelValues] ?? levelValues.info\n return markerValue <= flagLevelValue\n })\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'Unknown error'\n\n const message = `Exception occurred while validating value: ${errorMessage}`\n\n markers = [\n {\n level: 'error',\n message,\n path: [],\n },\n ]\n }\n\n validatedCount++\n\n const intentUrl =\n studioHost &&\n `${studioHost}${path.resolve(\n workspace.basePath,\n `/intent/edit/id=${encodeURIComponent(document._id)};type=${encodeURIComponent(\n document._type,\n )}`,\n )}`\n\n report.stream.validation.emit({\n documentId: document._id,\n documentType: document._type,\n ...(intentUrl && {intentUrl}),\n level: getLevel(markers),\n markers,\n revision: document._rev,\n validatedCount,\n })\n }\n\n await pMap(getDocuments(), validate, {concurrency: MAX_VALIDATION_CONCURRENCY})\n\n report.stream.validation.end()\n } finally {\n await cleanupDownloadedDocuments?.()\n }\n}\n"],"names":["fs","os","path","readline","Readable","workerData","_workerData","isMainThread","parentPort","getStudioWorkspaces","getWorkspace","resolveLocalPackage","createClient","WorkerChannelReporter","pMap","checkDocumentAvailability","exportDocuments","getDocumentCount","extractDocumentsFromNdjsonOrTarball","DOCUMENT_VALIDATION_TIMEOUT","getReferenceIds","isValidId","levelValues","MAX_VALIDATION_CONCURRENCY","REFERENCE_INTEGRITY_BATCH_SIZE","shouldIncludeDocument","clientConfig","dataset","level","maxCustomValidationConcurrency","maxFetchConcurrency","ndjsonFilePath","projectId","studioHost","workDir","workspace","workspaceName","Error","report","from","readerToGenerator","reader","done","value","read","main","process","exit","loadWorkspace","workspaces","resolvedDataset","resolvedProjectId","client","requestTagPrefix","config","apiVersion","event","loadedWorkspace","basePath","name","downloadFromExport","datasetName","projId","documentCount","loadedDocumentCount","response","body","getReader","downloadedCount","referencedIds","Set","documentIds","lines","createInterface","input","slugDate","Date","toISOString","replaceAll","toLowerCase","tempOutputFile","join","tmpdir","outputStream","createWriteStream","line","document","JSON","parse","add","_id","referenceId","write","stream","exportProgress","emit","Promise","resolve","reject","close","err","end","exportFinished","totalDocumentsToValidate","size","getDocuments","createReadStream","cleanup","promises","rm","downloadFromFile","filePath","undefined","checkReferenceExistence","_referencedIds","existingIds","idsToCheck","filter","id","has","toSorted","batches","i","length","push","slice","batch","omitted","omittedIds","item","reason","loadedReferenceIntegrity","cleanupDownloadedDocuments","getClient","options","withConfig","getDocumentExists","getLevel","markers","foundWarning","marker","validatedCount","validate","timeout","Symbol","validateDocument","result","race","environment","setTimeout","markerValue","flagLevelValue","info","errorMessage","message","intentUrl","encodeURIComponent","_type","validation","documentId","documentType","revision","_rev","concurrency"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/documents/validateDocuments.worker.ts"],"sourcesContent":["import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\nimport readline from 'node:readline'\nimport {Readable} from 'node:stream'\nimport {workerData as _workerData, isMainThread, parentPort} from 'node:worker_threads'\n\nimport {getStudioWorkspaces, getWorkspace, resolveLocalPackage} from '@sanity/cli-core'\nimport {type ClientConfig, createClient, type SanityDocument} from '@sanity/client'\nimport {type ValidationContext, type ValidationMarker} from '@sanity/types'\nimport {WorkerChannelReporter} from '@sanity/worker-channels'\nimport pMap from 'p-map'\n\nimport {\n type AvailabilityResponse,\n checkDocumentAvailability,\n exportDocuments,\n getDocumentCount,\n} from '../../services/documents.js'\nimport {extractDocumentsFromNdjsonOrTarball} from '../../util/extractDocumentsFromNdjsonOrTarball.js'\nimport {\n DOCUMENT_VALIDATION_TIMEOUT,\n getReferenceIds,\n isValidId,\n levelValues,\n MAX_VALIDATION_CONCURRENCY,\n REFERENCE_INTEGRITY_BATCH_SIZE,\n shouldIncludeDocument,\n} from '../../util/validation/validateDocumentsUtils.js'\nimport {type ValidateDocumentsWorkerData, type ValidationWorkerChannel} from './types.js'\n\nconst {\n clientConfig,\n dataset,\n level,\n maxCustomValidationConcurrency,\n maxFetchConcurrency,\n ndjsonFilePath,\n projectId,\n studioHost,\n workDir,\n workspace: workspaceName,\n} = _workerData as ValidateDocumentsWorkerData\n\nif (isMainThread || !parentPort) {\n throw new Error('This module must be run as a worker thread')\n}\n\nconst report = WorkerChannelReporter.from<ValidationWorkerChannel>(parentPort)\n\n// eslint-disable-next-line n/no-unsupported-features/node-builtins\nasync function* readerToGenerator(reader: ReadableStreamDefaultReader<Uint8Array>) {\n while (true) {\n const {done, value} = await reader.read()\n if (value) yield value\n if (done) return\n }\n}\n\nawait main()\nprocess.exit()\n\nasync function loadWorkspace() {\n const workspaces = await getStudioWorkspaces(workDir)\n const workspace = getWorkspace(workspaces, workspaceName)\n\n const resolvedDataset = dataset || workspace.dataset\n const resolvedProjectId = projectId || workspace.projectId\n\n const client = createClient({\n ...clientConfig,\n dataset: resolvedDataset,\n projectId: resolvedProjectId,\n requestTagPrefix: 'sanity.cli.validate',\n }).config({apiVersion: 'v2021-03-25'})\n\n report.event.loadedWorkspace({\n basePath: workspace.basePath,\n dataset: workspace.dataset,\n name: workspace.name,\n projectId: workspace.projectId,\n })\n\n return {client, dataset: resolvedDataset, projectId: resolvedProjectId, workspace}\n}\n\ninterface DownloadFromExportOptions {\n dataset: string\n projectId: string\n}\n\nasync function downloadFromExport({\n dataset: datasetName,\n projectId: projId,\n}: DownloadFromExportOptions) {\n const documentCount = await getDocumentCount({dataset: datasetName, projectId: projId})\n report.event.loadedDocumentCount({documentCount})\n\n const response = await exportDocuments({dataset: datasetName, projectId: projId})\n\n const reader = response.body?.getReader()\n if (!reader) throw new Error('Could not get reader from response body.')\n\n let downloadedCount = 0\n const referencedIds = new Set<string>()\n const documentIds = new Set<string>()\n const lines = readline.createInterface({input: Readable.from(readerToGenerator(reader))})\n\n // Note: we stream the export to a file and then re-read from that file to\n // make this less memory intensive.\n // this is a similar pattern to the import/export CLI commands\n const slugDate = new Date()\n .toISOString()\n .replaceAll(/[^a-z0-9]/gi, '-')\n .toLowerCase()\n const tempOutputFile = path.join(os.tmpdir(), `sanity-validate-${slugDate}.ndjson`)\n const outputStream = fs.createWriteStream(tempOutputFile)\n\n for await (const line of lines) {\n const document = JSON.parse(line) as SanityDocument\n\n if (shouldIncludeDocument(document)) {\n documentIds.add(document._id)\n for (const referenceId of getReferenceIds(document)) {\n referencedIds.add(referenceId)\n }\n\n outputStream.write(`${line}\\n`)\n }\n\n downloadedCount++\n report.stream.exportProgress.emit({documentCount, downloadedCount})\n }\n\n await new Promise<void>((resolve, reject) =>\n outputStream.close((err) => (err ? reject(err) : resolve())),\n )\n\n report.stream.exportProgress.end()\n report.event.exportFinished({totalDocumentsToValidate: documentIds.size})\n\n const getDocuments = () =>\n extractDocumentsFromNdjsonOrTarball(fs.createReadStream(tempOutputFile))\n\n return {cleanup: () => fs.promises.rm(tempOutputFile), documentIds, getDocuments, referencedIds}\n}\n\nasync function downloadFromFile(filePath: string) {\n const referencedIds = new Set<string>()\n const documentIds = new Set<string>()\n const getDocuments = () => extractDocumentsFromNdjsonOrTarball(fs.createReadStream(filePath))\n\n for await (const document of getDocuments()) {\n if (shouldIncludeDocument(document)) {\n documentIds.add(document._id)\n for (const referenceId of getReferenceIds(document)) {\n referencedIds.add(referenceId)\n }\n }\n }\n\n report.event.exportFinished({totalDocumentsToValidate: documentIds.size})\n\n return {cleanup: undefined, documentIds, getDocuments, referencedIds}\n}\n\ninterface CheckReferenceExistenceOptions {\n dataset: string\n documentIds: Set<string>\n projectId: string\n referencedIds: Set<string>\n}\n\nasync function checkReferenceExistence({\n dataset: datasetName,\n documentIds,\n projectId: projId,\n referencedIds: _referencedIds,\n}: CheckReferenceExistenceOptions) {\n const existingIds = new Set(documentIds)\n const idsToCheck = [..._referencedIds]\n .filter((id) => !existingIds.has(id) && isValidId(id))\n .toSorted()\n\n const batches: string[][] = []\n for (let i = 0; i < idsToCheck.length; i += REFERENCE_INTEGRITY_BATCH_SIZE) {\n batches.push(idsToCheck.slice(i, i + REFERENCE_INTEGRITY_BATCH_SIZE))\n }\n\n for (const batch of batches) {\n const {omitted} = await checkDocumentAvailability({\n dataset: datasetName,\n documentIds: batch,\n projectId: projId,\n })\n\n const omittedIds: Record<string, AvailabilityResponse['omitted'][number]['reason']> = {}\n for (const item of omitted) {\n omittedIds[item.id] = item.reason\n }\n\n for (const id of batch) {\n // unless the document ID is in the `omitted` object explictly due to\n // the reason `'existence'`, then it should exist\n if (omittedIds[id] !== 'existence') {\n existingIds.add(id)\n }\n }\n }\n report.event.loadedReferenceIntegrity()\n\n return {existingIds}\n}\n\nasync function main() {\n let cleanupDownloadedDocuments: (() => Promise<void>) | undefined\n\n try {\n const {client, dataset: datasetName, projectId: projId, workspace} = await loadWorkspace()\n const {cleanup, documentIds, getDocuments, referencedIds} = ndjsonFilePath\n ? await downloadFromFile(ndjsonFilePath)\n : await downloadFromExport({dataset: datasetName, projectId: projId})\n cleanupDownloadedDocuments = cleanup\n const {existingIds} = await checkReferenceExistence({\n dataset: datasetName,\n documentIds,\n projectId: projId,\n referencedIds,\n })\n\n const getClient = <TOptions extends Partial<ClientConfig>>(options: TOptions) =>\n client.withConfig(options)\n\n const getDocumentExists: ValidationContext['getDocumentExists'] = ({id}) =>\n Promise.resolve(existingIds.has(id))\n\n const getLevel = (markers: ValidationMarker[]) => {\n let foundWarning = false\n for (const marker of markers) {\n if (marker.level === 'error') return 'error'\n if (marker.level === 'warning') foundWarning = true\n }\n\n if (foundWarning) return 'warning'\n return 'info'\n }\n\n let validatedCount = 0\n\n const validate = async (document: SanityDocument) => {\n let markers: ValidationMarker[]\n\n try {\n const timeout = Symbol('timeout')\n\n const {validateDocument} = await resolveLocalPackage<typeof import('sanity')>(\n 'sanity',\n workDir,\n )\n\n const result = await Promise.race([\n validateDocument({\n document,\n environment: 'cli',\n getClient,\n getDocumentExists,\n maxCustomValidationConcurrency,\n maxFetchConcurrency,\n workspace,\n }),\n new Promise<typeof timeout>((resolve) =>\n setTimeout(() => resolve(timeout), DOCUMENT_VALIDATION_TIMEOUT),\n ),\n ])\n\n if (result === timeout) {\n throw new Error(\n `Document '${document._id}' failed to validate within ${DOCUMENT_VALIDATION_TIMEOUT}ms.`,\n )\n }\n\n markers = result\n // filter out unwanted levels\n .filter((marker) => {\n const markerValue = levelValues[marker.level]\n const flagLevelValue =\n levelValues[level as keyof typeof levelValues] ?? levelValues.info\n return markerValue <= flagLevelValue\n })\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'Unknown error'\n\n const message = `Exception occurred while validating value: ${errorMessage}`\n\n markers = [\n {\n level: 'error',\n message,\n path: [],\n },\n ]\n }\n\n validatedCount++\n\n const intentUrl =\n studioHost &&\n `${studioHost}${path.resolve(\n workspace.basePath,\n `/intent/edit/id=${encodeURIComponent(document._id)};type=${encodeURIComponent(\n document._type,\n )}`,\n )}`\n\n report.stream.validation.emit({\n documentId: document._id,\n documentType: document._type,\n ...(intentUrl && {intentUrl}),\n level: getLevel(markers),\n markers,\n revision: document._rev,\n validatedCount,\n })\n }\n\n await pMap(getDocuments(), validate, {concurrency: MAX_VALIDATION_CONCURRENCY})\n\n report.stream.validation.end()\n } finally {\n await cleanupDownloadedDocuments?.()\n }\n}\n"],"names":["fs","os","path","readline","Readable","workerData","_workerData","isMainThread","parentPort","getStudioWorkspaces","getWorkspace","resolveLocalPackage","createClient","WorkerChannelReporter","pMap","checkDocumentAvailability","exportDocuments","getDocumentCount","extractDocumentsFromNdjsonOrTarball","DOCUMENT_VALIDATION_TIMEOUT","getReferenceIds","isValidId","levelValues","MAX_VALIDATION_CONCURRENCY","REFERENCE_INTEGRITY_BATCH_SIZE","shouldIncludeDocument","clientConfig","dataset","level","maxCustomValidationConcurrency","maxFetchConcurrency","ndjsonFilePath","projectId","studioHost","workDir","workspace","workspaceName","Error","report","from","readerToGenerator","reader","done","value","read","main","process","exit","loadWorkspace","workspaces","resolvedDataset","resolvedProjectId","client","requestTagPrefix","config","apiVersion","event","loadedWorkspace","basePath","name","downloadFromExport","datasetName","projId","documentCount","loadedDocumentCount","response","body","getReader","downloadedCount","referencedIds","Set","documentIds","lines","createInterface","input","slugDate","Date","toISOString","replaceAll","toLowerCase","tempOutputFile","join","tmpdir","outputStream","createWriteStream","line","document","JSON","parse","add","_id","referenceId","write","stream","exportProgress","emit","Promise","resolve","reject","close","err","end","exportFinished","totalDocumentsToValidate","size","getDocuments","createReadStream","cleanup","promises","rm","downloadFromFile","filePath","undefined","checkReferenceExistence","_referencedIds","existingIds","idsToCheck","filter","id","has","toSorted","batches","i","length","push","slice","batch","omitted","omittedIds","item","reason","loadedReferenceIntegrity","cleanupDownloadedDocuments","getClient","options","withConfig","getDocumentExists","getLevel","markers","foundWarning","marker","validatedCount","validate","timeout","Symbol","validateDocument","result","race","environment","setTimeout","markerValue","flagLevelValue","info","errorMessage","message","intentUrl","encodeURIComponent","_type","validation","documentId","documentType","revision","_rev","concurrency"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/actions/media/importMedia.ts"],"sourcesContent":["import {createHash} from 'node:crypto'\nimport {createReadStream} from 'node:fs'\nimport fs, {mkdtemp} from 'node:fs/promises'\nimport {tmpdir} from 'node:os'\nimport path from 'node:path'\nimport {text} from 'node:stream/consumers'\nimport {pipeline} from 'node:stream/promises'\nimport {styleText} from 'node:util'\n\nimport {spinner} from '@sanity/cli-core/ux'\nimport {type SanityClient} from '@sanity/client'\nimport {type FileAsset, type ImageAsset, type SanityDocument} from '@sanity/types'\nimport gunzipMaybe from 'gunzip-maybe'\n// @ts-expect-error `peek-stream` module currently untyped\nimport peek from 'peek-stream'\nimport {\n catchError,\n EMPTY,\n filter,\n from,\n map,\n mergeMap,\n mergeWith,\n type Observable,\n of,\n type OperatorFunction,\n switchMap,\n tap,\n zip,\n} from 'rxjs'\nimport {x as extractTar} from 'tar'\nimport {glob} from 'tinyglobby'\n\nimport {isTar} from '../../util/isTar.js'\nimport {buildNdjsonIndex} from './buildNdjsonIndex.js'\nimport {importMediaDebug} from './importMediaDebug.js'\n\nconst DEFAULT_CONCURRENCY = 6\n\ninterface MediaLibraryUploadResult {\n asset: SanityDocument & {\n _type: 'sanity.asset'\n aspects: unknown\n assetType: FileAsset['_type'] | ImageAsset['_type']\n }\n assetInstance: FileAsset | ImageAsset\n}\n\ninterface MediaLibraryUploadResponse {\n body: MediaLibraryUploadResult\n type: 'response'\n}\n\nexport interface State {\n /**\n * The last asset processed.\n */\n asset: AssetWithAspects\n /**\n * The count of input files.\n */\n fileCount: number\n}\n\ninterface ResolvedAsset {\n /**\n * The ids of the `sanity.asset` documents that currently refer to the asset.\n *\n * These documents contain aspects, and reference an asset instance document.\n */\n assetIds: string[]\n isExistingAsset: boolean\n /**\n * The original filename of the asset as it appears in the import source.\n *\n * Note: Currently includes `images/` or `files/` prefix.\n */\n originalFilename: string\n sha1Hash: string\n}\n\nexport type AssetWithAspects<Asset extends ResolvedAsset = ResolvedAsset> = Asset & {\n aspects: unknown | undefined\n}\n\ninterface Options {\n client: SanityClient\n replaceAspects: boolean\n sourcePath: string\n spinner: ReturnType<typeof spinner>\n}\n\ninterface Context extends Options {\n aspectsIndex: Map<string, unknown>\n workingPath: string\n}\n\nexport function importer(options: Options): Observable<State> {\n return resolveSource(options).pipe(\n mergeMap(({aspectsNdjsonPath, files, images, workingPath}) => {\n const fileCount = files.length + images.length\n\n if (fileCount === 0) {\n throw new Error('No assets to import')\n }\n\n // Stream the ndjson file once and build an index to avoid creating\n // multiple read streams (which causes file descriptor leaks)\n const aspectsIndexPromise = buildNdjsonIndex(\n createReadStream(aspectsNdjsonPath),\n 'filename',\n 'aspects',\n )\n\n return from(aspectsIndexPromise).pipe(\n mergeMap((aspectsIndex) => {\n const context: Context = {\n ...options,\n aspectsIndex,\n workingPath,\n }\n\n return from(files).pipe(\n switchMap((file) => zip(of<'file'>('file'), of(file))),\n mergeWith(from(images).pipe(switchMap((file) => zip(of<'image'>('image'), of(file))))),\n fetchExistingAssets(context),\n uploadAsset(context),\n resolveAspectData(context),\n setAspects(context),\n map((asset) => ({\n asset,\n fileCount,\n })),\n )\n }),\n )\n }),\n )\n}\n\n/**\n * @internal\n */\nexport function resolveSource({sourcePath}: Pick<Context, 'sourcePath'>): Observable<{\n aspectsNdjsonPath: string\n files: string[]\n images: string[]\n workingPath: string\n}> {\n return from(fs.stat(sourcePath)).pipe(\n switchMap((stats) => {\n return stats.isDirectory()\n ? of(sourcePath)\n : from(mkdtemp(path.join(tmpdir(), 'sanity-media-library-import'))).pipe(\n switchMap((tempPath) => {\n return from(\n pipeline(createReadStream(sourcePath), gunzipMaybe(), untarMaybe(tempPath)),\n ).pipe(map(() => tempPath))\n }),\n )\n }),\n switchMap((importSourcePath) => {\n return from(\n glob(['**/data.ndjson'], {\n absolute: true,\n cwd: importSourcePath,\n deep: 2,\n }),\n ).pipe(\n map(([aspectsNdjsonPath]) => ({\n aspectsNdjsonPath,\n importSourcePath,\n workingPath:\n aspectsNdjsonPath === undefined ? importSourcePath : path.dirname(aspectsNdjsonPath),\n })),\n )\n }),\n tap(({aspectsNdjsonPath, importSourcePath}) => {\n if (aspectsNdjsonPath === undefined) {\n throw new Error(\n `No ${styleText('bold', 'data.ndjson')} file found in import source ${styleText('bold', importSourcePath)}`,\n )\n }\n importMediaDebug(`[Found NDJSON file] ${aspectsNdjsonPath}`)\n }),\n switchMap(({aspectsNdjsonPath, workingPath}) => {\n return from(\n Promise.all([\n glob(['files/*'], {\n cwd: workingPath,\n }),\n glob(['images/*'], {\n cwd: workingPath,\n }),\n ]),\n ).pipe(\n map(([files, images]) => ({\n aspectsNdjsonPath,\n files,\n images,\n workingPath,\n })),\n )\n }),\n )\n}\n\n/**\n * Untar the stream if its contents appear to be tarred.\n *\n * @internal\n */\nfunction untarMaybe(outputPath: string) {\n // @ts-expect-error `peek-stream` module currently untyped\n return peek({maxBuffer: 300, newline: false}, (data, swap) => {\n if (isTar(data)) {\n return swap(null, extractTar({cwd: outputPath}))\n }\n\n return swap(null)\n })\n}\n\n/**\n * Fetch the ids of all asset documents that reference the input asset.\n * The input asset is identified by its SHA-1 hash.\n *\n * @internal\n */\nfunction fetchAssetsByHash({\n client,\n type,\n}: {\n client: SanityClient\n type: 'file' | 'image'\n}): OperatorFunction<string, [hash: string, assetIds: string[]]> {\n return switchMap((hash) =>\n client.observable\n .fetch<string[]>(\n `*[\n _type == \"sanity.asset\" &&\n currentVersion._ref == *[\n _type == $type &&\n sha1hash == $hash\n ][0]._id\n ]._id`,\n {\n hash,\n type: ['sanity', `${type}Asset`].join('.'),\n },\n {\n tag: 'asset.getId',\n },\n )\n .pipe(switchMap((assetIds) => zip(of(hash), of(assetIds)))),\n )\n}\n\nfunction fetchExistingAssets({\n client,\n workingPath,\n}: Context): OperatorFunction<\n [type: 'file' | 'image', asset: string],\n [type: 'file' | 'image', asset: string, hash: string] | ResolvedAsset\n> {\n return mergeMap(([type, asset]) => {\n const createSha1Hash = createHash('sha1')\n\n const sha1hash = text(\n createReadStream(path.join(workingPath, asset)).pipe(createSha1Hash).setEncoding('hex'),\n )\n\n return from(sha1hash).pipe(\n tap((hash) =>\n importMediaDebug(`[Asset ${asset}] Checking for ${type} asset with hash ${hash}`),\n ),\n fetchAssetsByHash({client, type}),\n map<\n [string, string[]],\n [type: 'file' | 'image', asset: string, hash: string] | ResolvedAsset\n >(([hash, assetIds]) => {\n if (assetIds.length === 0) {\n return [type, asset, hash]\n }\n\n return {\n assetIds,\n isExistingAsset: true,\n originalFilename: asset,\n sha1Hash: hash,\n }\n }),\n )\n })\n}\n\n/**\n * Find the first matching entry in the cached aspect data and attach it to the asset object.\n *\n * @internal\n */\nfunction resolveAspectData({\n aspectsIndex,\n}: Context): OperatorFunction<ResolvedAsset, AssetWithAspects> {\n return map((resolvedAsset) => ({\n ...resolvedAsset,\n aspects: aspectsIndex.get(resolvedAsset.originalFilename),\n }))\n}\n\n// TODO: Batch mutations to reduce HTTP request count.\nexport function setAspects({\n client,\n replaceAspects,\n}: Pick<Context, 'client' | 'replaceAspects'>): OperatorFunction<\n AssetWithAspects,\n AssetWithAspects\n> {\n return mergeMap((asset) => {\n const {aspects, assetIds, isExistingAsset} = asset\n\n if (isExistingAsset && !replaceAspects) {\n importMediaDebug(`[Asset ${asset.originalFilename}] Skipping replacement of existing aspects`)\n return of(asset)\n }\n\n if (aspects === undefined) {\n importMediaDebug(`[Asset ${asset.originalFilename}] No aspects to import`)\n return of(asset)\n }\n\n const transaction = client.observable.transaction()\n for (const assetId of assetIds) {\n transaction.patch(assetId, {set: {aspects}})\n }\n\n importMediaDebug(\n `[Asset ${asset.originalFilename}] Setting aspects on asset documents ${JSON.stringify(assetIds)}`,\n )\n\n return transaction\n .commit({\n tag: 'asset.setAspects',\n visibility: 'async',\n })\n .pipe(map(() => asset))\n }, DEFAULT_CONCURRENCY)\n}\n\nfunction uploadAsset({\n client,\n workingPath,\n}: Context): OperatorFunction<\n [type: 'file' | 'image', asset: string, hash: string] | ResolvedAsset,\n ResolvedAsset\n> {\n return mergeMap((maybeResolvedAsset) => {\n if ('assetIds' in maybeResolvedAsset) {\n importMediaDebug(\n `[Asset ${maybeResolvedAsset.originalFilename}] Skipping upload of existing asset with hash ${maybeResolvedAsset.sha1Hash}`,\n )\n return of(maybeResolvedAsset)\n }\n\n const [type, asset, hash] = maybeResolvedAsset\n importMediaDebug(`[Asset ${asset}] Uploading new asset`)\n\n return client.observable.assets\n .upload(type, createReadStream(path.join(workingPath, asset)), {\n tag: 'asset.upload',\n })\n .pipe(\n catchError((error) => {\n // An asset matching the hash was not found during previous steps, but appears to exist upon upload.\n //\n // This may occur if:\n // - The asset was uploaded by another client since the check was performed.\n // - The asset instance document exists, but is not referenced by any asset document.\n if (error.statusCode === 409) {\n importMediaDebug(\n `[Asset ${asset}] Cannot overwrite existing ${type} asset with hash ${hash}`,\n )\n return EMPTY\n }\n return EMPTY\n }),\n filter((response) => response.type === 'response'),\n tap(() => importMediaDebug(`[Asset ${asset}] Finished uploading new asset`)),\n // TODO: The `client.assets.upload` method should return `MediaLibraryUploadResponse` when operating on Media Library resources. When that occurs, this type assertion can be removed.\n map((response) => (response as unknown as MediaLibraryUploadResponse).body),\n map<MediaLibraryUploadResult, ResolvedAsset>((result) => ({\n assetIds: [result.asset._id],\n isExistingAsset: false,\n originalFilename: asset,\n sha1Hash: hash,\n })),\n )\n }, DEFAULT_CONCURRENCY)\n}\n"],"names":["createHash","createReadStream","fs","mkdtemp","tmpdir","path","text","pipeline","styleText","gunzipMaybe","peek","catchError","EMPTY","filter","from","map","mergeMap","mergeWith","of","switchMap","tap","zip","x","extractTar","glob","isTar","buildNdjsonIndex","importMediaDebug","DEFAULT_CONCURRENCY","importer","options","resolveSource","pipe","aspectsNdjsonPath","files","images","workingPath","fileCount","length","Error","aspectsIndexPromise","aspectsIndex","context","file","fetchExistingAssets","uploadAsset","resolveAspectData","setAspects","asset","sourcePath","stat","stats","isDirectory","join","tempPath","untarMaybe","importSourcePath","absolute","cwd","deep","undefined","dirname","Promise","all","outputPath","maxBuffer","newline","data","swap","fetchAssetsByHash","client","type","hash","observable","fetch","tag","assetIds","createSha1Hash","sha1hash","setEncoding","isExistingAsset","originalFilename","sha1Hash","resolvedAsset","aspects","get","replaceAspects","transaction","assetId","patch","set","JSON","stringify","commit","visibility","maybeResolvedAsset","assets","upload","error","statusCode","response","body","result","_id"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/media/importMedia.ts"],"sourcesContent":["import {createHash} from 'node:crypto'\nimport {createReadStream} from 'node:fs'\nimport fs, {mkdtemp} from 'node:fs/promises'\nimport {tmpdir} from 'node:os'\nimport path from 'node:path'\nimport {text} from 'node:stream/consumers'\nimport {pipeline} from 'node:stream/promises'\nimport {styleText} from 'node:util'\n\nimport {spinner} from '@sanity/cli-core/ux'\nimport {type SanityClient} from '@sanity/client'\nimport gunzipMaybe from 'gunzip-maybe'\n// @ts-expect-error `peek-stream` module currently untyped\nimport peek from 'peek-stream'\nimport {\n catchError,\n EMPTY,\n filter,\n from,\n map,\n mergeMap,\n mergeWith,\n type Observable,\n of,\n type OperatorFunction,\n switchMap,\n tap,\n zip,\n} from 'rxjs'\nimport {x as extractTar} from 'tar'\nimport {glob} from 'tinyglobby'\n\nimport {type MediaLibraryAsset} from '../../services/mediaLibraries.js'\nimport {isTar} from '../../util/isTar.js'\nimport {buildNdjsonIndex} from './buildNdjsonIndex.js'\nimport {importMediaDebug} from './importMediaDebug.js'\n\nconst DEFAULT_CONCURRENCY = 6\n\ninterface MediaLibraryUploadResponse {\n body: MediaLibraryAsset\n type: 'response'\n}\n\nexport interface State {\n /**\n * The last asset processed.\n */\n asset: AssetWithAspects\n /**\n * The count of input files.\n */\n fileCount: number\n}\n\ninterface ResolvedAsset {\n /**\n * The ids of the `sanity.asset` documents that currently refer to the asset.\n *\n * These documents contain aspects, and reference an asset instance document.\n */\n assetIds: string[]\n isExistingAsset: boolean\n /**\n * The original filename of the asset as it appears in the import source.\n *\n * Note: Currently includes `images/` or `files/` prefix.\n */\n originalFilename: string\n sha1Hash: string\n}\n\nexport type AssetWithAspects<Asset extends ResolvedAsset = ResolvedAsset> = Asset & {\n aspects: unknown | undefined\n}\n\ninterface Options {\n client: SanityClient\n replaceAspects: boolean\n sourcePath: string\n spinner: ReturnType<typeof spinner>\n}\n\ninterface Context extends Options {\n aspectsIndex: Map<string, unknown>\n workingPath: string\n}\n\nexport function importer(options: Options): Observable<State> {\n return resolveSource(options).pipe(\n mergeMap(({aspectsNdjsonPath, files, images, workingPath}) => {\n const fileCount = files.length + images.length\n\n if (fileCount === 0) {\n throw new Error('No assets to import')\n }\n\n // Stream the ndjson file once and build an index to avoid creating\n // multiple read streams (which causes file descriptor leaks)\n const aspectsIndexPromise = buildNdjsonIndex(\n createReadStream(aspectsNdjsonPath),\n 'filename',\n 'aspects',\n )\n\n return from(aspectsIndexPromise).pipe(\n mergeMap((aspectsIndex) => {\n const context: Context = {\n ...options,\n aspectsIndex,\n workingPath,\n }\n\n return from(files).pipe(\n switchMap((file) => zip(of<'file'>('file'), of(file))),\n mergeWith(from(images).pipe(switchMap((file) => zip(of<'image'>('image'), of(file))))),\n fetchExistingAssets(context),\n uploadAsset(context),\n resolveAspectData(context),\n setAspects(context),\n map((asset) => ({\n asset,\n fileCount,\n })),\n )\n }),\n )\n }),\n )\n}\n\n/**\n * @internal\n */\nexport function resolveSource({sourcePath}: Pick<Context, 'sourcePath'>): Observable<{\n aspectsNdjsonPath: string\n files: string[]\n images: string[]\n workingPath: string\n}> {\n return from(fs.stat(sourcePath)).pipe(\n switchMap((stats) => {\n return stats.isDirectory()\n ? of(sourcePath)\n : from(mkdtemp(path.join(tmpdir(), 'sanity-media-library-import'))).pipe(\n switchMap((tempPath) => {\n return from(\n pipeline(createReadStream(sourcePath), gunzipMaybe(), untarMaybe(tempPath)),\n ).pipe(map(() => tempPath))\n }),\n )\n }),\n switchMap((importSourcePath) => {\n return from(\n glob(['**/data.ndjson'], {\n absolute: true,\n cwd: importSourcePath,\n deep: 2,\n }),\n ).pipe(\n map(([aspectsNdjsonPath]) => ({\n aspectsNdjsonPath,\n importSourcePath,\n workingPath:\n aspectsNdjsonPath === undefined ? importSourcePath : path.dirname(aspectsNdjsonPath),\n })),\n )\n }),\n tap(({aspectsNdjsonPath, importSourcePath}) => {\n if (aspectsNdjsonPath === undefined) {\n throw new Error(\n `No ${styleText('bold', 'data.ndjson')} file found in import source ${styleText('bold', importSourcePath)}`,\n )\n }\n importMediaDebug(`[Found NDJSON file] ${aspectsNdjsonPath}`)\n }),\n switchMap(({aspectsNdjsonPath, workingPath}) => {\n return from(\n Promise.all([\n glob(['files/*'], {\n cwd: workingPath,\n }),\n glob(['images/*'], {\n cwd: workingPath,\n }),\n ]),\n ).pipe(\n map(([files, images]) => ({\n aspectsNdjsonPath,\n files,\n images,\n workingPath,\n })),\n )\n }),\n )\n}\n\n/**\n * Untar the stream if its contents appear to be tarred.\n *\n * @internal\n */\nfunction untarMaybe(outputPath: string) {\n // @ts-expect-error `peek-stream` module currently untyped\n return peek({maxBuffer: 300, newline: false}, (data, swap) => {\n if (isTar(data)) {\n return swap(null, extractTar({cwd: outputPath}))\n }\n\n return swap(null)\n })\n}\n\n/**\n * Fetch the ids of all asset documents that reference the input asset.\n * The input asset is identified by its SHA-1 hash.\n *\n * @internal\n */\nfunction fetchAssetsByHash({\n client,\n type,\n}: {\n client: SanityClient\n type: 'file' | 'image'\n}): OperatorFunction<string, [hash: string, assetIds: string[]]> {\n return switchMap((hash) =>\n client.observable\n .fetch<string[]>(\n `*[\n _type == \"sanity.asset\" &&\n currentVersion._ref == *[\n _type == $type &&\n sha1hash == $hash\n ][0]._id\n ]._id`,\n {\n hash,\n type: ['sanity', `${type}Asset`].join('.'),\n },\n {\n tag: 'asset.getId',\n },\n )\n .pipe(switchMap((assetIds) => zip(of(hash), of(assetIds)))),\n )\n}\n\nfunction fetchExistingAssets({\n client,\n workingPath,\n}: Context): OperatorFunction<\n [type: 'file' | 'image', asset: string],\n [type: 'file' | 'image', asset: string, hash: string] | ResolvedAsset\n> {\n return mergeMap(([type, asset]) => {\n const createSha1Hash = createHash('sha1')\n\n const sha1hash = text(\n createReadStream(path.join(workingPath, asset)).pipe(createSha1Hash).setEncoding('hex'),\n )\n\n return from(sha1hash).pipe(\n tap((hash) =>\n importMediaDebug(`[Asset ${asset}] Checking for ${type} asset with hash ${hash}`),\n ),\n fetchAssetsByHash({client, type}),\n map<\n [string, string[]],\n [type: 'file' | 'image', asset: string, hash: string] | ResolvedAsset\n >(([hash, assetIds]) => {\n if (assetIds.length === 0) {\n return [type, asset, hash]\n }\n\n return {\n assetIds,\n isExistingAsset: true,\n originalFilename: asset,\n sha1Hash: hash,\n }\n }),\n )\n })\n}\n\n/**\n * Find the first matching entry in the cached aspect data and attach it to the asset object.\n *\n * @internal\n */\nfunction resolveAspectData({\n aspectsIndex,\n}: Context): OperatorFunction<ResolvedAsset, AssetWithAspects> {\n return map((resolvedAsset) => ({\n ...resolvedAsset,\n aspects: aspectsIndex.get(resolvedAsset.originalFilename),\n }))\n}\n\n// TODO: Batch mutations to reduce HTTP request count.\nexport function setAspects({\n client,\n replaceAspects,\n}: Pick<Context, 'client' | 'replaceAspects'>): OperatorFunction<\n AssetWithAspects,\n AssetWithAspects\n> {\n return mergeMap((asset) => {\n const {aspects, assetIds, isExistingAsset} = asset\n\n if (isExistingAsset && !replaceAspects) {\n importMediaDebug(`[Asset ${asset.originalFilename}] Skipping replacement of existing aspects`)\n return of(asset)\n }\n\n if (aspects === undefined) {\n importMediaDebug(`[Asset ${asset.originalFilename}] No aspects to import`)\n return of(asset)\n }\n\n const transaction = client.observable.transaction()\n for (const assetId of assetIds) {\n transaction.patch(assetId, {set: {aspects}})\n }\n\n importMediaDebug(\n `[Asset ${asset.originalFilename}] Setting aspects on asset documents ${JSON.stringify(assetIds)}`,\n )\n\n return transaction\n .commit({\n tag: 'asset.setAspects',\n visibility: 'async',\n })\n .pipe(map(() => asset))\n }, DEFAULT_CONCURRENCY)\n}\n\nfunction uploadAsset({\n client,\n workingPath,\n}: Context): OperatorFunction<\n [type: 'file' | 'image', asset: string, hash: string] | ResolvedAsset,\n ResolvedAsset\n> {\n return mergeMap((maybeResolvedAsset) => {\n if ('assetIds' in maybeResolvedAsset) {\n importMediaDebug(\n `[Asset ${maybeResolvedAsset.originalFilename}] Skipping upload of existing asset with hash ${maybeResolvedAsset.sha1Hash}`,\n )\n return of(maybeResolvedAsset)\n }\n\n const [type, asset, hash] = maybeResolvedAsset\n importMediaDebug(`[Asset ${asset}] Uploading new asset`)\n\n return client.observable.assets\n .upload(type, createReadStream(path.join(workingPath, asset)), {\n tag: 'asset.upload',\n })\n .pipe(\n catchError((error) => {\n // An asset matching the hash was not found during previous steps, but appears to exist upon upload.\n //\n // This may occur if:\n // - The asset was uploaded by another client since the check was performed.\n // - The asset instance document exists, but is not referenced by any asset document.\n if (error.statusCode === 409) {\n importMediaDebug(\n `[Asset ${asset}] Cannot overwrite existing ${type} asset with hash ${hash}`,\n )\n return EMPTY\n }\n return EMPTY\n }),\n filter((response) => response.type === 'response'),\n tap(() => importMediaDebug(`[Asset ${asset}] Finished uploading new asset`)),\n // TODO: The `client.assets.upload` method should return `MediaLibraryUploadResponse` when operating on Media Library resources. When that occurs, this type assertion can be removed.\n map((response) => (response as unknown as MediaLibraryUploadResponse).body),\n map<MediaLibraryAsset, ResolvedAsset>((result) => ({\n assetIds: [result.asset._id],\n isExistingAsset: false,\n originalFilename: asset,\n sha1Hash: hash,\n })),\n )\n }, DEFAULT_CONCURRENCY)\n}\n"],"names":["createHash","createReadStream","fs","mkdtemp","tmpdir","path","text","pipeline","styleText","gunzipMaybe","peek","catchError","EMPTY","filter","from","map","mergeMap","mergeWith","of","switchMap","tap","zip","x","extractTar","glob","isTar","buildNdjsonIndex","importMediaDebug","DEFAULT_CONCURRENCY","importer","options","resolveSource","pipe","aspectsNdjsonPath","files","images","workingPath","fileCount","length","Error","aspectsIndexPromise","aspectsIndex","context","file","fetchExistingAssets","uploadAsset","resolveAspectData","setAspects","asset","sourcePath","stat","stats","isDirectory","join","tempPath","untarMaybe","importSourcePath","absolute","cwd","deep","undefined","dirname","Promise","all","outputPath","maxBuffer","newline","data","swap","fetchAssetsByHash","client","type","hash","observable","fetch","tag","assetIds","createSha1Hash","sha1hash","setEncoding","isExistingAsset","originalFilename","sha1Hash","resolvedAsset","aspects","get","replaceAspects","transaction","assetId","patch","set","JSON","stringify","commit","visibility","maybeResolvedAsset","assets","upload","error","statusCode","response","body","result","_id"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ingestMediaLibraryAssetFromUrl } from '../../services/mediaLibraries.js';
|
|
2
|
+
import { withUrlIngestProgress } from '../assets/withUrlIngestProgress.js';
|
|
3
|
+
/**
|
|
4
|
+
* Ingest a media library asset from a URL, reporting the wait.
|
|
5
|
+
*/ export async function ingestMediaAssetFromUrlWithProgress(options) {
|
|
6
|
+
const { isInteractive, logToStderr, ...ingestOptions } = options;
|
|
7
|
+
return withUrlIngestProgress({
|
|
8
|
+
ingest: (signal)=>ingestMediaLibraryAssetFromUrl({
|
|
9
|
+
...ingestOptions,
|
|
10
|
+
signal
|
|
11
|
+
}),
|
|
12
|
+
isInteractive,
|
|
13
|
+
logToStderr,
|
|
14
|
+
successMessage: (result)=>`Imported asset: ${result.asset._id}`,
|
|
15
|
+
waitMessage: 'Fetching asset from URL. Sanity downloads the asset, which may take several minutes.'
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=ingestMediaAssetFromUrlWithProgress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/media/ingestMediaAssetFromUrlWithProgress.ts"],"sourcesContent":["import {\n ingestMediaLibraryAssetFromUrl,\n type MediaLibraryAsset,\n} from '../../services/mediaLibraries.js'\nimport {withUrlIngestProgress} from '../assets/withUrlIngestProgress.js'\n\ninterface IngestMediaAssetFromUrlWithProgressOptions {\n isInteractive: boolean\n logToStderr: (message: string) => void\n mediaLibraryId: string\n url: string\n\n aspects?: Record<string, unknown>\n filename?: string\n}\n\n/**\n * Ingest a media library asset from a URL, reporting the wait.\n */\nexport async function ingestMediaAssetFromUrlWithProgress(\n options: IngestMediaAssetFromUrlWithProgressOptions,\n): Promise<MediaLibraryAsset> {\n const {isInteractive, logToStderr, ...ingestOptions} = options\n\n return withUrlIngestProgress({\n ingest: (signal) => ingestMediaLibraryAssetFromUrl({...ingestOptions, signal}),\n isInteractive,\n logToStderr,\n successMessage: (result) => `Imported asset: ${result.asset._id}`,\n waitMessage:\n 'Fetching asset from URL. Sanity downloads the asset, which may take several minutes.',\n })\n}\n"],"names":["ingestMediaLibraryAssetFromUrl","withUrlIngestProgress","ingestMediaAssetFromUrlWithProgress","options","isInteractive","logToStderr","ingestOptions","ingest","signal","successMessage","result","asset","_id","waitMessage"],"mappings":"AAAA,SACEA,8BAA8B,QAEzB,mCAAkC;AACzC,SAAQC,qBAAqB,QAAO,qCAAoC;AAYxE;;CAEC,GACD,OAAO,eAAeC,oCACpBC,OAAmD;IAEnD,MAAM,EAACC,aAAa,EAAEC,WAAW,EAAE,GAAGC,eAAc,GAAGH;IAEvD,OAAOF,sBAAsB;QAC3BM,QAAQ,CAACC,SAAWR,+BAA+B;gBAAC,GAAGM,aAAa;gBAAEE;YAAM;QAC5EJ;QACAC;QACAI,gBAAgB,CAACC,SAAW,CAAC,gBAAgB,EAAEA,OAAOC,KAAK,CAACC,GAAG,EAAE;QACjEC,aACE;IACJ;AACF"}
|
|
@@ -1,46 +1,15 @@
|
|
|
1
1
|
import { basename, resolve } from 'node:path';
|
|
2
2
|
import { Flags } from '@oclif/core';
|
|
3
3
|
import { exitCodes, SanityCommand, subdebug } from '@sanity/cli-core';
|
|
4
|
-
import { getErrorMessage } from '@sanity/cli-core/errors';
|
|
5
|
-
import { isHttpError } from '@sanity/client';
|
|
6
4
|
import { AssetFileError } from '../../actions/assets/assetFileError.js';
|
|
5
|
+
import { ingestAssetFromUrlWithProgress } from '../../actions/assets/ingestAssetFromUrlWithProgress.js';
|
|
7
6
|
import { uploadAssetWithProgress } from '../../actions/assets/uploadAssetWithProgress.js';
|
|
8
7
|
import { promptForProject } from '../../prompts/promptForProject.js';
|
|
8
|
+
import { getAssetFilenameError, getIngestUrlError } from '../../util/assetSourceValidation.js';
|
|
9
|
+
import { getAssetUploadErrorMessage } from '../../util/assetUploadErrors.js';
|
|
9
10
|
import { getDatasetFlag, getProjectIdFlag } from '../../util/sharedFlags.js';
|
|
10
11
|
import { defineCommandTelemetry } from '../../util/telemetry/commandTelemetry.js';
|
|
11
12
|
const uploadAssetDebug = subdebug('assets:upload');
|
|
12
|
-
const DATASET_ASSET_LIMITS_URL = 'https://www.sanity.io/docs/content-lake/technical-limits#k2c53dc30e24b';
|
|
13
|
-
function isProjectUserNotFoundError(body) {
|
|
14
|
-
const responseError = body.error;
|
|
15
|
-
return typeof responseError === 'object' && responseError !== null && 'type' in responseError && responseError.type === 'projectUserNotFoundError';
|
|
16
|
-
}
|
|
17
|
-
function getAssetUploadErrorMessage(error) {
|
|
18
|
-
if (!isHttpError(error)) {
|
|
19
|
-
return `Asset upload failed: ${getErrorMessage(error)}`;
|
|
20
|
-
}
|
|
21
|
-
const body = typeof error.response.body === 'object' && error.response.body !== null && !Array.isArray(error.response.body) ? error.response.body : {};
|
|
22
|
-
const responseError = typeof body.error === 'string' ? body.error : error.response.statusMessage || 'HTTP error';
|
|
23
|
-
const statusCode = typeof body.statusCode === 'number' || typeof body.statusCode === 'string' ? body.statusCode : error.statusCode;
|
|
24
|
-
const projectUserNotFound = isProjectUserNotFoundError(body);
|
|
25
|
-
const responseMessage = projectUserNotFound ? error.message : getErrorMessage(error);
|
|
26
|
-
const message = /[.!?]$/.test(responseMessage) ? responseMessage : `${responseMessage}.`;
|
|
27
|
-
const details = typeof body.details === 'string' ? `\n\nDetails:\n${body.details}` : '';
|
|
28
|
-
const response = `Asset upload failed: HTTP ${statusCode} - ${responseError}\n${message}${details}`;
|
|
29
|
-
if (error.statusCode === 401 && !projectUserNotFound) {
|
|
30
|
-
return `${response}\n\nRun \`sanity login\` to authenticate, then try again.`;
|
|
31
|
-
}
|
|
32
|
-
if (error.statusCode === 403) {
|
|
33
|
-
return `${response}\n\nCheck that your account has write access to this dataset, then try again.`;
|
|
34
|
-
}
|
|
35
|
-
if ([
|
|
36
|
-
400,
|
|
37
|
-
413,
|
|
38
|
-
422
|
|
39
|
-
].includes(error.statusCode)) {
|
|
40
|
-
return `${response}\n\nCheck the asset requirements and current technical limits, then try again: ${DATASET_ASSET_LIMITS_URL}`;
|
|
41
|
-
}
|
|
42
|
-
return `${response}\n\nTry again.`;
|
|
43
|
-
}
|
|
44
13
|
const flags = {
|
|
45
14
|
...getProjectIdFlag({
|
|
46
15
|
description: 'Project ID to upload the asset to',
|
|
@@ -51,18 +20,29 @@ const flags = {
|
|
|
51
20
|
semantics: 'override'
|
|
52
21
|
}),
|
|
53
22
|
'content-type': Flags.string({
|
|
54
|
-
description: 'MIME type of the asset, such as image/png or application/pdf',
|
|
23
|
+
description: 'MIME type of the asset, such as image/png or application/pdf. Only applies to --file',
|
|
55
24
|
helpValue: '<mime-type>'
|
|
56
25
|
}),
|
|
57
26
|
file: Flags.string({
|
|
58
27
|
description: 'Path to the local file to upload',
|
|
59
|
-
|
|
60
|
-
|
|
28
|
+
exactlyOne: [
|
|
29
|
+
'file',
|
|
30
|
+
'from-url'
|
|
31
|
+
],
|
|
32
|
+
helpValue: '<path>'
|
|
61
33
|
}),
|
|
62
34
|
filename: Flags.string({
|
|
63
|
-
description: 'Original filename stored on the asset document. Defaults to the local filename',
|
|
35
|
+
description: 'Original filename stored on the asset document. Defaults to the local filename when using --file',
|
|
64
36
|
helpValue: '<filename>'
|
|
65
37
|
}),
|
|
38
|
+
'from-url': Flags.string({
|
|
39
|
+
description: 'https URL for Sanity to fetch the asset from, instead of uploading a local file. Must be reachable from the public internet without authentication',
|
|
40
|
+
exactlyOne: [
|
|
41
|
+
'file',
|
|
42
|
+
'from-url'
|
|
43
|
+
],
|
|
44
|
+
helpValue: '<url>'
|
|
45
|
+
}),
|
|
66
46
|
type: Flags.custom({
|
|
67
47
|
default: 'image',
|
|
68
48
|
description: 'Asset type to create',
|
|
@@ -73,7 +53,7 @@ const flags = {
|
|
|
73
53
|
})()
|
|
74
54
|
};
|
|
75
55
|
export class UploadAssetCommand extends SanityCommand {
|
|
76
|
-
static description = 'Upload one
|
|
56
|
+
static description = 'Upload one image or file to a Sanity dataset, from a local path or a URL, and print the asset document as JSON';
|
|
77
57
|
static examples = [
|
|
78
58
|
{
|
|
79
59
|
command: '<%= config.bin %> <%= command.id %> --file ./hero.png --type image --dataset production',
|
|
@@ -82,6 +62,10 @@ export class UploadAssetCommand extends SanityCommand {
|
|
|
82
62
|
{
|
|
83
63
|
command: '<%= config.bin %> <%= command.id %> --file ./brief.pdf --type file --content-type application/pdf --project-id abc123 --dataset production',
|
|
84
64
|
description: 'Upload a file with explicit project, dataset, and MIME type'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
command: "<%= config.bin %> <%= command.id %> --from-url 'https://example.com/hero.png' --type image --dataset production",
|
|
68
|
+
description: 'Have Sanity fetch an image from a public URL'
|
|
85
69
|
}
|
|
86
70
|
];
|
|
87
71
|
static flags = flags;
|
|
@@ -91,12 +75,30 @@ export class UploadAssetCommand extends SanityCommand {
|
|
|
91
75
|
static telemetry = defineCommandTelemetry(flags, {
|
|
92
76
|
redact: [
|
|
93
77
|
'file',
|
|
94
|
-
'filename'
|
|
78
|
+
'filename',
|
|
79
|
+
'from-url'
|
|
95
80
|
]
|
|
96
81
|
});
|
|
97
82
|
async run() {
|
|
98
83
|
const { flags } = await this.parse(UploadAssetCommand);
|
|
99
|
-
const
|
|
84
|
+
const sourceUrl = flags['from-url'];
|
|
85
|
+
if (sourceUrl && flags['content-type']) {
|
|
86
|
+
this.error('Asset upload failed: --content-type cannot be combined with --from-url. Sanity derives the MIME type automatically.', {
|
|
87
|
+
exit: exitCodes.USAGE_ERROR
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const sourceUrlError = sourceUrl === undefined ? undefined : getIngestUrlError(sourceUrl);
|
|
91
|
+
if (sourceUrlError) {
|
|
92
|
+
this.error(`Asset upload failed: ${sourceUrlError}`, {
|
|
93
|
+
exit: exitCodes.USAGE_ERROR
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const filenameError = flags.filename === undefined ? undefined : getAssetFilenameError(flags.filename);
|
|
97
|
+
if (filenameError) {
|
|
98
|
+
this.error(`Asset upload failed: ${filenameError}`, {
|
|
99
|
+
exit: exitCodes.USAGE_ERROR
|
|
100
|
+
});
|
|
101
|
+
}
|
|
100
102
|
const cliConfig = await this.tryGetCliConfig();
|
|
101
103
|
const projectId = await this.getProjectId({
|
|
102
104
|
fallback: ()=>promptForProject({})
|
|
@@ -107,17 +109,38 @@ export class UploadAssetCommand extends SanityCommand {
|
|
|
107
109
|
exit: exitCodes.USAGE_ERROR
|
|
108
110
|
});
|
|
109
111
|
}
|
|
112
|
+
const isInteractive = this.resolveIsInteractive();
|
|
110
113
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
let asset;
|
|
115
|
+
if (sourceUrl === undefined) {
|
|
116
|
+
// `exactlyOne` on the flags guarantees a source, but not to the compiler.
|
|
117
|
+
if (flags.file === undefined) {
|
|
118
|
+
this.error('Asset upload failed: Pass either --file <path> or --from-url <url>.', {
|
|
119
|
+
exit: exitCodes.USAGE_ERROR
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const filePath = resolve(flags.file);
|
|
123
|
+
asset = await uploadAssetWithProgress({
|
|
124
|
+
assetType: flags.type,
|
|
125
|
+
contentType: flags['content-type'],
|
|
126
|
+
dataset,
|
|
127
|
+
filename: flags.filename ?? basename(filePath),
|
|
128
|
+
filePath,
|
|
129
|
+
isInteractive,
|
|
130
|
+
logToStderr: (message)=>this.logToStderr(message),
|
|
131
|
+
projectId
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
asset = await ingestAssetFromUrlWithProgress({
|
|
135
|
+
assetType: flags.type,
|
|
136
|
+
dataset,
|
|
137
|
+
filename: flags.filename,
|
|
138
|
+
isInteractive,
|
|
139
|
+
logToStderr: (message)=>this.logToStderr(message),
|
|
140
|
+
projectId,
|
|
141
|
+
url: sourceUrl
|
|
142
|
+
});
|
|
143
|
+
}
|
|
121
144
|
const fieldType = flags.type === 'image' ? 'image' : 'file';
|
|
122
145
|
this.log(JSON.stringify({
|
|
123
146
|
asset: {
|
|
@@ -150,7 +173,10 @@ export class UploadAssetCommand extends SanityCommand {
|
|
|
150
173
|
});
|
|
151
174
|
}
|
|
152
175
|
uploadAssetDebug('Asset upload failed', error);
|
|
153
|
-
this.error(getAssetUploadErrorMessage(error
|
|
176
|
+
this.error(getAssetUploadErrorMessage(error, {
|
|
177
|
+
fromUrl: sourceUrl !== undefined,
|
|
178
|
+
target: 'dataset'
|
|
179
|
+
}), {
|
|
154
180
|
exit: exitCodes.RUNTIME_ERROR
|
|
155
181
|
});
|
|
156
182
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/commands/assets/upload.ts"],"sourcesContent":["import {basename, resolve} from 'node:path'\n\nimport {Flags} from '@oclif/core'\nimport {type FlagInput} from '@oclif/core/interfaces'\nimport {exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\nimport {getErrorMessage} from '@sanity/cli-core/errors'\nimport {isHttpError} from '@sanity/client'\n\nimport {AssetFileError} from '../../actions/assets/assetFileError.js'\nimport {uploadAssetWithProgress} from '../../actions/assets/uploadAssetWithProgress.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {type AssetType} from '../../services/assets.js'\nimport {getDatasetFlag, getProjectIdFlag} from '../../util/sharedFlags.js'\nimport {defineCommandTelemetry} from '../../util/telemetry/commandTelemetry.js'\n\nconst uploadAssetDebug = subdebug('assets:upload')\nconst DATASET_ASSET_LIMITS_URL =\n 'https://www.sanity.io/docs/content-lake/technical-limits#k2c53dc30e24b'\n\nfunction isProjectUserNotFoundError(body: Record<string, unknown>): boolean {\n const responseError = body.error\n return (\n typeof responseError === 'object' &&\n responseError !== null &&\n 'type' in responseError &&\n responseError.type === 'projectUserNotFoundError'\n )\n}\n\nfunction getAssetUploadErrorMessage(error: unknown): string {\n if (!isHttpError(error)) {\n return `Asset upload failed: ${getErrorMessage(error)}`\n }\n\n const body =\n typeof error.response.body === 'object' &&\n error.response.body !== null &&\n !Array.isArray(error.response.body)\n ? (error.response.body as Record<string, unknown>)\n : {}\n const responseError =\n typeof body.error === 'string' ? body.error : error.response.statusMessage || 'HTTP error'\n const statusCode =\n typeof body.statusCode === 'number' || typeof body.statusCode === 'string'\n ? body.statusCode\n : error.statusCode\n const projectUserNotFound = isProjectUserNotFoundError(body)\n const responseMessage = projectUserNotFound ? error.message : getErrorMessage(error)\n const message = /[.!?]$/.test(responseMessage) ? responseMessage : `${responseMessage}.`\n const details = typeof body.details === 'string' ? `\\n\\nDetails:\\n${body.details}` : ''\n const response = `Asset upload failed: HTTP ${statusCode} - ${responseError}\\n${message}${details}`\n\n if (error.statusCode === 401 && !projectUserNotFound) {\n return `${response}\\n\\nRun \\`sanity login\\` to authenticate, then try again.`\n }\n if (error.statusCode === 403) {\n return `${response}\\n\\nCheck that your account has write access to this dataset, then try again.`\n }\n if ([400, 413, 422].includes(error.statusCode)) {\n return `${response}\\n\\nCheck the asset requirements and current technical limits, then try again: ${DATASET_ASSET_LIMITS_URL}`\n }\n return `${response}\\n\\nTry again.`\n}\n\nconst flags = {\n ...getProjectIdFlag({\n description: 'Project ID to upload the asset to',\n semantics: 'override',\n }),\n ...getDatasetFlag({description: 'Dataset to upload the asset to', semantics: 'override'}),\n 'content-type': Flags.string({\n description: 'MIME type of the asset, such as image/png or application/pdf',\n helpValue: '<mime-type>',\n }),\n file: Flags.string({\n description: 'Path to the local file to upload',\n helpValue: '<path>',\n required: true,\n }),\n filename: Flags.string({\n description: 'Original filename stored on the asset document. Defaults to the local filename',\n helpValue: '<filename>',\n }),\n type: Flags.custom<AssetType>({\n default: 'image',\n description: 'Asset type to create',\n options: ['image', 'file'],\n })(),\n} satisfies FlagInput\n\nexport class UploadAssetCommand extends SanityCommand<typeof UploadAssetCommand> {\n static override description =\n 'Upload one local image or file to a Sanity dataset and print the asset document as JSON'\n\n static override examples = [\n {\n command:\n '<%= config.bin %> <%= command.id %> --file ./hero.png --type image --dataset production',\n description: 'Upload an image using the configured project',\n },\n {\n command:\n '<%= config.bin %> <%= command.id %> --file ./brief.pdf --type file --content-type application/pdf --project-id abc123 --dataset production',\n description: 'Upload a file with explicit project, dataset, and MIME type',\n },\n ]\n\n static override flags = flags\n\n static override hiddenAliases: string[] = ['asset:upload']\n\n static telemetry = defineCommandTelemetry(flags, {\n redact: ['file', 'filename'],\n })\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(UploadAssetCommand)\n const filePath = resolve(flags.file)\n\n const cliConfig = await this.tryGetCliConfig()\n const projectId = await this.getProjectId({fallback: () => promptForProject({})})\n const dataset = flags.dataset ?? cliConfig.api?.dataset\n if (!dataset) {\n this.error(\n 'Asset upload failed: Dataset is required. Pass --dataset <name> or configure a dataset in sanity.cli.ts.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n try {\n const asset = await uploadAssetWithProgress({\n assetType: flags.type,\n contentType: flags['content-type'],\n dataset,\n filename: flags.filename ?? basename(filePath),\n filePath,\n isInteractive: this.resolveIsInteractive(),\n logToStderr: (message) => this.logToStderr(message),\n projectId,\n })\n const fieldType = flags.type === 'image' ? 'image' : 'file'\n\n this.log(\n JSON.stringify(\n {\n asset: {\n _id: asset._id,\n _type: asset._type,\n extension: asset.extension,\n mimeType: asset.mimeType,\n originalFilename: asset.originalFilename,\n size: asset.size,\n url: asset.url,\n },\n reference: {\n _type: fieldType,\n asset: {_ref: asset._id, _type: 'reference'},\n },\n },\n null,\n 2,\n ),\n )\n } catch (error) {\n if (error instanceof Error && error.message === 'SIGINT') throw error\n if (error instanceof AssetFileError) {\n if (error.reason === 'not-file') {\n this.error('Asset upload failed: --file must point to a file, not a directory.', {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n this.error(\n 'Asset upload failed: Cannot read the local file. Check that --file points to a readable file, then retry.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n uploadAssetDebug('Asset upload failed', error)\n this.error(getAssetUploadErrorMessage(error), {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n}\n"],"names":["basename","resolve","Flags","exitCodes","SanityCommand","subdebug","getErrorMessage","isHttpError","AssetFileError","uploadAssetWithProgress","promptForProject","getDatasetFlag","getProjectIdFlag","defineCommandTelemetry","uploadAssetDebug","DATASET_ASSET_LIMITS_URL","isProjectUserNotFoundError","body","responseError","error","type","getAssetUploadErrorMessage","response","Array","isArray","statusMessage","statusCode","projectUserNotFound","responseMessage","message","test","details","includes","flags","description","semantics","string","helpValue","file","required","filename","custom","default","options","UploadAssetCommand","examples","command","hiddenAliases","telemetry","redact","run","parse","filePath","cliConfig","tryGetCliConfig","projectId","getProjectId","fallback","dataset","api","exit","USAGE_ERROR","asset","assetType","contentType","isInteractive","resolveIsInteractive","logToStderr","fieldType","log","JSON","stringify","_id","_type","extension","mimeType","originalFilename","size","url","reference","_ref","Error","reason","RUNTIME_ERROR"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/assets/upload.ts"],"sourcesContent":["import {basename, resolve} from 'node:path'\n\nimport {Flags} from '@oclif/core'\nimport {type FlagInput} from '@oclif/core/interfaces'\nimport {exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\n\nimport {AssetFileError} from '../../actions/assets/assetFileError.js'\nimport {ingestAssetFromUrlWithProgress} from '../../actions/assets/ingestAssetFromUrlWithProgress.js'\nimport {uploadAssetWithProgress} from '../../actions/assets/uploadAssetWithProgress.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {type AssetType} from '../../services/assets.js'\nimport {getAssetFilenameError, getIngestUrlError} from '../../util/assetSourceValidation.js'\nimport {getAssetUploadErrorMessage} from '../../util/assetUploadErrors.js'\nimport {getDatasetFlag, getProjectIdFlag} from '../../util/sharedFlags.js'\nimport {defineCommandTelemetry} from '../../util/telemetry/commandTelemetry.js'\n\nconst uploadAssetDebug = subdebug('assets:upload')\n\nconst flags = {\n ...getProjectIdFlag({\n description: 'Project ID to upload the asset to',\n semantics: 'override',\n }),\n ...getDatasetFlag({description: 'Dataset to upload the asset to', semantics: 'override'}),\n 'content-type': Flags.string({\n description:\n 'MIME type of the asset, such as image/png or application/pdf. Only applies to --file',\n helpValue: '<mime-type>',\n }),\n file: Flags.string({\n description: 'Path to the local file to upload',\n exactlyOne: ['file', 'from-url'],\n helpValue: '<path>',\n }),\n filename: Flags.string({\n description:\n 'Original filename stored on the asset document. Defaults to the local filename when using --file',\n helpValue: '<filename>',\n }),\n 'from-url': Flags.string({\n description:\n 'https URL for Sanity to fetch the asset from, instead of uploading a local file. Must be reachable from the public internet without authentication',\n exactlyOne: ['file', 'from-url'],\n helpValue: '<url>',\n }),\n type: Flags.custom<AssetType>({\n default: 'image',\n description: 'Asset type to create',\n options: ['image', 'file'],\n })(),\n} satisfies FlagInput\n\nexport class UploadAssetCommand extends SanityCommand<typeof UploadAssetCommand> {\n static override description =\n 'Upload one image or file to a Sanity dataset, from a local path or a URL, and print the asset document as JSON'\n\n static override examples = [\n {\n command:\n '<%= config.bin %> <%= command.id %> --file ./hero.png --type image --dataset production',\n description: 'Upload an image using the configured project',\n },\n {\n command:\n '<%= config.bin %> <%= command.id %> --file ./brief.pdf --type file --content-type application/pdf --project-id abc123 --dataset production',\n description: 'Upload a file with explicit project, dataset, and MIME type',\n },\n {\n command:\n \"<%= config.bin %> <%= command.id %> --from-url 'https://example.com/hero.png' --type image --dataset production\",\n description: 'Have Sanity fetch an image from a public URL',\n },\n ]\n\n static override flags = flags\n\n static override hiddenAliases: string[] = ['asset:upload']\n\n static telemetry = defineCommandTelemetry(flags, {\n redact: ['file', 'filename', 'from-url'],\n })\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(UploadAssetCommand)\n const sourceUrl = flags['from-url']\n\n if (sourceUrl && flags['content-type']) {\n this.error(\n 'Asset upload failed: --content-type cannot be combined with --from-url. Sanity derives the MIME type automatically.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n const sourceUrlError = sourceUrl === undefined ? undefined : getIngestUrlError(sourceUrl)\n if (sourceUrlError) {\n this.error(`Asset upload failed: ${sourceUrlError}`, {exit: exitCodes.USAGE_ERROR})\n }\n\n const filenameError =\n flags.filename === undefined ? undefined : getAssetFilenameError(flags.filename)\n if (filenameError) {\n this.error(`Asset upload failed: ${filenameError}`, {exit: exitCodes.USAGE_ERROR})\n }\n\n const cliConfig = await this.tryGetCliConfig()\n const projectId = await this.getProjectId({fallback: () => promptForProject({})})\n const dataset = flags.dataset ?? cliConfig.api?.dataset\n if (!dataset) {\n this.error(\n 'Asset upload failed: Dataset is required. Pass --dataset <name> or configure a dataset in sanity.cli.ts.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n const isInteractive = this.resolveIsInteractive()\n\n try {\n let asset\n if (sourceUrl === undefined) {\n // `exactlyOne` on the flags guarantees a source, but not to the compiler.\n if (flags.file === undefined) {\n this.error('Asset upload failed: Pass either --file <path> or --from-url <url>.', {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n const filePath = resolve(flags.file)\n asset = await uploadAssetWithProgress({\n assetType: flags.type,\n contentType: flags['content-type'],\n dataset,\n filename: flags.filename ?? basename(filePath),\n filePath,\n isInteractive,\n logToStderr: (message) => this.logToStderr(message),\n projectId,\n })\n } else {\n asset = await ingestAssetFromUrlWithProgress({\n assetType: flags.type,\n dataset,\n filename: flags.filename,\n isInteractive,\n logToStderr: (message) => this.logToStderr(message),\n projectId,\n url: sourceUrl,\n })\n }\n const fieldType = flags.type === 'image' ? 'image' : 'file'\n\n this.log(\n JSON.stringify(\n {\n asset: {\n _id: asset._id,\n _type: asset._type,\n extension: asset.extension,\n mimeType: asset.mimeType,\n originalFilename: asset.originalFilename,\n size: asset.size,\n url: asset.url,\n },\n reference: {\n _type: fieldType,\n asset: {_ref: asset._id, _type: 'reference'},\n },\n },\n null,\n 2,\n ),\n )\n } catch (error) {\n if (error instanceof Error && error.message === 'SIGINT') throw error\n if (error instanceof AssetFileError) {\n if (error.reason === 'not-file') {\n this.error('Asset upload failed: --file must point to a file, not a directory.', {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n this.error(\n 'Asset upload failed: Cannot read the local file. Check that --file points to a readable file, then retry.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n uploadAssetDebug('Asset upload failed', error)\n this.error(\n getAssetUploadErrorMessage(error, {\n fromUrl: sourceUrl !== undefined,\n target: 'dataset',\n }),\n {exit: exitCodes.RUNTIME_ERROR},\n )\n }\n }\n}\n"],"names":["basename","resolve","Flags","exitCodes","SanityCommand","subdebug","AssetFileError","ingestAssetFromUrlWithProgress","uploadAssetWithProgress","promptForProject","getAssetFilenameError","getIngestUrlError","getAssetUploadErrorMessage","getDatasetFlag","getProjectIdFlag","defineCommandTelemetry","uploadAssetDebug","flags","description","semantics","string","helpValue","file","exactlyOne","filename","type","custom","default","options","UploadAssetCommand","examples","command","hiddenAliases","telemetry","redact","run","parse","sourceUrl","error","exit","USAGE_ERROR","sourceUrlError","undefined","filenameError","cliConfig","tryGetCliConfig","projectId","getProjectId","fallback","dataset","api","isInteractive","resolveIsInteractive","asset","filePath","assetType","contentType","logToStderr","message","url","fieldType","log","JSON","stringify","_id","_type","extension","mimeType","originalFilename","size","reference","_ref","Error","reason","fromUrl","target","RUNTIME_ERROR"],"mappings":"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"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -2,9 +2,11 @@ import path from 'node:path';
|
|
|
2
2
|
import { Args, Flags } from '@oclif/core';
|
|
3
3
|
import { exitCodes, SanityCommand } from '@sanity/cli-core';
|
|
4
4
|
import { confirm } from '@sanity/cli-core/ux';
|
|
5
|
+
import { getWorkbench, resolveWorkbenchConfig } from '@sanity/workbench-cli/deploy';
|
|
5
6
|
import { deployApp } from '../actions/deploy/deployApp.js';
|
|
6
7
|
import { deployDebug } from '../actions/deploy/deployDebug.js';
|
|
7
8
|
import { deployStudio } from '../actions/deploy/deployStudio.js';
|
|
9
|
+
import { getAppId } from '../util/appId.js';
|
|
8
10
|
import { determineIsApp } from '../util/determineIsApp.js';
|
|
9
11
|
import { dirIsEmptyOrNonExistent } from '../util/dirIsEmptyOrNonExistent.js';
|
|
10
12
|
export class DeployCommand extends SanityCommand {
|
|
@@ -30,6 +32,18 @@ export class DeployCommand extends SanityCommand {
|
|
|
30
32
|
{
|
|
31
33
|
command: '<%= config.bin %> <%= command.id %> --external',
|
|
32
34
|
description: 'Register an externally hosted studio (studioHost contains full URL)'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
command: '<%= config.bin %> <%= command.id %> --create --title "My App" --yes --json',
|
|
38
|
+
description: 'Create and deploy an App SDK app without prompts. Save application.id as deployment.appId, then omit --create on later deploys'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
command: '<%= config.bin %> <%= command.id %> --create --title "My App" --dry-run --json',
|
|
42
|
+
description: 'Preview a new App SDK deployment without creating or uploading anything'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
command: '<%= config.bin %> <%= command.id %> --url my-studio --yes',
|
|
46
|
+
description: 'Deploy a studio without prompts using its hostname'
|
|
33
47
|
}
|
|
34
48
|
];
|
|
35
49
|
static flags = {
|
|
@@ -43,6 +57,13 @@ export class DeployCommand extends SanityCommand {
|
|
|
43
57
|
default: true,
|
|
44
58
|
description: 'Build the studio before deploying (use --no-build to deploy existing `dist/` output)'
|
|
45
59
|
}),
|
|
60
|
+
create: Flags.boolean({
|
|
61
|
+
description: 'Create a new App SDK app, even if the organization has other apps',
|
|
62
|
+
exclusive: [
|
|
63
|
+
'external',
|
|
64
|
+
'url'
|
|
65
|
+
]
|
|
66
|
+
}),
|
|
46
67
|
'dry-run': Flags.boolean({
|
|
47
68
|
default: false,
|
|
48
69
|
description: 'Report what would be deployed without uploading or creating anything'
|
|
@@ -75,7 +96,7 @@ export class DeployCommand extends SanityCommand {
|
|
|
75
96
|
description: 'Enable source maps for built bundles (increases size of bundle)'
|
|
76
97
|
}),
|
|
77
98
|
title: Flags.string({
|
|
78
|
-
description: 'Title for a newly created application or studio. For apps
|
|
99
|
+
description: 'Title for a newly created application or studio. For apps, use --create to select creation when the organization already has apps'
|
|
79
100
|
}),
|
|
80
101
|
url: Flags.string({
|
|
81
102
|
description: 'Studio URL for deployment. For external studios, the full URL. For hosted studios, the hostname (e.g. "my-studio" or "my-studio.sanity.studio")'
|
|
@@ -95,6 +116,17 @@ export class DeployCommand extends SanityCommand {
|
|
|
95
116
|
const cliConfig = await this.getCliConfig();
|
|
96
117
|
const projectRoot = await this.getProjectRoot();
|
|
97
118
|
const isApp = determineIsApp(cliConfig);
|
|
119
|
+
if (flags.create) {
|
|
120
|
+
if (!isApp) {
|
|
121
|
+
return this.output.error('--create is only supported for App SDK apps. For a studio, use --url <hostname>.');
|
|
122
|
+
}
|
|
123
|
+
if (getAppId(cliConfig)) {
|
|
124
|
+
return this.output.error('--create cannot be used with a configured app ID. Omit --create to deploy to that app, or remove deployment.appId and app.id to create a new one.');
|
|
125
|
+
}
|
|
126
|
+
if (getWorkbench(cliConfig) || resolveWorkbenchConfig(cliConfig)) {
|
|
127
|
+
return this.output.error('--create is not supported for this app configuration. Omit --create and run sanity deploy.');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
98
130
|
const defaultOutputDir = path.resolve(path.join(projectRoot.directory, 'dist'));
|
|
99
131
|
const sourceDir = path.resolve(process.cwd(), this.args.sourceDir || defaultOutputDir);
|
|
100
132
|
// Skip the directory check if the studio is externally hosted
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/commands/deploy.ts"],"sourcesContent":["import path from 'node:path'\n\nimport {Args, Flags} from '@oclif/core'\nimport {exitCodes, SanityCommand} from '@sanity/cli-core'\nimport {confirm} from '@sanity/cli-core/ux'\n\nimport {deployApp} from '../actions/deploy/deployApp.js'\nimport {deployDebug} from '../actions/deploy/deployDebug.js'\nimport {deployStudio} from '../actions/deploy/deployStudio.js'\nimport {determineIsApp} from '../util/determineIsApp.js'\nimport {dirIsEmptyOrNonExistent} from '../util/dirIsEmptyOrNonExistent.js'\n\nexport class DeployCommand extends SanityCommand<typeof DeployCommand> {\n static override args = {\n sourceDir: Args.directory({\n description: 'Source directory',\n }),\n }\n\n static override description = 'Builds and deploys Sanity Studio or application to Sanity hosting'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Build and deploy the studio to Sanity hosting',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --no-minify --source-maps',\n description: 'Deploys non-minified build with source maps',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --schema-required',\n description:\n 'Fail fast on schema store fails - for when other services rely on the stored schema',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --external',\n description: 'Register an externally hosted studio (studioHost contains full URL)',\n },\n ]\n\n static override flags = {\n 'auto-updates': Flags.boolean({\n allowNo: true,\n deprecated: true,\n description: 'Automatically update the studio to the latest version',\n }),\n build: Flags.boolean({\n allowNo: true,\n default: true,\n description:\n 'Build the studio before deploying (use --no-build to deploy existing `dist/` output)',\n }),\n 'dry-run': Flags.boolean({\n default: false,\n description: 'Report what would be deployed without uploading or creating anything',\n }),\n external: Flags.boolean({\n default: false,\n description: 'Register an externally hosted studio',\n exclusive: ['source-maps', 'minify', 'build'],\n }),\n json: Flags.boolean({\n char: 'j',\n default: false,\n description: 'Output the result as JSON',\n }),\n minify: Flags.boolean({\n allowNo: true,\n default: true,\n description: 'Minify built JavaScript (use --no-minify to skip for faster builds)',\n }),\n 'schema-required': Flags.boolean({\n default: false,\n description: 'Fail if schema deployment fails',\n }),\n 'source-maps': Flags.boolean({\n default: false,\n description: 'Enable source maps for built bundles (increases size of bundle)',\n }),\n title: Flags.string({\n description:\n 'Title for a newly created application or studio. For apps it also skips the interactive title prompt, enabling unattended creation',\n }),\n url: Flags.string({\n description:\n 'Studio URL for deployment. For external studios, the full URL. For hosted studios, the hostname (e.g. \"my-studio\" or \"my-studio.sanity.studio\")',\n }),\n verbose: Flags.boolean({\n default: false,\n description: 'Enable verbose logging',\n }),\n yes: Flags.boolean({\n char: 'y',\n default: false,\n description:\n 'Unattended mode, answers \"yes\" to any \"yes/no\" prompt and otherwise uses defaults',\n }),\n }\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(DeployCommand)\n\n const cliConfig = await this.getCliConfig()\n const projectRoot = await this.getProjectRoot()\n\n const isApp = determineIsApp(cliConfig)\n\n const defaultOutputDir = path.resolve(path.join(projectRoot.directory, 'dist'))\n const sourceDir = path.resolve(process.cwd(), this.args.sourceDir || defaultOutputDir)\n\n // Skip the directory check if the studio is externally hosted\n if (this.args.sourceDir && this.args.sourceDir !== 'dist' && !flags.external) {\n let relativeOutput = path.relative(process.cwd(), sourceDir)\n if (relativeOutput[0] !== '.') {\n relativeOutput = `./${relativeOutput}`\n }\n\n if (!this.isUnattended() && !flags['dry-run']) {\n const isEmpty = await dirIsEmptyOrNonExistent(sourceDir)\n const shouldProceed =\n isEmpty ||\n (await confirm({\n default: false,\n message: `\"${relativeOutput}\" is not empty, do you want to proceed?`,\n }))\n\n if (!shouldProceed) {\n this.output.error('Cancelled.', {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n\n // Keep --json's stdout clean for the payload\n if (!flags.json) this.output.log(`Building to ${relativeOutput}\\n`)\n }\n\n // Force yes downstream: build/app resolution otherwise prompts for prerelease/version choices\n const deployFlags = this.isUnattended() || flags['dry-run'] ? {...flags, yes: true} : flags\n\n if (isApp) {\n deployDebug('Deploying app')\n await deployApp({\n cliConfig,\n flags: deployFlags,\n output: this.output,\n projectRoot,\n sourceDir,\n })\n } else {\n deployDebug('Deploying studio')\n await deployStudio({\n cliConfig,\n flags: deployFlags,\n output: this.output,\n projectRoot,\n sourceDir,\n })\n }\n }\n}\n"],"names":["path","Args","Flags","exitCodes","SanityCommand","confirm","deployApp","deployDebug","deployStudio","determineIsApp","dirIsEmptyOrNonExistent","DeployCommand","args","sourceDir","directory","description","examples","command","flags","boolean","allowNo","deprecated","build","default","external","exclusive","json","char","minify","title","string","url","verbose","yes","run","parse","cliConfig","getCliConfig","projectRoot","getProjectRoot","isApp","defaultOutputDir","resolve","join","process","cwd","relativeOutput","relative","isUnattended","isEmpty","shouldProceed","message","output","error","exit","RUNTIME_ERROR","log","deployFlags"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/commands/deploy.ts"],"sourcesContent":["import path from 'node:path'\n\nimport {Args, Flags} from '@oclif/core'\nimport {exitCodes, SanityCommand} from '@sanity/cli-core'\nimport {confirm} from '@sanity/cli-core/ux'\nimport {getWorkbench, resolveWorkbenchConfig} from '@sanity/workbench-cli/deploy'\n\nimport {deployApp} from '../actions/deploy/deployApp.js'\nimport {deployDebug} from '../actions/deploy/deployDebug.js'\nimport {deployStudio} from '../actions/deploy/deployStudio.js'\nimport {getAppId} from '../util/appId.js'\nimport {determineIsApp} from '../util/determineIsApp.js'\nimport {dirIsEmptyOrNonExistent} from '../util/dirIsEmptyOrNonExistent.js'\n\nexport class DeployCommand extends SanityCommand<typeof DeployCommand> {\n static override args = {\n sourceDir: Args.directory({\n description: 'Source directory',\n }),\n }\n\n static override description = 'Builds and deploys Sanity Studio or application to Sanity hosting'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Build and deploy the studio to Sanity hosting',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --no-minify --source-maps',\n description: 'Deploys non-minified build with source maps',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --schema-required',\n description:\n 'Fail fast on schema store fails - for when other services rely on the stored schema',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --external',\n description: 'Register an externally hosted studio (studioHost contains full URL)',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --create --title \"My App\" --yes --json',\n description:\n 'Create and deploy an App SDK app without prompts. Save application.id as deployment.appId, then omit --create on later deploys',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --create --title \"My App\" --dry-run --json',\n description: 'Preview a new App SDK deployment without creating or uploading anything',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --url my-studio --yes',\n description: 'Deploy a studio without prompts using its hostname',\n },\n ]\n\n static override flags = {\n 'auto-updates': Flags.boolean({\n allowNo: true,\n deprecated: true,\n description: 'Automatically update the studio to the latest version',\n }),\n build: Flags.boolean({\n allowNo: true,\n default: true,\n description:\n 'Build the studio before deploying (use --no-build to deploy existing `dist/` output)',\n }),\n create: Flags.boolean({\n description: 'Create a new App SDK app, even if the organization has other apps',\n exclusive: ['external', 'url'],\n }),\n 'dry-run': Flags.boolean({\n default: false,\n description: 'Report what would be deployed without uploading or creating anything',\n }),\n external: Flags.boolean({\n default: false,\n description: 'Register an externally hosted studio',\n exclusive: ['source-maps', 'minify', 'build'],\n }),\n json: Flags.boolean({\n char: 'j',\n default: false,\n description: 'Output the result as JSON',\n }),\n minify: Flags.boolean({\n allowNo: true,\n default: true,\n description: 'Minify built JavaScript (use --no-minify to skip for faster builds)',\n }),\n 'schema-required': Flags.boolean({\n default: false,\n description: 'Fail if schema deployment fails',\n }),\n 'source-maps': Flags.boolean({\n default: false,\n description: 'Enable source maps for built bundles (increases size of bundle)',\n }),\n title: Flags.string({\n description:\n 'Title for a newly created application or studio. For apps, use --create to select creation when the organization already has apps',\n }),\n url: Flags.string({\n description:\n 'Studio URL for deployment. For external studios, the full URL. For hosted studios, the hostname (e.g. \"my-studio\" or \"my-studio.sanity.studio\")',\n }),\n verbose: Flags.boolean({\n default: false,\n description: 'Enable verbose logging',\n }),\n yes: Flags.boolean({\n char: 'y',\n default: false,\n description:\n 'Unattended mode, answers \"yes\" to any \"yes/no\" prompt and otherwise uses defaults',\n }),\n }\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(DeployCommand)\n\n const cliConfig = await this.getCliConfig()\n const projectRoot = await this.getProjectRoot()\n\n const isApp = determineIsApp(cliConfig)\n\n if (flags.create) {\n if (!isApp) {\n return this.output.error(\n '--create is only supported for App SDK apps. For a studio, use --url <hostname>.',\n )\n }\n if (getAppId(cliConfig)) {\n return this.output.error(\n '--create cannot be used with a configured app ID. Omit --create to deploy to that app, or remove deployment.appId and app.id to create a new one.',\n )\n }\n if (getWorkbench(cliConfig) || resolveWorkbenchConfig(cliConfig)) {\n return this.output.error(\n '--create is not supported for this app configuration. Omit --create and run sanity deploy.',\n )\n }\n }\n\n const defaultOutputDir = path.resolve(path.join(projectRoot.directory, 'dist'))\n const sourceDir = path.resolve(process.cwd(), this.args.sourceDir || defaultOutputDir)\n\n // Skip the directory check if the studio is externally hosted\n if (this.args.sourceDir && this.args.sourceDir !== 'dist' && !flags.external) {\n let relativeOutput = path.relative(process.cwd(), sourceDir)\n if (relativeOutput[0] !== '.') {\n relativeOutput = `./${relativeOutput}`\n }\n\n if (!this.isUnattended() && !flags['dry-run']) {\n const isEmpty = await dirIsEmptyOrNonExistent(sourceDir)\n const shouldProceed =\n isEmpty ||\n (await confirm({\n default: false,\n message: `\"${relativeOutput}\" is not empty, do you want to proceed?`,\n }))\n\n if (!shouldProceed) {\n this.output.error('Cancelled.', {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n\n // Keep --json's stdout clean for the payload\n if (!flags.json) this.output.log(`Building to ${relativeOutput}\\n`)\n }\n\n // Force yes downstream: build/app resolution otherwise prompts for prerelease/version choices\n const deployFlags = this.isUnattended() || flags['dry-run'] ? {...flags, yes: true} : flags\n\n if (isApp) {\n deployDebug('Deploying app')\n await deployApp({\n cliConfig,\n flags: deployFlags,\n output: this.output,\n projectRoot,\n sourceDir,\n })\n } else {\n deployDebug('Deploying studio')\n await deployStudio({\n cliConfig,\n flags: deployFlags,\n output: this.output,\n projectRoot,\n sourceDir,\n })\n }\n }\n}\n"],"names":["path","Args","Flags","exitCodes","SanityCommand","confirm","getWorkbench","resolveWorkbenchConfig","deployApp","deployDebug","deployStudio","getAppId","determineIsApp","dirIsEmptyOrNonExistent","DeployCommand","args","sourceDir","directory","description","examples","command","flags","boolean","allowNo","deprecated","build","default","create","exclusive","external","json","char","minify","title","string","url","verbose","yes","run","parse","cliConfig","getCliConfig","projectRoot","getProjectRoot","isApp","output","error","defaultOutputDir","resolve","join","process","cwd","relativeOutput","relative","isUnattended","isEmpty","shouldProceed","message","exit","RUNTIME_ERROR","log","deployFlags"],"mappings":"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"}
|