@sanity/cli 8.22.1 → 8.24.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 +313 -0
- package/dist/actions/codemods/dashboard.js +136 -8
- package/dist/actions/codemods/dashboard.js.map +1 -1
- package/dist/actions/codemods/types.js.map +1 -1
- package/dist/actions/context/watchJob.js +9 -3
- package/dist/actions/context/watchJob.js.map +1 -1
- package/dist/actions/deploy/blueprintManaged.js +59 -0
- package/dist/actions/deploy/blueprintManaged.js.map +1 -0
- package/dist/actions/deploy/deployApp.js +117 -1
- package/dist/actions/deploy/deployApp.js.map +1 -1
- package/dist/actions/deploy/deployRunner.js.map +1 -1
- package/dist/actions/deploy/deployStudio.js +6 -31
- package/dist/actions/deploy/deployStudio.js.map +1 -1
- package/dist/actions/exec/runScript.worker.js +6 -2
- package/dist/actions/exec/runScript.worker.js.map +1 -1
- package/dist/actions/init/bootstrapLocalTemplate.js +35 -11
- package/dist/actions/init/bootstrapLocalTemplate.js.map +1 -1
- package/dist/actions/init/createMediaLibraryCliConfig.js +12 -0
- package/dist/actions/init/createMediaLibraryCliConfig.js.map +1 -0
- package/dist/actions/init/determineMediaLibraryTemplate.js +14 -0
- package/dist/actions/init/determineMediaLibraryTemplate.js.map +1 -0
- package/dist/actions/init/initAction.js +35 -6
- package/dist/actions/init/initAction.js.map +1 -1
- package/dist/actions/init/initApp.js +81 -6
- package/dist/actions/init/initApp.js.map +1 -1
- package/dist/actions/init/offerAspectPull.js +81 -0
- package/dist/actions/init/offerAspectPull.js.map +1 -0
- package/dist/actions/init/project/getOrCreateDataset.js +8 -5
- package/dist/actions/init/project/getOrCreateDataset.js.map +1 -1
- package/dist/actions/init/project/getOrCreateProject.js +7 -5
- package/dist/actions/init/project/getOrCreateProject.js.map +1 -1
- package/dist/actions/init/project/getProjectDetails.js +25 -2
- package/dist/actions/init/project/getProjectDetails.js.map +1 -1
- package/dist/actions/init/project/promptUserForOrganization.js +6 -4
- package/dist/actions/init/project/promptUserForOrganization.js.map +1 -1
- package/dist/actions/init/templates/index.js +3 -0
- package/dist/actions/init/templates/index.js.map +1 -1
- package/dist/actions/init/templates/mediaLibraryConfig.js +51 -0
- package/dist/actions/init/templates/mediaLibraryConfig.js.map +1 -0
- package/dist/actions/media/aspectName.js +8 -0
- package/dist/actions/media/aspectName.js.map +1 -0
- package/dist/actions/media/aspectSync/contract.js +163 -0
- package/dist/actions/media/aspectSync/contract.js.map +1 -0
- package/dist/actions/media/aspectSync/syncAspects.js +115 -0
- package/dist/actions/media/aspectSync/syncAspects.js.map +1 -0
- package/dist/actions/media/cliConfigFields.js +192 -0
- package/dist/actions/media/cliConfigFields.js.map +1 -0
- package/dist/actions/media/pullAspects.js +225 -0
- package/dist/actions/media/pullAspects.js.map +1 -0
- package/dist/actions/media/resolveMediaLibraryId.js +83 -0
- package/dist/actions/media/resolveMediaLibraryId.js.map +1 -0
- package/dist/commands/codemod.js +1 -0
- package/dist/commands/codemod.js.map +1 -1
- package/dist/commands/context/build.js +2 -2
- package/dist/commands/context/build.js.map +1 -1
- package/dist/commands/context/entries/get.js +83 -0
- package/dist/commands/context/entries/get.js.map +1 -0
- package/dist/commands/context/entries/list.js +82 -0
- package/dist/commands/context/entries/list.js.map +1 -0
- package/dist/commands/context/entries/rebuild.js +95 -0
- package/dist/commands/context/entries/rebuild.js.map +1 -0
- package/dist/commands/context/issues/apply.js +86 -0
- package/dist/commands/context/issues/apply.js.map +1 -0
- package/dist/commands/context/issues/dismiss.js +52 -0
- package/dist/commands/context/issues/dismiss.js.map +1 -0
- package/dist/commands/context/issues/get.js +105 -0
- package/dist/commands/context/issues/get.js.map +1 -0
- package/dist/commands/context/issues/list.js +128 -0
- package/dist/commands/context/issues/list.js.map +1 -0
- package/dist/commands/context/issues/reopen.js +53 -0
- package/dist/commands/context/issues/reopen.js.map +1 -0
- package/dist/commands/context/issues/resolve.js +137 -0
- package/dist/commands/context/issues/resolve.js.map +1 -0
- package/dist/commands/context/jobs/get.js +2 -2
- package/dist/commands/context/jobs/get.js.map +1 -1
- package/dist/commands/context/query.js +92 -0
- package/dist/commands/context/query.js.map +1 -0
- package/dist/commands/deploy.js +13 -0
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/media/prune-aspect-data.js +170 -0
- package/dist/commands/media/prune-aspect-data.js.map +1 -0
- package/dist/commands/media/pull-aspects.js +309 -0
- package/dist/commands/media/pull-aspects.js.map +1 -0
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +17 -0
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
- package/dist/services/context.js +96 -3
- package/dist/services/context.js.map +1 -1
- package/dist/services/mediaLibraries.js +175 -8
- package/dist/services/mediaLibraries.js.map +1 -1
- package/dist/util/searchChoices.js +52 -0
- package/dist/util/searchChoices.js.map +1 -0
- package/oclif.manifest.json +2134 -1445
- package/package.json +9 -8
- package/templates/media-library-config/README.md +52 -0
- package/templates/media-library-config/eslint.config.txt +52 -0
- package/templates/media-library-config/src/fields/caption.tsx +29 -0
- package/templates/media-library-config/src/fields/notes.tsx +54 -0
- package/templates/shopify/.gitignore +0 -26
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/context/query.ts"],"sourcesContent":["import {Args, Flags} from '@oclif/core'\nimport {type FlagInput} from '@oclif/core/interfaces'\nimport {colorizeJson, exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\nimport {getErrorMessage} from '@sanity/cli-core/errors'\nimport {isRecord} from '@sanity/cli-core/util'\n\nimport {\n MissingOrganizationError,\n resolveOrganizationId,\n} from '../../actions/context/resolveOrganizationId.js'\nimport {queryContext} from '../../services/context.js'\nimport {getOrganizationFlag} from '../../util/sharedFlags.js'\nimport {defineCommandTelemetry} from '../../util/telemetry/commandTelemetry.js'\n\nconst queryContextDebug = subdebug('context:query')\n\nconst flags = {\n ...getOrganizationFlag({\n description: 'Organization to query',\n semantics: 'override',\n }),\n params: Flags.custom<Record<string, unknown>>({\n description: 'Query parameters as a JSON object',\n helpValue: '<json>',\n parse: async (input: string) => {\n let parsed: unknown\n try {\n parsed = JSON.parse(input)\n } catch {\n throw new Error('`--params` must be valid JSON')\n }\n if (!isRecord(parsed)) {\n throw new Error('`--params` must be a JSON object, like \\'{\"type\": \"issue\"}\\'')\n }\n return parsed\n },\n })(),\n} satisfies FlagInput\n\nexport class QueryContextCommand extends SanityCommand<typeof QueryContextCommand> {\n static override args = {\n query: Args.string({\n description: \"GROQ query to run against the organization's Context documents\",\n required: true,\n }),\n }\n\n static override description = \"Query an organization's Context documents with GROQ\"\n\n static override enableJsonFlag = true\n\n static override examples = [\n {\n command:\n '<%= config.bin %> <%= command.id %> \\'*[_type == \"sanity.context.issue\" && status == \"open\"]{_id, knowledgeBaseId}\\'',\n description: 'List open issues across all knowledge bases',\n },\n {\n command:\n '<%= config.bin %> <%= command.id %> \\'*[_type == \"sanity.context.entry\" && knowledgeBaseId == $kb].path\\' --params \\'{\"kb\": \"kb-abc123\"}\\' --json',\n description: 'List the entry paths of one knowledge base as plain JSON',\n },\n ]\n\n static override flags = flags\n\n static telemetry = defineCommandTelemetry(flags, {redact: ['params']})\n\n public async run(): Promise<void> {\n const {query} = this.args\n const {json, organization, params} = this.flags\n\n let organizationId: string\n try {\n organizationId = await resolveOrganizationId({\n configuredOrganizationId: (await this.tryGetCliConfig()).app?.organizationId,\n flagOrganizationId: organization,\n unattended: this.isUnattended(),\n })\n } catch (error) {\n queryContextDebug('Error resolving organization', error)\n this.error(getErrorMessage(error), {\n exit:\n error instanceof MissingOrganizationError\n ? exitCodes.USAGE_ERROR\n : exitCodes.RUNTIME_ERROR,\n })\n }\n\n let result\n try {\n result = await queryContext(organizationId, query, params)\n } catch (error) {\n queryContextDebug('Error running query', error)\n this.error(`Failed to run query: ${getErrorMessage(error)}`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n // GROQ returns null for no match; guard undefined, which colorizeJson cannot tokenize\n const output = result ?? null\n if (json) {\n this.logJson(output)\n return\n }\n\n this.log(colorizeJson(output))\n }\n}\n"],"names":["Args","Flags","colorizeJson","exitCodes","SanityCommand","subdebug","getErrorMessage","isRecord","MissingOrganizationError","resolveOrganizationId","queryContext","getOrganizationFlag","defineCommandTelemetry","queryContextDebug","flags","description","semantics","params","custom","helpValue","parse","input","parsed","JSON","Error","QueryContextCommand","args","query","string","required","enableJsonFlag","examples","command","telemetry","redact","run","json","organization","organizationId","configuredOrganizationId","tryGetCliConfig","app","flagOrganizationId","unattended","isUnattended","error","exit","USAGE_ERROR","RUNTIME_ERROR","result","output","logJson","log"],"mappings":"AAAA,SAAQA,IAAI,EAAEC,KAAK,QAAO,cAAa;AAEvC,SAAQC,YAAY,EAAEC,SAAS,EAAEC,aAAa,EAAEC,QAAQ,QAAO,mBAAkB;AACjF,SAAQC,eAAe,QAAO,0BAAyB;AACvD,SAAQC,QAAQ,QAAO,wBAAuB;AAE9C,SACEC,wBAAwB,EACxBC,qBAAqB,QAChB,iDAAgD;AACvD,SAAQC,YAAY,QAAO,4BAA2B;AACtD,SAAQC,mBAAmB,QAAO,4BAA2B;AAC7D,SAAQC,sBAAsB,QAAO,2CAA0C;AAE/E,MAAMC,oBAAoBR,SAAS;AAEnC,MAAMS,QAAQ;IACZ,GAAGH,oBAAoB;QACrBI,aAAa;QACbC,WAAW;IACb,EAAE;IACFC,QAAQhB,MAAMiB,MAAM,CAA0B;QAC5CH,aAAa;QACbI,WAAW;QACXC,OAAO,OAAOC;YACZ,IAAIC;YACJ,IAAI;gBACFA,SAASC,KAAKH,KAAK,CAACC;YACtB,EAAE,OAAM;gBACN,MAAM,IAAIG,MAAM;YAClB;YACA,IAAI,CAACjB,SAASe,SAAS;gBACrB,MAAM,IAAIE,MAAM;YAClB;YACA,OAAOF;QACT;IACF;AACF;AAEA,OAAO,MAAMG,4BAA4BrB;IACvC,OAAgBsB,OAAO;QACrBC,OAAO3B,KAAK4B,MAAM,CAAC;YACjBb,aAAa;YACbc,UAAU;QACZ;IACF,EAAC;IAED,OAAgBd,cAAc,sDAAqD;IAEnF,OAAgBe,iBAAiB,KAAI;IAErC,OAAgBC,WAAW;QACzB;YACEC,SACE;YACFjB,aAAa;QACf;QACA;YACEiB,SACE;YACFjB,aAAa;QACf;KACD,CAAA;IAED,OAAgBD,QAAQA,MAAK;IAE7B,OAAOmB,YAAYrB,uBAAuBE,OAAO;QAACoB,QAAQ;YAAC;SAAS;IAAA,GAAE;IAEtE,MAAaC,MAAqB;QAChC,MAAM,EAACR,KAAK,EAAC,GAAG,IAAI,CAACD,IAAI;QACzB,MAAM,EAACU,IAAI,EAAEC,YAAY,EAAEpB,MAAM,EAAC,GAAG,IAAI,CAACH,KAAK;QAE/C,IAAIwB;QACJ,IAAI;YACFA,iBAAiB,MAAM7B,sBAAsB;gBAC3C8B,0BAA0B,AAAC,CAAA,MAAM,IAAI,CAACC,eAAe,EAAC,EAAGC,GAAG,EAAEH;gBAC9DI,oBAAoBL;gBACpBM,YAAY,IAAI,CAACC,YAAY;YAC/B;QACF,EAAE,OAAOC,OAAO;YACdhC,kBAAkB,gCAAgCgC;YAClD,IAAI,CAACA,KAAK,CAACvC,gBAAgBuC,QAAQ;gBACjCC,MACED,iBAAiBrC,2BACbL,UAAU4C,WAAW,GACrB5C,UAAU6C,aAAa;YAC/B;QACF;QAEA,IAAIC;QACJ,IAAI;YACFA,SAAS,MAAMvC,aAAa4B,gBAAgBX,OAAOV;QACrD,EAAE,OAAO4B,OAAO;YACdhC,kBAAkB,uBAAuBgC;YACzC,IAAI,CAACA,KAAK,CAAC,CAAC,qBAAqB,EAAEvC,gBAAgBuC,QAAQ,EAAE;gBAC3DC,MAAM3C,UAAU6C,aAAa;YAC/B;QACF;QAEA,sFAAsF;QACtF,MAAME,SAASD,UAAU;QACzB,IAAIb,MAAM;YACR,IAAI,CAACe,OAAO,CAACD;YACb;QACF;QAEA,IAAI,CAACE,GAAG,CAAClD,aAAagD;IACxB;AACF"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -86,6 +86,12 @@ export class DeployCommand extends SanityCommand {
|
|
|
86
86
|
default: false,
|
|
87
87
|
description: 'Output the result as JSON'
|
|
88
88
|
}),
|
|
89
|
+
'media-library-id': Flags.string({
|
|
90
|
+
description: 'For a Media Library config deploy: the media library to sync the aspect definitions to. Defaults to the single active library of the configured organization; required when the organization has several',
|
|
91
|
+
// Part of the beta config-app flow, like the hidden media commands it
|
|
92
|
+
// pairs with. A help-listed flag is hard to take back — unhide at GA.
|
|
93
|
+
hidden: true
|
|
94
|
+
}),
|
|
89
95
|
minify: Flags.boolean({
|
|
90
96
|
allowNo: true,
|
|
91
97
|
default: true,
|
|
@@ -120,6 +126,13 @@ export class DeployCommand extends SanityCommand {
|
|
|
120
126
|
const cliConfig = await this.getCliConfig();
|
|
121
127
|
const projectRoot = await this.getProjectRoot();
|
|
122
128
|
const isApp = determineIsApp(cliConfig);
|
|
129
|
+
// `--media-library-id` only steers the aspect-document sync of a Media
|
|
130
|
+
// Library config deploy (see deployApp); studio and app deploys never read
|
|
131
|
+
// it. Warned rather than silently dropped (same courtesy init gives
|
|
132
|
+
// --dashboard on the media template).
|
|
133
|
+
if (flags['media-library-id'] && !resolveWorkbenchConfig(cliConfig)) {
|
|
134
|
+
this.output.warn('--media-library-id is ignored — it only applies when deploying a Media Library config');
|
|
135
|
+
}
|
|
123
136
|
if (flags.create) {
|
|
124
137
|
if (!isApp) {
|
|
125
138
|
return this.output.error('--create is only supported for App SDK apps. For a studio, use --url <hostname>.');
|
|
@@ -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'\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 enableJsonFlag = true\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 %> --external --url https://apps.example.com',\n description: 'Register an externally hosted studio, served from the provided origin',\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'],\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 deploys, 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","enableJsonFlag","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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"}
|
|
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 enableJsonFlag = true\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 %> --external --url https://apps.example.com',\n description: 'Register an externally hosted studio, served from the provided origin',\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'],\n }),\n json: Flags.boolean({\n char: 'j',\n default: false,\n description: 'Output the result as JSON',\n }),\n 'media-library-id': Flags.string({\n description:\n 'For a Media Library config deploy: the media library to sync the aspect definitions to. Defaults to the single active library of the configured organization; required when the organization has several',\n // Part of the beta config-app flow, like the hidden media commands it\n // pairs with. A help-listed flag is hard to take back — unhide at GA.\n hidden: true,\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 deploys, 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 // `--media-library-id` only steers the aspect-document sync of a Media\n // Library config deploy (see deployApp); studio and app deploys never read\n // it. Warned rather than silently dropped (same courtesy init gives\n // --dashboard on the media template).\n if (flags['media-library-id'] && !resolveWorkbenchConfig(cliConfig)) {\n this.output.warn(\n '--media-library-id is ignored — it only applies when deploying a Media Library config',\n )\n }\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","enableJsonFlag","examples","command","flags","boolean","allowNo","deprecated","build","default","create","exclusive","external","json","char","string","hidden","minify","title","url","verbose","yes","run","parse","cliConfig","getCliConfig","projectRoot","getProjectRoot","isApp","output","warn","error","defaultOutputDir","resolve","join","process","cwd","relativeOutput","relative","isUnattended","isEmpty","shouldProceed","message","exit","RUNTIME_ERROR","log","deployFlags"],"mappings":"AAAA,OAAOA,UAAU,YAAW;AAE5B,SAAQC,IAAI,EAAEC,KAAK,QAAO,cAAa;AACvC,SAAQC,SAAS,EAAEC,aAAa,QAAO,mBAAkB;AACzD,SAAQC,OAAO,QAAO,sBAAqB;AAC3C,SAAQC,YAAY,EAAEC,sBAAsB,QAAO,+BAA8B;AAEjF,SAAQC,SAAS,QAAO,iCAAgC;AACxD,SAAQC,WAAW,QAAO,mCAAkC;AAC5D,SAAQC,YAAY,QAAO,oCAAmC;AAC9D,SAAQC,QAAQ,QAAO,mBAAkB;AACzC,SAAQC,cAAc,QAAO,4BAA2B;AACxD,SAAQC,uBAAuB,QAAO,qCAAoC;AAE1E,OAAO,MAAMC,sBAAsBV;IACjC,OAAgBW,OAAO;QACrBC,WAAWf,KAAKgB,SAAS,CAAC;YACxBC,aAAa;QACf;IACF,EAAC;IAED,OAAgBA,cAAc,oEAAmE;IAEjG,OAAgBC,iBAAiB,KAAI;IAErC,OAAgBC,WAAW;QACzB;YACEC,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aACE;QACJ;QACA;YACEG,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aACE;QACJ;QACA;YACEG,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aAAa;QACf;KACD,CAAA;IAED,OAAgBI,QAAQ;QACtB,gBAAgBpB,MAAMqB,OAAO,CAAC;YAC5BC,SAAS;YACTC,YAAY;YACZP,aAAa;QACf;QACAQ,OAAOxB,MAAMqB,OAAO,CAAC;YACnBC,SAAS;YACTG,SAAS;YACTT,aACE;QACJ;QACAU,QAAQ1B,MAAMqB,OAAO,CAAC;YACpBL,aAAa;YACbW,WAAW;gBAAC;gBAAY;aAAM;QAChC;QACA,WAAW3B,MAAMqB,OAAO,CAAC;YACvBI,SAAS;YACTT,aAAa;QACf;QACAY,UAAU5B,MAAMqB,OAAO,CAAC;YACtBI,SAAS;YACTT,aAAa;YACbW,WAAW;gBAAC;gBAAe;aAAS;QACtC;QACAE,MAAM7B,MAAMqB,OAAO,CAAC;YAClBS,MAAM;YACNL,SAAS;YACTT,aAAa;QACf;QACA,oBAAoBhB,MAAM+B,MAAM,CAAC;YAC/Bf,aACE;YACF,sEAAsE;YACtE,sEAAsE;YACtEgB,QAAQ;QACV;QACAC,QAAQjC,MAAMqB,OAAO,CAAC;YACpBC,SAAS;YACTG,SAAS;YACTT,aAAa;QACf;QACA,mBAAmBhB,MAAMqB,OAAO,CAAC;YAC/BI,SAAS;YACTT,aAAa;QACf;QACA,eAAehB,MAAMqB,OAAO,CAAC;YAC3BI,SAAS;YACTT,aAAa;QACf;QACAkB,OAAOlC,MAAM+B,MAAM,CAAC;YAClBf,aACE;QACJ;QACAmB,KAAKnC,MAAM+B,MAAM,CAAC;YAChBf,aACE;QACJ;QACAoB,SAASpC,MAAMqB,OAAO,CAAC;YACrBI,SAAS;YACTT,aAAa;QACf;QACAqB,KAAKrC,MAAMqB,OAAO,CAAC;YACjBS,MAAM;YACNL,SAAS;YACTT,aACE;QACJ;IACF,EAAC;IAED,MAAasB,MAAqB;QAChC,MAAM,EAAClB,KAAK,EAAC,GAAG,MAAM,IAAI,CAACmB,KAAK,CAAC3B;QAEjC,MAAM4B,YAAY,MAAM,IAAI,CAACC,YAAY;QACzC,MAAMC,cAAc,MAAM,IAAI,CAACC,cAAc;QAE7C,MAAMC,QAAQlC,eAAe8B;QAE7B,uEAAuE;QACvE,2EAA2E;QAC3E,oEAAoE;QACpE,sCAAsC;QACtC,IAAIpB,KAAK,CAAC,mBAAmB,IAAI,CAACf,uBAAuBmC,YAAY;YACnE,IAAI,CAACK,MAAM,CAACC,IAAI,CACd;QAEJ;QAEA,IAAI1B,MAAMM,MAAM,EAAE;YAChB,IAAI,CAACkB,OAAO;gBACV,OAAO,IAAI,CAACC,MAAM,CAACE,KAAK,CACtB;YAEJ;YACA,IAAItC,SAAS+B,YAAY;gBACvB,OAAO,IAAI,CAACK,MAAM,CAACE,KAAK,CACtB;YAEJ;YACA,IAAI3C,aAAaoC,cAAcnC,uBAAuBmC,YAAY;gBAChE,OAAO,IAAI,CAACK,MAAM,CAACE,KAAK,CACtB;YAEJ;QACF;QAEA,MAAMC,mBAAmBlD,KAAKmD,OAAO,CAACnD,KAAKoD,IAAI,CAACR,YAAY3B,SAAS,EAAE;QACvE,MAAMD,YAAYhB,KAAKmD,OAAO,CAACE,QAAQC,GAAG,IAAI,IAAI,CAACvC,IAAI,CAACC,SAAS,IAAIkC;QAErE,8DAA8D;QAC9D,IAAI,IAAI,CAACnC,IAAI,CAACC,SAAS,IAAI,IAAI,CAACD,IAAI,CAACC,SAAS,KAAK,UAAU,CAACM,MAAMQ,QAAQ,EAAE;YAC5E,IAAIyB,iBAAiBvD,KAAKwD,QAAQ,CAACH,QAAQC,GAAG,IAAItC;YAClD,IAAIuC,cAAc,CAAC,EAAE,KAAK,KAAK;gBAC7BA,iBAAiB,CAAC,EAAE,EAAEA,gBAAgB;YACxC;YAEA,IAAI,CAAC,IAAI,CAACE,YAAY,MAAM,CAACnC,KAAK,CAAC,UAAU,EAAE;gBAC7C,MAAMoC,UAAU,MAAM7C,wBAAwBG;gBAC9C,MAAM2C,gBACJD,WACC,MAAMrD,QAAQ;oBACbsB,SAAS;oBACTiC,SAAS,CAAC,CAAC,EAAEL,eAAe,uCAAuC,CAAC;gBACtE;gBAEF,IAAI,CAACI,eAAe;oBAClB,IAAI,CAACZ,MAAM,CAACE,KAAK,CAAC,cAAc;wBAACY,MAAM1D,UAAU2D,aAAa;oBAAA;gBAChE;YACF;YAEA,6CAA6C;YAC7C,IAAI,CAACxC,MAAMS,IAAI,EAAE,IAAI,CAACgB,MAAM,CAACgB,GAAG,CAAC,CAAC,YAAY,EAAER,eAAe,EAAE,CAAC;QACpE;QAEA,8FAA8F;QAC9F,MAAMS,cAAc,IAAI,CAACP,YAAY,MAAMnC,KAAK,CAAC,UAAU,GAAG;YAAC,GAAGA,KAAK;YAAEiB,KAAK;QAAI,IAAIjB;QAEtF,IAAIwB,OAAO;YACTrC,YAAY;YACZ,MAAMD,UAAU;gBACdkC;gBACApB,OAAO0C;gBACPjB,QAAQ,IAAI,CAACA,MAAM;gBACnBH;gBACA5B;YACF;QACF,OAAO;YACLP,YAAY;YACZ,MAAMC,aAAa;gBACjBgC;gBACApB,OAAO0C;gBACPjB,QAAQ,IAAI,CAACA,MAAM;gBACnBH;gBACA5B;YACF;QACF;IACF;AACF"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { exitCodes, ProjectRootNotFoundError, SanityCommand, subdebug } from '@sanity/cli-core';
|
|
4
|
+
import { getErrorMessage } from '@sanity/cli-core/errors';
|
|
5
|
+
import { logSymbols, spinner } from '@sanity/cli-core/ux';
|
|
6
|
+
import { resolveWorkbenchConfig } from '@sanity/workbench-cli/deploy';
|
|
7
|
+
import { VALID_ASPECT_NAME } from '../../actions/media/aspectName.js';
|
|
8
|
+
import { resolveMediaLibraryId } from '../../actions/media/resolveMediaLibraryId.js';
|
|
9
|
+
import { getAspectDataCarrierIds, getStoredAspectIds, unsetAspectData } from '../../services/mediaLibraries.js';
|
|
10
|
+
import { pluralize } from '../../util/pluralize.js';
|
|
11
|
+
const pruneAspectDataDebug = subdebug('media:prune-aspect-data');
|
|
12
|
+
const NOT_A_CONFIG_APP = 'This command must be run inside a Media Library config app — a project whose ' + '`sanity.cli.ts` declares `unstable_defineMediaLibrary(...)`. Scaffold one with ' + '`sanity init --template media-library-config`.';
|
|
13
|
+
/** Unset-patch batch size per mutate request. */ const BATCH_SIZE = 100;
|
|
14
|
+
/** How many carrier ids the report lists before truncating. */ const SAMPLE_SIZE = 10;
|
|
15
|
+
/**
|
|
16
|
+
* The deliberate, explicitly-invoked counterpart to the deploy-time aspect
|
|
17
|
+
* sync: deploying a config only mirrors aspect *definitions* — `aspects.<name>`
|
|
18
|
+
* values on assets are never touched and resurface if a matching field comes
|
|
19
|
+
* back. This command removes that data. Irreversible fan-out mutation, so it
|
|
20
|
+
* never asks for confirmation: without `--yes` it only reports what would change.
|
|
21
|
+
*/ export class MediaPruneAspectDataCommand extends SanityCommand {
|
|
22
|
+
static args = {
|
|
23
|
+
aspectName: Args.string({
|
|
24
|
+
description: 'Name of the aspect whose data to remove from all assets',
|
|
25
|
+
required: true
|
|
26
|
+
})
|
|
27
|
+
};
|
|
28
|
+
static description = "Irreversibly remove an aspect's data (aspects.<name> values) from every asset of a media library";
|
|
29
|
+
static examples = [
|
|
30
|
+
{
|
|
31
|
+
command: '<%= config.bin %> <%= command.id %> legacyAspect',
|
|
32
|
+
description: 'Report which assets carry aspects.legacyAspect (changes nothing)'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
command: '<%= config.bin %> <%= command.id %> legacyAspect --yes',
|
|
36
|
+
description: 'Remove aspects.legacyAspect from every asset that carries it'
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
static flags = {
|
|
40
|
+
'media-library-id': Flags.string({
|
|
41
|
+
description: 'The id of the media library to prune (defaults to the single active library of the configured organization)'
|
|
42
|
+
}),
|
|
43
|
+
yes: Flags.boolean({
|
|
44
|
+
char: 'y',
|
|
45
|
+
description: 'Execute the prune. Without this flag the command only reports what would change'
|
|
46
|
+
})
|
|
47
|
+
};
|
|
48
|
+
// Part of the beta config-app flow: the command requires a config app to
|
|
49
|
+
// resolve its organization, so listing it in the general help would only
|
|
50
|
+
// route legacy-flow users into the beta paradigm. The prune itself is
|
|
51
|
+
// paradigm-neutral (it only unsets asset data) — when there is demand from
|
|
52
|
+
// the legacy flow, ungate it from the config app and unhide it.
|
|
53
|
+
static hidden = true;
|
|
54
|
+
async run() {
|
|
55
|
+
const { aspectName } = this.args;
|
|
56
|
+
const { 'media-library-id': mediaLibraryIdFlag, yes = false } = this.flags;
|
|
57
|
+
// The name rule shared with the sync contract; also makes the
|
|
58
|
+
// interpolation into the carrier GROQ filter injection-safe.
|
|
59
|
+
if (!VALID_ASPECT_NAME.test(aspectName)) {
|
|
60
|
+
this.error(`"${aspectName}" is not a valid aspect name (${VALID_ASPECT_NAME})`, {
|
|
61
|
+
exit: exitCodes.USAGE_ERROR
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
let cliConfig;
|
|
65
|
+
try {
|
|
66
|
+
cliConfig = await this.getCliConfig();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof ProjectRootNotFoundError) {
|
|
69
|
+
this.error(NOT_A_CONFIG_APP, {
|
|
70
|
+
exit: exitCodes.RUNTIME_ERROR
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
const config = resolveWorkbenchConfig(cliConfig);
|
|
76
|
+
if (!config) {
|
|
77
|
+
this.error(NOT_A_CONFIG_APP, {
|
|
78
|
+
exit: exitCodes.RUNTIME_ERROR
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
// Not `isUnattended()`: that treats `--yes` as unattended, but here
|
|
83
|
+
// `--yes` consents to the prune, not to skipping prompts — the report
|
|
84
|
+
// path literally instructs "Re-run with --yes", and that run must still
|
|
85
|
+
// offer the library picker in a multi-library organization. Only a
|
|
86
|
+
// genuinely non-interactive run (CI, piped stdin) must not prompt.
|
|
87
|
+
const mediaLibraryId = await resolveMediaLibraryId({
|
|
88
|
+
mediaLibraryIdFlag,
|
|
89
|
+
organizationId: config.organizationId,
|
|
90
|
+
unattended: !this.resolveIsInteractive()
|
|
91
|
+
});
|
|
92
|
+
const spin = spinner('Finding assets that carry the aspect').start();
|
|
93
|
+
let storedIds;
|
|
94
|
+
let carriers;
|
|
95
|
+
try {
|
|
96
|
+
storedIds = await getStoredAspectIds(mediaLibraryId, 'prune-aspect-data');
|
|
97
|
+
carriers = await getAspectDataCarrierIds(mediaLibraryId, aspectName);
|
|
98
|
+
} finally{
|
|
99
|
+
// Also on failure — a spinner left running garbles the error output.
|
|
100
|
+
spin.stop();
|
|
101
|
+
}
|
|
102
|
+
// Guardrails, not gates: pruning while either is true can be deliberate
|
|
103
|
+
// (e.g. clearing data before re-shaping a field), so warn and continue.
|
|
104
|
+
if (config.fields.some((field)=>field.name === aspectName)) {
|
|
105
|
+
this.warn(`"${aspectName}" is still declared in sanity.cli.ts — its data is likely in active use and will grow back.`);
|
|
106
|
+
}
|
|
107
|
+
if (storedIds.includes(aspectName)) {
|
|
108
|
+
this.warn(`Aspect definition "${aspectName}" still exists in the library — this only removes data, not the definition.`);
|
|
109
|
+
}
|
|
110
|
+
if (carriers.length === 0) {
|
|
111
|
+
this.log(`No assets in media library ${styleText('bold', mediaLibraryId)} carry ${styleText('bold', `aspects.${aspectName}`)} — nothing to prune.`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const sample = carriers.slice(0, SAMPLE_SIZE);
|
|
115
|
+
// "documents", not "assets": under the raw perspective drafts and
|
|
116
|
+
// release versions are separate ids, so an asset count would overstate.
|
|
117
|
+
this.log(`${styleText('bold', String(carriers.length))} ${pluralize('document', carriers.length)} in media library ${styleText('bold', mediaLibraryId)} ${carriers.length === 1 ? 'carries' : 'carry'} ${styleText('bold', `aspects.${aspectName}`)}:`);
|
|
118
|
+
for (const id of sample){
|
|
119
|
+
this.log(` - ${id}`);
|
|
120
|
+
}
|
|
121
|
+
if (carriers.length > sample.length) {
|
|
122
|
+
this.log(styleText('dim', ` … and ${carriers.length - sample.length} more`));
|
|
123
|
+
}
|
|
124
|
+
if (!yes) {
|
|
125
|
+
this.log('');
|
|
126
|
+
this.log(`This would ${styleText('bold', 'irreversibly')} unset aspects.${aspectName} on all of them. Re-run with ${styleText('bold', '--yes')} to execute.`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let pruned = 0;
|
|
130
|
+
for(let offset = 0; offset < carriers.length; offset += BATCH_SIZE){
|
|
131
|
+
const batch = carriers.slice(offset, offset + BATCH_SIZE);
|
|
132
|
+
try {
|
|
133
|
+
await unsetAspectData({
|
|
134
|
+
aspectName,
|
|
135
|
+
assetIds: batch,
|
|
136
|
+
mediaLibraryId
|
|
137
|
+
});
|
|
138
|
+
} catch (batchError) {
|
|
139
|
+
// The progress made so far must survive into the error: re-running
|
|
140
|
+
// is safe (pruned assets no longer match the carrier query), so the
|
|
141
|
+
// message says how far it got and that a retry finishes the rest.
|
|
142
|
+
throw new Error(`Removed aspects.${aspectName} from ${pruned} of ${carriers.length} ${pluralize('document', carriers.length)} before a batch failed: ${getErrorMessage(batchError)}\n` + `Re-running the command with --yes is safe — already-pruned documents no longer ` + `match and only the remaining ${carriers.length - pruned} will be mutated.`, {
|
|
143
|
+
cause: batchError
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
pruned += batch.length;
|
|
147
|
+
this.log(styleText('dim', ` pruned ${pruned}/${carriers.length}`));
|
|
148
|
+
}
|
|
149
|
+
this.log('');
|
|
150
|
+
this.log(`${logSymbols.success} aspects.${aspectName} removed from ${pruned} ${pluralize('document', pruned)}.`);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const err = error;
|
|
153
|
+
// Ctrl+C in the library prompt and errors that already carry their exit
|
|
154
|
+
// story (`this.error`, CLIError from the resolver) pass through to
|
|
155
|
+
// `SanityCommand.catch` instead of being re-wrapped here.
|
|
156
|
+
if (err.name === 'ExitPromptError' || 'oclif' in err) {
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
pruneAspectDataDebug('Failed to prune aspect data', {
|
|
160
|
+
aspectName,
|
|
161
|
+
error: err
|
|
162
|
+
});
|
|
163
|
+
this.error(styleText('bold', 'Failed to prune aspect data') + `\n\n${styleText('red', err.message)}`, {
|
|
164
|
+
exit: exitCodes.RUNTIME_ERROR
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//# sourceMappingURL=prune-aspect-data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/media/prune-aspect-data.ts"],"sourcesContent":["import {styleText} from 'node:util'\n\nimport {Args, Flags} from '@oclif/core'\nimport {\n type CliConfig,\n exitCodes,\n ProjectRootNotFoundError,\n SanityCommand,\n subdebug,\n} from '@sanity/cli-core'\nimport {getErrorMessage} from '@sanity/cli-core/errors'\nimport {logSymbols, spinner} from '@sanity/cli-core/ux'\nimport {resolveWorkbenchConfig} from '@sanity/workbench-cli/deploy'\n\nimport {VALID_ASPECT_NAME} from '../../actions/media/aspectName.js'\nimport {resolveMediaLibraryId} from '../../actions/media/resolveMediaLibraryId.js'\nimport {\n getAspectDataCarrierIds,\n getStoredAspectIds,\n unsetAspectData,\n} from '../../services/mediaLibraries.js'\nimport {pluralize} from '../../util/pluralize.js'\n\nconst pruneAspectDataDebug = subdebug('media:prune-aspect-data')\n\nconst NOT_A_CONFIG_APP =\n 'This command must be run inside a Media Library config app — a project whose ' +\n '`sanity.cli.ts` declares `unstable_defineMediaLibrary(...)`. Scaffold one with ' +\n '`sanity init --template media-library-config`.'\n\n/** Unset-patch batch size per mutate request. */\nconst BATCH_SIZE = 100\n\n/** How many carrier ids the report lists before truncating. */\nconst SAMPLE_SIZE = 10\n\n/**\n * The deliberate, explicitly-invoked counterpart to the deploy-time aspect\n * sync: deploying a config only mirrors aspect *definitions* — `aspects.<name>`\n * values on assets are never touched and resurface if a matching field comes\n * back. This command removes that data. Irreversible fan-out mutation, so it\n * never asks for confirmation: without `--yes` it only reports what would change.\n */\nexport class MediaPruneAspectDataCommand extends SanityCommand<typeof MediaPruneAspectDataCommand> {\n static override args = {\n aspectName: Args.string({\n description: 'Name of the aspect whose data to remove from all assets',\n required: true,\n }),\n }\n\n static override description =\n \"Irreversibly remove an aspect's data (aspects.<name> values) from every asset of a media library\"\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %> legacyAspect',\n description: 'Report which assets carry aspects.legacyAspect (changes nothing)',\n },\n {\n command: '<%= config.bin %> <%= command.id %> legacyAspect --yes',\n description: 'Remove aspects.legacyAspect from every asset that carries it',\n },\n ]\n\n static override flags = {\n 'media-library-id': Flags.string({\n description:\n 'The id of the media library to prune (defaults to the single active library of the configured organization)',\n }),\n yes: Flags.boolean({\n char: 'y',\n description:\n 'Execute the prune. Without this flag the command only reports what would change',\n }),\n }\n\n // Part of the beta config-app flow: the command requires a config app to\n // resolve its organization, so listing it in the general help would only\n // route legacy-flow users into the beta paradigm. The prune itself is\n // paradigm-neutral (it only unsets asset data) — when there is demand from\n // the legacy flow, ungate it from the config app and unhide it.\n static override hidden = true\n\n public async run(): Promise<void> {\n const {aspectName} = this.args\n const {'media-library-id': mediaLibraryIdFlag, yes = false} = this.flags\n\n // The name rule shared with the sync contract; also makes the\n // interpolation into the carrier GROQ filter injection-safe.\n if (!VALID_ASPECT_NAME.test(aspectName)) {\n this.error(`\"${aspectName}\" is not a valid aspect name (${VALID_ASPECT_NAME})`, {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n\n let cliConfig: CliConfig\n try {\n cliConfig = await this.getCliConfig()\n } catch (err) {\n if (err instanceof ProjectRootNotFoundError) {\n this.error(NOT_A_CONFIG_APP, {exit: exitCodes.RUNTIME_ERROR})\n }\n throw err\n }\n\n const config = resolveWorkbenchConfig(cliConfig)\n if (!config) {\n this.error(NOT_A_CONFIG_APP, {exit: exitCodes.RUNTIME_ERROR})\n }\n\n try {\n // Not `isUnattended()`: that treats `--yes` as unattended, but here\n // `--yes` consents to the prune, not to skipping prompts — the report\n // path literally instructs \"Re-run with --yes\", and that run must still\n // offer the library picker in a multi-library organization. Only a\n // genuinely non-interactive run (CI, piped stdin) must not prompt.\n const mediaLibraryId = await resolveMediaLibraryId({\n mediaLibraryIdFlag,\n organizationId: config.organizationId,\n unattended: !this.resolveIsInteractive(),\n })\n\n const spin = spinner('Finding assets that carry the aspect').start()\n let storedIds: string[]\n let carriers: string[]\n try {\n storedIds = await getStoredAspectIds(mediaLibraryId, 'prune-aspect-data')\n carriers = await getAspectDataCarrierIds(mediaLibraryId, aspectName)\n } finally {\n // Also on failure — a spinner left running garbles the error output.\n spin.stop()\n }\n\n // Guardrails, not gates: pruning while either is true can be deliberate\n // (e.g. clearing data before re-shaping a field), so warn and continue.\n if (config.fields.some((field) => field.name === aspectName)) {\n this.warn(\n `\"${aspectName}\" is still declared in sanity.cli.ts — its data is likely in active use and will grow back.`,\n )\n }\n if (storedIds.includes(aspectName)) {\n this.warn(\n `Aspect definition \"${aspectName}\" still exists in the library — this only removes data, not the definition.`,\n )\n }\n\n if (carriers.length === 0) {\n this.log(\n `No assets in media library ${styleText('bold', mediaLibraryId)} carry ${styleText(\n 'bold',\n `aspects.${aspectName}`,\n )} — nothing to prune.`,\n )\n return\n }\n\n const sample = carriers.slice(0, SAMPLE_SIZE)\n // \"documents\", not \"assets\": under the raw perspective drafts and\n // release versions are separate ids, so an asset count would overstate.\n this.log(\n `${styleText('bold', String(carriers.length))} ${pluralize(\n 'document',\n carriers.length,\n )} in media library ${styleText('bold', mediaLibraryId)} ${\n carriers.length === 1 ? 'carries' : 'carry'\n } ${styleText('bold', `aspects.${aspectName}`)}:`,\n )\n for (const id of sample) {\n this.log(` - ${id}`)\n }\n if (carriers.length > sample.length) {\n this.log(styleText('dim', ` … and ${carriers.length - sample.length} more`))\n }\n\n if (!yes) {\n this.log('')\n this.log(\n `This would ${styleText(\n 'bold',\n 'irreversibly',\n )} unset aspects.${aspectName} on all of them. Re-run with ${styleText(\n 'bold',\n '--yes',\n )} to execute.`,\n )\n return\n }\n\n let pruned = 0\n for (let offset = 0; offset < carriers.length; offset += BATCH_SIZE) {\n const batch = carriers.slice(offset, offset + BATCH_SIZE)\n try {\n await unsetAspectData({aspectName, assetIds: batch, mediaLibraryId})\n } catch (batchError) {\n // The progress made so far must survive into the error: re-running\n // is safe (pruned assets no longer match the carrier query), so the\n // message says how far it got and that a retry finishes the rest.\n throw new Error(\n `Removed aspects.${aspectName} from ${pruned} of ${carriers.length} ${pluralize(\n 'document',\n carriers.length,\n )} before a batch failed: ${getErrorMessage(batchError)}\\n` +\n `Re-running the command with --yes is safe — already-pruned documents no longer ` +\n `match and only the remaining ${carriers.length - pruned} will be mutated.`,\n {cause: batchError},\n )\n }\n pruned += batch.length\n this.log(styleText('dim', ` pruned ${pruned}/${carriers.length}`))\n }\n\n this.log('')\n this.log(\n `${logSymbols.success} aspects.${aspectName} removed from ${pruned} ${pluralize(\n 'document',\n pruned,\n )}.`,\n )\n } catch (error) {\n const err = error as Error\n // Ctrl+C in the library prompt and errors that already carry their exit\n // story (`this.error`, CLIError from the resolver) pass through to\n // `SanityCommand.catch` instead of being re-wrapped here.\n if (err.name === 'ExitPromptError' || 'oclif' in err) {\n throw err\n }\n pruneAspectDataDebug('Failed to prune aspect data', {\n aspectName,\n error: err,\n })\n this.error(\n styleText('bold', 'Failed to prune aspect data') + `\\n\\n${styleText('red', err.message)}`,\n {exit: exitCodes.RUNTIME_ERROR},\n )\n }\n }\n}\n"],"names":["styleText","Args","Flags","exitCodes","ProjectRootNotFoundError","SanityCommand","subdebug","getErrorMessage","logSymbols","spinner","resolveWorkbenchConfig","VALID_ASPECT_NAME","resolveMediaLibraryId","getAspectDataCarrierIds","getStoredAspectIds","unsetAspectData","pluralize","pruneAspectDataDebug","NOT_A_CONFIG_APP","BATCH_SIZE","SAMPLE_SIZE","MediaPruneAspectDataCommand","args","aspectName","string","description","required","examples","command","flags","yes","boolean","char","hidden","run","mediaLibraryIdFlag","test","error","exit","USAGE_ERROR","cliConfig","getCliConfig","err","RUNTIME_ERROR","config","mediaLibraryId","organizationId","unattended","resolveIsInteractive","spin","start","storedIds","carriers","stop","fields","some","field","name","warn","includes","length","log","sample","slice","String","id","pruned","offset","batch","assetIds","batchError","Error","cause","success","message"],"mappings":"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"}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { styleText } from 'node:util';
|
|
4
|
+
import { Flags } from '@oclif/core';
|
|
5
|
+
import { exitCodes, ProjectRootNotFoundError, SanityCommand, subdebug } from '@sanity/cli-core';
|
|
6
|
+
import { confirm, logSymbols, spinner } from '@sanity/cli-core/ux';
|
|
7
|
+
import { resolveWorkbenchConfig } from '@sanity/workbench-cli/deploy';
|
|
8
|
+
import { appendFieldEntries } from '../../actions/media/cliConfigFields.js';
|
|
9
|
+
import { convertAspectDocuments, formatFieldEntries } from '../../actions/media/pullAspects.js';
|
|
10
|
+
import { resolveMediaLibraryId } from '../../actions/media/resolveMediaLibraryId.js';
|
|
11
|
+
import { getAspects } from '../../services/mediaLibraries.js';
|
|
12
|
+
import { pluralize } from '../../util/pluralize.js';
|
|
13
|
+
import { defineCommandTelemetry } from '../../util/telemetry/commandTelemetry.js';
|
|
14
|
+
const pullAspectsDebug = subdebug('media:pull-aspects');
|
|
15
|
+
const NOT_A_CONFIG_APP = 'This command must be run inside a Media Library config app — a project whose ' + '`sanity.cli.ts` declares `unstable_defineMediaLibrary(...)`. Scaffold one with ' + '`sanity init --template media-library-config`.';
|
|
16
|
+
const flags = {
|
|
17
|
+
'media-library-id': Flags.string({
|
|
18
|
+
description: 'The id of the media library to pull from (defaults to the single active library of the configured organization)'
|
|
19
|
+
}),
|
|
20
|
+
'out-dir': Flags.string({
|
|
21
|
+
default: 'src/fields',
|
|
22
|
+
description: 'Directory to write the generated field modules to, relative to the project root'
|
|
23
|
+
}),
|
|
24
|
+
overwrite: Flags.boolean({
|
|
25
|
+
description: 'Overwrite existing field modules and re-generate fields already declared in the config'
|
|
26
|
+
}),
|
|
27
|
+
'update-config': Flags.boolean({
|
|
28
|
+
allowNo: true,
|
|
29
|
+
description: 'Declare the generated fields in sanity.cli.ts (--no-update-config to skip). Prompted when omitted; unattended runs only print the entries'
|
|
30
|
+
})
|
|
31
|
+
};
|
|
32
|
+
export class MediaPullAspectsCommand extends SanityCommand {
|
|
33
|
+
static description = "Generate field modules from a media library's stored aspect documents";
|
|
34
|
+
static examples = [
|
|
35
|
+
{
|
|
36
|
+
command: '<%= config.bin %> <%= command.id %>',
|
|
37
|
+
description: 'Generate src/fields/<name>.tsx modules from the stored aspects'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
command: '<%= config.bin %> <%= command.id %> --media-library-id ml123 --overwrite',
|
|
41
|
+
description: 'Pull from a specific media library, overwriting existing modules'
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
static flags = flags;
|
|
45
|
+
// Part of the beta config-app flow: only usable inside a config app (like
|
|
46
|
+
// the unlisted media-library-config init template, which points to it), and
|
|
47
|
+
// listing it next to the legacy aspect commands would mix the two
|
|
48
|
+
// paradigms. Unhide when the config app leaves beta.
|
|
49
|
+
static hidden = true;
|
|
50
|
+
// --out-dir is a local filesystem path.
|
|
51
|
+
static telemetry = defineCommandTelemetry(flags, {
|
|
52
|
+
redact: [
|
|
53
|
+
'out-dir'
|
|
54
|
+
]
|
|
55
|
+
});
|
|
56
|
+
async run() {
|
|
57
|
+
const { 'media-library-id': mediaLibraryIdFlag, 'out-dir': outDir, overwrite = false } = this.flags;
|
|
58
|
+
let cliConfig;
|
|
59
|
+
try {
|
|
60
|
+
cliConfig = await this.getCliConfig();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err instanceof ProjectRootNotFoundError) {
|
|
63
|
+
this.error(NOT_A_CONFIG_APP, {
|
|
64
|
+
exit: exitCodes.RUNTIME_ERROR
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
const config = resolveWorkbenchConfig(cliConfig);
|
|
70
|
+
if (!config) {
|
|
71
|
+
this.error(NOT_A_CONFIG_APP, {
|
|
72
|
+
exit: exitCodes.RUNTIME_ERROR
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const root = await this.getProjectRoot();
|
|
76
|
+
// The generated `src:` entries and the ownership story both assume the
|
|
77
|
+
// modules live inside the project; a path escaping the root is a mistake.
|
|
78
|
+
// `outDirRelative` (not the raw flag) feeds the report so an absolute
|
|
79
|
+
// --out-dir inside the root still prints proper relative `src:` entries.
|
|
80
|
+
const outDirAbsolute = path.resolve(root.directory, outDir);
|
|
81
|
+
const outDirRelative = path.relative(root.directory, outDirAbsolute);
|
|
82
|
+
if (path.isAbsolute(outDirRelative) || outDirRelative === '..' || outDirRelative.startsWith(`..${path.sep}`)) {
|
|
83
|
+
this.error(`--out-dir must stay inside the project root (got "${outDir}")`, {
|
|
84
|
+
exit: exitCodes.USAGE_ERROR
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const mediaLibraryId = await resolveMediaLibraryId({
|
|
89
|
+
mediaLibraryIdFlag,
|
|
90
|
+
organizationId: config.organizationId,
|
|
91
|
+
unattended: this.isUnattended()
|
|
92
|
+
});
|
|
93
|
+
const spin = spinner('Fetching stored aspect documents').start();
|
|
94
|
+
let documents;
|
|
95
|
+
try {
|
|
96
|
+
documents = await getAspects(mediaLibraryId);
|
|
97
|
+
} finally{
|
|
98
|
+
// Also on failure — a spinner left running garbles the error output.
|
|
99
|
+
spin.stop();
|
|
100
|
+
}
|
|
101
|
+
if (documents.length === 0) {
|
|
102
|
+
this.log(`Media library ${styleText('bold', mediaLibraryId)} has no stored aspects — nothing to pull.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Names already declared in the config are owned by existing field
|
|
106
|
+
// modules; pulling the stored mirror back over them would regress local
|
|
107
|
+
// edits, so they are skipped unless --overwrite says otherwise.
|
|
108
|
+
const configuredNames = new Set(config.fields.map((field)=>field.name));
|
|
109
|
+
const configuredSrcByName = new Map(config.fields.map((field)=>[
|
|
110
|
+
field.name,
|
|
111
|
+
field.src
|
|
112
|
+
]));
|
|
113
|
+
// One module per field name; name collisions and invalid documents are skipped.
|
|
114
|
+
const { modules: winners, skipped } = convertAspectDocuments(documents);
|
|
115
|
+
const generated = [];
|
|
116
|
+
// Module name -> written path relative to the project root, so the
|
|
117
|
+
// report shows where each module actually landed.
|
|
118
|
+
const writtenPaths = new Map();
|
|
119
|
+
// Module name -> path of a module skipped because it already exists on
|
|
120
|
+
// disk. Those fields may still be undeclared and need registration.
|
|
121
|
+
const existingPaths = new Map();
|
|
122
|
+
for (const module of winners){
|
|
123
|
+
if (!overwrite && configuredNames.has(module.name)) {
|
|
124
|
+
skipped.push({
|
|
125
|
+
id: module.name,
|
|
126
|
+
reasons: [
|
|
127
|
+
'Already declared in sanity.cli.ts (pass --overwrite to re-generate)'
|
|
128
|
+
],
|
|
129
|
+
status: 'skipped',
|
|
130
|
+
validationErrors: []
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const defaultDestination = path.join(outDirAbsolute, module.filename);
|
|
135
|
+
// The pull generates `.tsx`, but a hand-written module for the same
|
|
136
|
+
// field may be plain `.ts` — treat both as taken.
|
|
137
|
+
const existing = await firstExisting([
|
|
138
|
+
defaultDestination,
|
|
139
|
+
defaultDestination.replace(/\.tsx$/, '.ts')
|
|
140
|
+
]);
|
|
141
|
+
if (!overwrite && existing) {
|
|
142
|
+
// The file is left alone, but the field is undeclared (declared
|
|
143
|
+
// names were skipped above) — so it still joins the registration
|
|
144
|
+
// offer below. An on-disk module without a config entry is exactly
|
|
145
|
+
// the state the next deploy punishes, however the file got there.
|
|
146
|
+
existingPaths.set(module.name, path.relative(root.directory, existing));
|
|
147
|
+
skipped.push({
|
|
148
|
+
id: module.name,
|
|
149
|
+
reasons: [
|
|
150
|
+
`A file already exists at ${path.relative(root.directory, existing)} (pass --overwrite to replace it)`
|
|
151
|
+
],
|
|
152
|
+
status: 'skipped',
|
|
153
|
+
validationErrors: []
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// When overwriting a field the config already declares, the module
|
|
158
|
+
// must land at the declared `src` path — writing <out-dir>/<name>.tsx
|
|
159
|
+
// instead would orphan the new file while deploy keeps evaluating the
|
|
160
|
+
// old one. Failing that, replace the module found on disk (which may
|
|
161
|
+
// be the .ts variant), and only then fall back to the default.
|
|
162
|
+
const declaredSrc = overwrite ? configuredSrcByName.get(module.name) : undefined;
|
|
163
|
+
const destination = declaredSrc ? await resolveDeclaredSrc(path.resolve(root.directory, declaredSrc), module.filename) : existing ?? defaultDestination;
|
|
164
|
+
await mkdir(path.dirname(destination), {
|
|
165
|
+
recursive: true
|
|
166
|
+
});
|
|
167
|
+
await writeFile(destination, module.source);
|
|
168
|
+
writtenPaths.set(module.name, path.relative(root.directory, destination));
|
|
169
|
+
generated.push(module);
|
|
170
|
+
}
|
|
171
|
+
// Fields present locally — written this run or already on disk — but not
|
|
172
|
+
// declared get offered for registration: the deploy mirrors config to
|
|
173
|
+
// stored documents, so an undeclared stored aspect is deleted by the
|
|
174
|
+
// next deploy — leaving the entries unpasted is a data hazard, not just
|
|
175
|
+
// friction. Existing files count because a prior pull may have been
|
|
176
|
+
// declined (or edited by hand): the file being there does not make the
|
|
177
|
+
// field declared.
|
|
178
|
+
const entryPaths = new Map([
|
|
179
|
+
...writtenPaths,
|
|
180
|
+
...existingPaths
|
|
181
|
+
]);
|
|
182
|
+
const newEntries = winners.filter((module)=>!configuredNames.has(module.name) && entryPaths.has(module.name));
|
|
183
|
+
const entryLines = newEntries.length > 0 ? formatFieldEntries(newEntries, outDirRelative, entryPaths).split('\n') : [];
|
|
184
|
+
const registration = newEntries.length > 0 ? await this.maybeRegisterFields({
|
|
185
|
+
configPath: root.path,
|
|
186
|
+
entryLines
|
|
187
|
+
}) : null;
|
|
188
|
+
this.report({
|
|
189
|
+
configuredNames,
|
|
190
|
+
entryLines,
|
|
191
|
+
generated,
|
|
192
|
+
newEntries,
|
|
193
|
+
outDir: outDirRelative,
|
|
194
|
+
registration,
|
|
195
|
+
skipped,
|
|
196
|
+
writtenPaths
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
// Ctrl+C on the library prompt and errors that already carry their exit
|
|
200
|
+
// code (`this.error` inside helpers) pass through to SanityCommand.catch.
|
|
201
|
+
if (error instanceof Error && error.name === 'ExitPromptError') throw error;
|
|
202
|
+
if (error && typeof error === 'object' && 'oclif' in error) throw error;
|
|
203
|
+
const err = error;
|
|
204
|
+
pullAspectsDebug('Failed to pull aspects', {
|
|
205
|
+
error: err,
|
|
206
|
+
mediaLibraryId: mediaLibraryIdFlag
|
|
207
|
+
});
|
|
208
|
+
this.error(styleText('bold', 'Failed to pull aspects') + `\n\n${styleText('red', err.message)}`, {
|
|
209
|
+
exit: exitCodes.RUNTIME_ERROR
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Offer to declare the generated fields in `sanity.cli.ts`. `--update-config`
|
|
215
|
+
* / `--no-update-config` decide outright; otherwise an interactive run
|
|
216
|
+
* prompts (default yes) and an unattended run leaves the config alone.
|
|
217
|
+
*/ async maybeRegisterFields({ configPath, entryLines }) {
|
|
218
|
+
const flag = this.flags['update-config'];
|
|
219
|
+
const shouldUpdate = flag ?? (this.isUnattended() ? false : await confirm({
|
|
220
|
+
default: true,
|
|
221
|
+
message: `Declare the ${entryLines.length} pulled ${pluralize('field', entryLines.length)} in sanity.cli.ts?`
|
|
222
|
+
}));
|
|
223
|
+
if (!shouldUpdate) return 'declined';
|
|
224
|
+
const source = await readFile(configPath, 'utf8');
|
|
225
|
+
const result = appendFieldEntries(source, entryLines);
|
|
226
|
+
if (!result.ok) return {
|
|
227
|
+
reason: result.reason
|
|
228
|
+
};
|
|
229
|
+
await writeFile(configPath, result.updated);
|
|
230
|
+
return 'added';
|
|
231
|
+
}
|
|
232
|
+
report({ configuredNames, entryLines, generated, newEntries, outDir, registration, skipped, writtenPaths }) {
|
|
233
|
+
if (skipped.length > 0) {
|
|
234
|
+
this.logToStderr('');
|
|
235
|
+
this.warn(styleText('bold', `Skipped ${skipped.length} ${pluralize('aspect', skipped.length)}`));
|
|
236
|
+
for (const aspect of skipped){
|
|
237
|
+
const errors = aspect.validationErrors.flatMap((group)=>group.map(({ message })=>message));
|
|
238
|
+
const reasons = [
|
|
239
|
+
...aspect.reasons,
|
|
240
|
+
...errors
|
|
241
|
+
];
|
|
242
|
+
this.logToStderr(` - ${aspect.id} ${styleText('dim', reasons.join('; '))}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// No modules written and nothing to declare: done. Skipped-but-undeclared
|
|
246
|
+
// fields keep the report alive — their registration outcome still matters.
|
|
247
|
+
if (generated.length === 0 && newEntries.length === 0) {
|
|
248
|
+
this.log('');
|
|
249
|
+
this.log('No field modules generated.');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (generated.length === 0) {
|
|
253
|
+
this.log('');
|
|
254
|
+
this.log('No field modules generated.');
|
|
255
|
+
} else {
|
|
256
|
+
this.log('');
|
|
257
|
+
this.log(`${logSymbols.success} ${styleText('bold', `Generated ${generated.length} field ${pluralize('module', generated.length)}`)}`);
|
|
258
|
+
for (const module of generated){
|
|
259
|
+
const location = writtenPaths.get(module.name) ?? path.join(outDir, module.filename);
|
|
260
|
+
this.log(` - ${module.name} ${styleText('dim', location)}`);
|
|
261
|
+
for (const warning of module.warnings){
|
|
262
|
+
this.log(` ${logSymbols.warning} ${warning}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (newEntries.length > 0 && registration === 'added') {
|
|
267
|
+
this.log('');
|
|
268
|
+
this.log(`${logSymbols.success} Declared ${newEntries.length} ${pluralize('field', newEntries.length)} in ${styleText('bold', 'sanity.cli.ts')}`);
|
|
269
|
+
} else if (newEntries.length > 0) {
|
|
270
|
+
if (registration !== null && typeof registration === 'object') {
|
|
271
|
+
this.warn(`Could not update sanity.cli.ts automatically: ${registration.reason}`);
|
|
272
|
+
}
|
|
273
|
+
this.log('');
|
|
274
|
+
this.log(configuredNames.size > 0 ? `Add the pulled ${pluralize('field', newEntries.length)} to the \`fields\` array in ${styleText('bold', 'sanity.cli.ts')}:` : `Declare the pulled ${pluralize('field', newEntries.length)} in ${styleText('bold', 'sanity.cli.ts')}:`);
|
|
275
|
+
this.log('');
|
|
276
|
+
this.log(entryLines.map((line)=>` ${line}`).join('\n'));
|
|
277
|
+
// The deploy mirrors config into stored documents; a pulled-but-undeclared
|
|
278
|
+
// field reads as stale there, so leaving the entries out is destructive.
|
|
279
|
+
this.warn('`sanity deploy` deletes the stored definition of any field not declared in sanity.cli.ts.');
|
|
280
|
+
}
|
|
281
|
+
this.log('');
|
|
282
|
+
this.log('Once declared in sanity.cli.ts, the config app owns these definitions — deploying the');
|
|
283
|
+
this.log('config overwrites the stored documents this pull read from. Stored definitions');
|
|
284
|
+
this.log('not declared in sanity.cli.ts are deleted on deploy.');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The write target for a declared `src` path. A declared path may omit the
|
|
289
|
+
* extension — the bundler and jiti both resolve `./src/fields/notes` to
|
|
290
|
+
* `notes.ts(x)` — so writing it literally would create an extensionless file
|
|
291
|
+
* next to the module deploy actually evaluates. Resolve to the existing
|
|
292
|
+
* module file; if none exists, append the generated module's extension.
|
|
293
|
+
*/ async function resolveDeclaredSrc(declaredPath, moduleFilename) {
|
|
294
|
+
if (path.extname(declaredPath) !== '') return declaredPath;
|
|
295
|
+
const existing = await firstExisting([
|
|
296
|
+
`${declaredPath}.ts`,
|
|
297
|
+
`${declaredPath}.tsx`
|
|
298
|
+
]);
|
|
299
|
+
return existing ?? `${declaredPath}${path.extname(moduleFilename)}`;
|
|
300
|
+
}
|
|
301
|
+
/** The first path in `paths` that exists, or `undefined`. */ async function firstExisting(paths) {
|
|
302
|
+
for (const filePath of paths){
|
|
303
|
+
const exists = await access(filePath).then(()=>true, ()=>false);
|
|
304
|
+
if (exists) return filePath;
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
//# sourceMappingURL=pull-aspects.js.map
|