@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/actions/init/project/getProjectDetails.ts"],"sourcesContent":["import {type SanityOrgUser, subdebug, type TelemetryUserProperties} from '@sanity/cli-core'\nimport {type TelemetryTrace} from '@sanity/telemetry'\n\nimport {listOrganizations, type ProjectOrganization} from '../../../services/organizations.js'\nimport {type InitStepResult} from '../../../telemetry/init.telemetry.js'\nimport {InitError} from '../initError.js'\nimport {type InitContext} from '../types.js'\nimport {getOrCreateDataset} from './getOrCreateDataset.js'\nimport {getOrCreateProject} from './getOrCreateProject.js'\nimport {promptForAppTemplateSetup} from './promptForAppTemplateSetup.js'\nimport {promptUserForOrganization} from './promptUserForOrganization.js'\n\nconst debug = subdebug('init')\n\nexport async function getProjectDetails({\n coupon,\n dataset,\n datasetDefault,\n isAppTemplate,\n newProject,\n organization,\n output,\n planId,\n project,\n showDefaultConfigPrompt,\n trace,\n unattended,\n user,\n visibility,\n}: {\n coupon: string | undefined\n dataset: string | undefined\n datasetDefault: boolean\n isAppTemplate: boolean\n newProject: string | undefined\n organization: string | undefined\n output: InitContext['output']\n planId: string | undefined\n project: string | undefined\n showDefaultConfigPrompt: boolean\n trace: TelemetryTrace<TelemetryUserProperties, InitStepResult>\n unattended: boolean\n user: SanityOrgUser\n visibility: 'private' | 'public' | undefined\n}): Promise<{\n datasetName: string\n displayName: string\n isFirstProject: boolean\n organizationId?: string\n projectId: string\n schemaUrl?: string\n}> {\n if (isAppTemplate) {\n const hasProjectFlag = Boolean(project || newProject)\n let appOrganizationId: string | undefined = organization\n if (!appOrganizationId && !hasProjectFlag) {\n let organizations: ProjectOrganization[]\n try {\n organizations = await listOrganizations()\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err)\n throw new InitError(`Failed to communicate with the Sanity API:\\n${message}`, 1)\n }\n appOrganizationId = await promptUserForOrganization({\n isAppTemplate: true,\n organizations,\n user,\n })\n }\n\n const {\n datasetName: appDatasetName,\n displayName: appDisplayName,\n organizationId: resolvedOrganizationId,\n projectId: appProjectId,\n } = await promptForAppTemplateSetup({\n coupon,\n dataset,\n datasetDefault,\n newProject,\n organization,\n organizationId: appOrganizationId,\n output,\n planId,\n project,\n trace,\n unattended,\n user,\n visibility,\n })\n\n return {\n datasetName: appDatasetName,\n displayName: appDisplayName,\n isFirstProject: false,\n organizationId: resolvedOrganizationId ?? appOrganizationId,\n projectId: appProjectId,\n }\n }\n\n debug('Prompting user to select or create a project')\n const projectResult = await getOrCreateProject({\n coupon,\n newProject,\n organization,\n planId,\n project,\n unattended,\n user,\n })\n debug(`Project with name ${projectResult.displayName} selected`)\n\n debug('Prompting user to select or create a dataset')\n const datasetResult = await getOrCreateDataset({\n dataset,\n defaultConfig: datasetDefault || undefined,\n displayName: projectResult.displayName,\n output,\n projectId: projectResult.projectId,\n showDefaultConfigPrompt,\n unattended,\n visibility,\n })\n debug(`Dataset with name ${datasetResult.datasetName} selected`)\n\n trace.log({\n datasetName: datasetResult.datasetName,\n selectedOption: datasetResult.userAction,\n step: 'createOrSelectDataset',\n visibility,\n })\n\n return {\n datasetName: datasetResult.datasetName,\n displayName: projectResult.displayName,\n isFirstProject: projectResult.isFirstProject,\n organizationId: projectResult.organizationId,\n projectId: projectResult.projectId,\n }\n}\n"],"names":["subdebug","listOrganizations","InitError","getOrCreateDataset","getOrCreateProject","promptForAppTemplateSetup","promptUserForOrganization","debug","getProjectDetails","coupon","dataset","datasetDefault","isAppTemplate","newProject","organization","output","planId","project","showDefaultConfigPrompt","trace","unattended","user","visibility","hasProjectFlag","Boolean","appOrganizationId","organizations","err","message","Error","String","datasetName","
|
|
1
|
+
{"version":3,"sources":["../../../../src/actions/init/project/getProjectDetails.ts"],"sourcesContent":["import {type SanityOrgUser, subdebug, type TelemetryUserProperties} from '@sanity/cli-core'\nimport {type TelemetryTrace} from '@sanity/telemetry'\n\nimport {listOrganizations, type ProjectOrganization} from '../../../services/organizations.js'\nimport {type InitStepResult} from '../../../telemetry/init.telemetry.js'\nimport {InitError} from '../initError.js'\nimport {type InitContext} from '../types.js'\nimport {getOrCreateDataset} from './getOrCreateDataset.js'\nimport {getOrCreateProject} from './getOrCreateProject.js'\nimport {promptForAppTemplateSetup} from './promptForAppTemplateSetup.js'\nimport {promptUserForOrganization} from './promptUserForOrganization.js'\n\nconst debug = subdebug('init')\n\nexport async function getProjectDetails({\n coupon,\n dataset,\n datasetDefault,\n isAppTemplate,\n isMediaLibraryTemplate = false,\n newProject,\n organization,\n output,\n planId,\n project,\n showDefaultConfigPrompt,\n trace,\n unattended,\n user,\n visibility,\n}: {\n coupon: string | undefined\n dataset: string | undefined\n datasetDefault: boolean\n isAppTemplate: boolean\n isMediaLibraryTemplate?: boolean\n newProject: string | undefined\n organization: string | undefined\n output: InitContext['output']\n planId: string | undefined\n project: string | undefined\n showDefaultConfigPrompt: boolean\n trace: TelemetryTrace<TelemetryUserProperties, InitStepResult>\n unattended: boolean\n user: SanityOrgUser\n visibility: 'private' | 'public' | undefined\n}): Promise<{\n datasetName: string\n displayName: string\n isFirstProject: boolean\n organizationId?: string\n projectId: string\n schemaUrl?: string\n}> {\n if (isAppTemplate) {\n const hasProjectFlag = Boolean(project || newProject)\n let appOrganizationId: string | undefined = organization\n // A media-library config is organization-only, so the org is always\n // resolved here — project flags can't stand in for it.\n if (!appOrganizationId && (isMediaLibraryTemplate || !hasProjectFlag)) {\n let organizations: ProjectOrganization[]\n try {\n organizations = await listOrganizations()\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err)\n throw new InitError(`Failed to communicate with the Sanity API:\\n${message}`, 1)\n }\n appOrganizationId = await promptUserForOrganization({\n isAppTemplate: true,\n organizations,\n user,\n })\n }\n\n // The media-library config scaffold records only `app.organizationId`, so\n // the project/dataset setup below would create or select resources the\n // template throws away — and picking a project from another organization\n // would silently override the chosen org. Skip it entirely.\n if (isMediaLibraryTemplate) {\n if (hasProjectFlag) {\n output.warn(\n 'Project flags are ignored for the media-library-config template — it only records the organization.',\n )\n }\n trace.log({datasetName: '', selectedOption: 'skip', step: 'createOrSelectDataset'})\n return {\n datasetName: '',\n displayName: '',\n isFirstProject: false,\n organizationId: appOrganizationId,\n projectId: '',\n }\n }\n\n const {\n datasetName: appDatasetName,\n displayName: appDisplayName,\n organizationId: resolvedOrganizationId,\n projectId: appProjectId,\n } = await promptForAppTemplateSetup({\n coupon,\n dataset,\n datasetDefault,\n newProject,\n organization,\n organizationId: appOrganizationId,\n output,\n planId,\n project,\n trace,\n unattended,\n user,\n visibility,\n })\n\n return {\n datasetName: appDatasetName,\n displayName: appDisplayName,\n isFirstProject: false,\n organizationId: resolvedOrganizationId ?? appOrganizationId,\n projectId: appProjectId,\n }\n }\n\n debug('Prompting user to select or create a project')\n const projectResult = await getOrCreateProject({\n coupon,\n newProject,\n organization,\n planId,\n project,\n unattended,\n user,\n })\n debug(`Project with name ${projectResult.displayName} selected`)\n\n debug('Prompting user to select or create a dataset')\n const datasetResult = await getOrCreateDataset({\n dataset,\n defaultConfig: datasetDefault || undefined,\n displayName: projectResult.displayName,\n output,\n projectId: projectResult.projectId,\n showDefaultConfigPrompt,\n unattended,\n visibility,\n })\n debug(`Dataset with name ${datasetResult.datasetName} selected`)\n\n trace.log({\n datasetName: datasetResult.datasetName,\n selectedOption: datasetResult.userAction,\n step: 'createOrSelectDataset',\n visibility,\n })\n\n return {\n datasetName: datasetResult.datasetName,\n displayName: projectResult.displayName,\n isFirstProject: projectResult.isFirstProject,\n organizationId: projectResult.organizationId,\n projectId: projectResult.projectId,\n }\n}\n"],"names":["subdebug","listOrganizations","InitError","getOrCreateDataset","getOrCreateProject","promptForAppTemplateSetup","promptUserForOrganization","debug","getProjectDetails","coupon","dataset","datasetDefault","isAppTemplate","isMediaLibraryTemplate","newProject","organization","output","planId","project","showDefaultConfigPrompt","trace","unattended","user","visibility","hasProjectFlag","Boolean","appOrganizationId","organizations","err","message","Error","String","warn","log","datasetName","selectedOption","step","displayName","isFirstProject","organizationId","projectId","appDatasetName","appDisplayName","resolvedOrganizationId","appProjectId","projectResult","datasetResult","defaultConfig","undefined","userAction"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { subdebug } from '@sanity/cli-core';
|
|
2
|
-
import {
|
|
2
|
+
import { search } from '@sanity/cli-core/ux';
|
|
3
|
+
import { searchChoices, searchTheme } from '../../../util/searchChoices.js';
|
|
3
4
|
import { findOrganizationByUserName } from '../../organizations/findOrganizationByUserName.js';
|
|
4
5
|
import { getOrganizationChoices } from '../../organizations/getOrganizationChoices.js';
|
|
5
6
|
import { getOrganizationsWithAttachGrantInfo } from '../../organizations/getOrganizationsWithAttachGrantInfo.js';
|
|
@@ -23,10 +24,11 @@ export async function promptUserForOrganization({ isAppTemplate = false, organiz
|
|
|
23
24
|
organizationChoices = getOrganizationChoices(withGrantInfo);
|
|
24
25
|
defaultOrganizationId = withAttach.length === 1 ? withAttach[0].organization.id : findOrganizationByUserName(organizations, user);
|
|
25
26
|
}
|
|
26
|
-
const chosenOrg = await
|
|
27
|
-
choices: organizationChoices,
|
|
27
|
+
const chosenOrg = await search({
|
|
28
28
|
default: defaultOrganizationId || undefined,
|
|
29
|
-
message: 'Select organization:'
|
|
29
|
+
message: 'Select organization:',
|
|
30
|
+
source: searchChoices(organizationChoices),
|
|
31
|
+
theme: searchTheme
|
|
30
32
|
});
|
|
31
33
|
if (chosenOrg === '-new-') {
|
|
32
34
|
const newOrganization = await promptUserForNewOrganization(user);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/actions/init/project/promptUserForOrganization.ts"],"sourcesContent":["import {type SanityOrgUser, subdebug} from '@sanity/cli-core'\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../../src/actions/init/project/promptUserForOrganization.ts"],"sourcesContent":["import {type SanityOrgUser, subdebug} from '@sanity/cli-core'\nimport {search} from '@sanity/cli-core/ux'\n\nimport {type ProjectOrganization} from '../../../services/organizations.js'\nimport {searchChoices, searchTheme} from '../../../util/searchChoices.js'\nimport {findOrganizationByUserName} from '../../organizations/findOrganizationByUserName.js'\nimport {getOrganizationChoices} from '../../organizations/getOrganizationChoices.js'\nimport {getOrganizationsWithAttachGrantInfo} from '../../organizations/getOrganizationsWithAttachGrantInfo.js'\nimport {type OrganizationChoices} from '../../organizations/types.js'\nimport {promptUserForNewOrganization} from './promptUserForNewOrganization.js'\n\nconst debug = subdebug('init')\n\nexport async function promptUserForOrganization({\n isAppTemplate = false,\n organizations,\n user,\n}: {\n isAppTemplate?: boolean\n organizations: ProjectOrganization[]\n user: SanityOrgUser\n}) {\n if (organizations.length === 0) {\n const newOrganization = await promptUserForNewOrganization(user)\n return newOrganization.id\n }\n\n let organizationChoices: OrganizationChoices\n let defaultOrganizationId: string | undefined\n\n if (isAppTemplate) {\n organizationChoices = getOrganizationChoices(organizations)\n defaultOrganizationId =\n organizations.length === 1\n ? organizations[0].id\n : findOrganizationByUserName(organizations, user)\n } else {\n debug(`User has ${organizations.length} organization(s), checking attach access`)\n const withGrantInfo = await getOrganizationsWithAttachGrantInfo(organizations)\n const withAttach = withGrantInfo.filter(({hasAttachGrant}) => hasAttachGrant)\n\n debug('User has attach access to %d organizations.', withAttach.length)\n organizationChoices = getOrganizationChoices(withGrantInfo)\n defaultOrganizationId =\n withAttach.length === 1\n ? withAttach[0].organization.id\n : findOrganizationByUserName(organizations, user)\n }\n\n const chosenOrg = await search({\n default: defaultOrganizationId || undefined,\n message: 'Select organization:',\n source: searchChoices(organizationChoices),\n theme: searchTheme,\n })\n\n if (chosenOrg === '-new-') {\n const newOrganization = await promptUserForNewOrganization(user)\n return newOrganization.id\n }\n\n return chosenOrg || undefined\n}\n"],"names":["subdebug","search","searchChoices","searchTheme","findOrganizationByUserName","getOrganizationChoices","getOrganizationsWithAttachGrantInfo","promptUserForNewOrganization","debug","promptUserForOrganization","isAppTemplate","organizations","user","length","newOrganization","id","organizationChoices","defaultOrganizationId","withGrantInfo","withAttach","filter","hasAttachGrant","organization","chosenOrg","default","undefined","message","source","theme"],"mappings":"AAAA,SAA4BA,QAAQ,QAAO,mBAAkB;AAC7D,SAAQC,MAAM,QAAO,sBAAqB;AAG1C,SAAQC,aAAa,EAAEC,WAAW,QAAO,iCAAgC;AACzE,SAAQC,0BAA0B,QAAO,oDAAmD;AAC5F,SAAQC,sBAAsB,QAAO,gDAA+C;AACpF,SAAQC,mCAAmC,QAAO,6DAA4D;AAE9G,SAAQC,4BAA4B,QAAO,oCAAmC;AAE9E,MAAMC,QAAQR,SAAS;AAEvB,OAAO,eAAeS,0BAA0B,EAC9CC,gBAAgB,KAAK,EACrBC,aAAa,EACbC,IAAI,EAKL;IACC,IAAID,cAAcE,MAAM,KAAK,GAAG;QAC9B,MAAMC,kBAAkB,MAAMP,6BAA6BK;QAC3D,OAAOE,gBAAgBC,EAAE;IAC3B;IAEA,IAAIC;IACJ,IAAIC;IAEJ,IAAIP,eAAe;QACjBM,sBAAsBX,uBAAuBM;QAC7CM,wBACEN,cAAcE,MAAM,KAAK,IACrBF,aAAa,CAAC,EAAE,CAACI,EAAE,GACnBX,2BAA2BO,eAAeC;IAClD,OAAO;QACLJ,MAAM,CAAC,SAAS,EAAEG,cAAcE,MAAM,CAAC,wCAAwC,CAAC;QAChF,MAAMK,gBAAgB,MAAMZ,oCAAoCK;QAChE,MAAMQ,aAAaD,cAAcE,MAAM,CAAC,CAAC,EAACC,cAAc,EAAC,GAAKA;QAE9Db,MAAM,+CAA+CW,WAAWN,MAAM;QACtEG,sBAAsBX,uBAAuBa;QAC7CD,wBACEE,WAAWN,MAAM,KAAK,IAClBM,UAAU,CAAC,EAAE,CAACG,YAAY,CAACP,EAAE,GAC7BX,2BAA2BO,eAAeC;IAClD;IAEA,MAAMW,YAAY,MAAMtB,OAAO;QAC7BuB,SAASP,yBAAyBQ;QAClCC,SAAS;QACTC,QAAQzB,cAAcc;QACtBY,OAAOzB;IACT;IAEA,IAAIoB,cAAc,SAAS;QACzB,MAAMT,kBAAkB,MAAMP,6BAA6BK;QAC3D,OAAOE,gBAAgBC,EAAE;IAC3B;IAEA,OAAOQ,aAAaE;AACtB"}
|
|
@@ -3,6 +3,7 @@ import appSanityUiTemplate from './appSanityUi.js';
|
|
|
3
3
|
import blog from './blog.js';
|
|
4
4
|
import clean from './clean.js';
|
|
5
5
|
import getStartedTemplate from './getStarted.js';
|
|
6
|
+
import mediaLibraryConfig from './mediaLibraryConfig.js';
|
|
6
7
|
import moviedb from './moviedb.js';
|
|
7
8
|
import pageBuilder from './pageBuilder.js';
|
|
8
9
|
import quickstart from './quickstart.js';
|
|
@@ -14,6 +15,8 @@ const templates = {
|
|
|
14
15
|
blog,
|
|
15
16
|
clean,
|
|
16
17
|
'get-started': getStartedTemplate,
|
|
18
|
+
// Unlisted (not in `templateChoices`): `--template media-library-config` only.
|
|
19
|
+
'media-library-config': mediaLibraryConfig,
|
|
17
20
|
moviedb,
|
|
18
21
|
'page-builder': pageBuilder,
|
|
19
22
|
quickstart,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/actions/init/templates/index.ts"],"sourcesContent":["import {type ProjectTemplate} from '../types.js'\nimport appTemplate from './appQuickstart.js'\nimport appSanityUiTemplate from './appSanityUi.js'\nimport blog from './blog.js'\nimport clean from './clean.js'\nimport getStartedTemplate from './getStarted.js'\nimport moviedb from './moviedb.js'\nimport pageBuilder from './pageBuilder.js'\nimport quickstart from './quickstart.js'\nimport shopify from './shopify.js'\nimport shopifyOnline from './shopifyOnline.js'\n\nconst templates: Record<string, ProjectTemplate | undefined> = {\n 'app-quickstart': appTemplate,\n 'app-sanity-ui': appSanityUiTemplate,\n blog,\n clean,\n 'get-started': getStartedTemplate,\n moviedb,\n 'page-builder': pageBuilder,\n quickstart, // empty project that dynamically imports its own schema\n shopify,\n 'shopify-online-storefront': shopifyOnline,\n}\n\nexport default templates\n"],"names":["appTemplate","appSanityUiTemplate","blog","clean","getStartedTemplate","moviedb","pageBuilder","quickstart","shopify","shopifyOnline","templates"],"mappings":"AACA,OAAOA,iBAAiB,qBAAoB;AAC5C,OAAOC,yBAAyB,mBAAkB;AAClD,OAAOC,UAAU,YAAW;AAC5B,OAAOC,WAAW,aAAY;AAC9B,OAAOC,wBAAwB,kBAAiB;AAChD,OAAOC,aAAa,eAAc;AAClC,OAAOC,iBAAiB,mBAAkB;AAC1C,OAAOC,gBAAgB,kBAAiB;AACxC,OAAOC,aAAa,eAAc;AAClC,OAAOC,mBAAmB,qBAAoB;AAE9C,MAAMC,YAAyD;IAC7D,
|
|
1
|
+
{"version":3,"sources":["../../../../src/actions/init/templates/index.ts"],"sourcesContent":["import {type ProjectTemplate} from '../types.js'\nimport appTemplate from './appQuickstart.js'\nimport appSanityUiTemplate from './appSanityUi.js'\nimport blog from './blog.js'\nimport clean from './clean.js'\nimport getStartedTemplate from './getStarted.js'\nimport mediaLibraryConfig from './mediaLibraryConfig.js'\nimport moviedb from './moviedb.js'\nimport pageBuilder from './pageBuilder.js'\nimport quickstart from './quickstart.js'\nimport shopify from './shopify.js'\nimport shopifyOnline from './shopifyOnline.js'\n\nconst templates: Record<string, ProjectTemplate | undefined> = {\n 'app-quickstart': appTemplate,\n 'app-sanity-ui': appSanityUiTemplate,\n blog,\n clean,\n 'get-started': getStartedTemplate,\n // Unlisted (not in `templateChoices`): `--template media-library-config` only.\n 'media-library-config': mediaLibraryConfig,\n moviedb,\n 'page-builder': pageBuilder,\n quickstart, // empty project that dynamically imports its own schema\n shopify,\n 'shopify-online-storefront': shopifyOnline,\n}\n\nexport default templates\n"],"names":["appTemplate","appSanityUiTemplate","blog","clean","getStartedTemplate","mediaLibraryConfig","moviedb","pageBuilder","quickstart","shopify","shopifyOnline","templates"],"mappings":"AACA,OAAOA,iBAAiB,qBAAoB;AAC5C,OAAOC,yBAAyB,mBAAkB;AAClD,OAAOC,UAAU,YAAW;AAC5B,OAAOC,WAAW,aAAY;AAC9B,OAAOC,wBAAwB,kBAAiB;AAChD,OAAOC,wBAAwB,0BAAyB;AACxD,OAAOC,aAAa,eAAc;AAClC,OAAOC,iBAAiB,mBAAkB;AAC1C,OAAOC,gBAAgB,kBAAiB;AACxC,OAAOC,aAAa,eAAc;AAClC,OAAOC,mBAAmB,qBAAoB;AAE9C,MAAMC,YAAyD;IAC7D,kBAAkBX;IAClB,iBAAiBC;IACjBC;IACAC;IACA,eAAeC;IACf,+EAA+E;IAC/E,wBAAwBC;IACxBC;IACA,gBAAgBC;IAChBC;IACAC;IACA,6BAA6BC;AAC/B;AAEA,eAAeC,UAAS"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media Library config app: field (aspect) definitions for an organization's
|
|
3
|
+
* Media Library, bundled into a federated remote with island-rendered custom
|
|
4
|
+
* inputs. Unlisted — reachable only as `sanity init` with the template flag
|
|
5
|
+
* set to `media-library-config`, not offered in the template prompt.
|
|
6
|
+
*
|
|
7
|
+
* Brings its own dependency set (neither the SDK-app nor the studio base
|
|
8
|
+
* applies): field modules import `sanity` and `@sanity/ui` directly, and the
|
|
9
|
+
* island runtime the build bundles first shipped in `sanity` 6.20.0
|
|
10
|
+
* (`sanity/media-library-island-runtime`), which the range pins as the floor.
|
|
11
|
+
*/ const mediaLibraryConfigTemplate = {
|
|
12
|
+
dependencies: {
|
|
13
|
+
// Matches the @sanity/ui major `sanity` itself depends on, so one copy
|
|
14
|
+
// serves both the field modules and the studio dependency tree.
|
|
15
|
+
'@sanity/ui': '^4.4.10',
|
|
16
|
+
// The react floor @sanity/ui 4 and sanity 6.20 peer-require.
|
|
17
|
+
react: '^19.2.2',
|
|
18
|
+
'react-dom': '^19.2.2',
|
|
19
|
+
sanity: '^6.20.0',
|
|
20
|
+
// Peer of both `sanity` and `@sanity/ui`, imported by @sanity/ui at
|
|
21
|
+
// runtime and bundled into the island build — declared explicitly because
|
|
22
|
+
// installs without auto-installed peers would fail at runtime. (Sanity's
|
|
23
|
+
// own monorepos swap it for the `@sanity/css-in-js` fork via a pnpm
|
|
24
|
+
// override; a scaffolded project has no override, so it needs the real
|
|
25
|
+
// package.)
|
|
26
|
+
'styled-components': '^6.1.18'
|
|
27
|
+
},
|
|
28
|
+
devDependencies: {
|
|
29
|
+
'@sanity/eslint-config-studio': '^7',
|
|
30
|
+
'@types/react': '^19',
|
|
31
|
+
'@types/react-dom': '^19',
|
|
32
|
+
eslint: '^10',
|
|
33
|
+
// typescript-eslint (via eslint-config-studio) only supports TypeScript up to 6.0.x
|
|
34
|
+
typescript: '~6.0',
|
|
35
|
+
// The scaffolded eslint.config.mjs imports it directly (plugin declaration
|
|
36
|
+
// for the restricted-imports override). Same range eslint-config-studio
|
|
37
|
+
// uses, so npm dedupes to one instance — a second copy would make the
|
|
38
|
+
// plugin redefinition conflict.
|
|
39
|
+
'typescript-eslint': '^8'
|
|
40
|
+
},
|
|
41
|
+
scripts: {
|
|
42
|
+
build: 'sanity build',
|
|
43
|
+
deploy: 'sanity deploy',
|
|
44
|
+
dev: 'sanity dev'
|
|
45
|
+
},
|
|
46
|
+
type: 'module',
|
|
47
|
+
typescriptOnly: true
|
|
48
|
+
};
|
|
49
|
+
export default mediaLibraryConfigTemplate;
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=mediaLibraryConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/actions/init/templates/mediaLibraryConfig.ts"],"sourcesContent":["import {type ProjectTemplate} from '../types.js'\n\n/**\n * Media Library config app: field (aspect) definitions for an organization's\n * Media Library, bundled into a federated remote with island-rendered custom\n * inputs. Unlisted — reachable only as `sanity init` with the template flag\n * set to `media-library-config`, not offered in the template prompt.\n *\n * Brings its own dependency set (neither the SDK-app nor the studio base\n * applies): field modules import `sanity` and `@sanity/ui` directly, and the\n * island runtime the build bundles first shipped in `sanity` 6.20.0\n * (`sanity/media-library-island-runtime`), which the range pins as the floor.\n */\nconst mediaLibraryConfigTemplate: ProjectTemplate = {\n dependencies: {\n // Matches the @sanity/ui major `sanity` itself depends on, so one copy\n // serves both the field modules and the studio dependency tree.\n '@sanity/ui': '^4.4.10',\n // The react floor @sanity/ui 4 and sanity 6.20 peer-require.\n react: '^19.2.2',\n 'react-dom': '^19.2.2',\n sanity: '^6.20.0',\n // Peer of both `sanity` and `@sanity/ui`, imported by @sanity/ui at\n // runtime and bundled into the island build — declared explicitly because\n // installs without auto-installed peers would fail at runtime. (Sanity's\n // own monorepos swap it for the `@sanity/css-in-js` fork via a pnpm\n // override; a scaffolded project has no override, so it needs the real\n // package.)\n 'styled-components': '^6.1.18',\n },\n devDependencies: {\n '@sanity/eslint-config-studio': '^7',\n '@types/react': '^19',\n '@types/react-dom': '^19',\n eslint: '^10',\n // typescript-eslint (via eslint-config-studio) only supports TypeScript up to 6.0.x\n typescript: '~6.0',\n // The scaffolded eslint.config.mjs imports it directly (plugin declaration\n // for the restricted-imports override). Same range eslint-config-studio\n // uses, so npm dedupes to one instance — a second copy would make the\n // plugin redefinition conflict.\n 'typescript-eslint': '^8',\n },\n scripts: {\n build: 'sanity build',\n deploy: 'sanity deploy',\n dev: 'sanity dev',\n },\n type: 'module',\n typescriptOnly: true,\n}\n\nexport default mediaLibraryConfigTemplate\n"],"names":["mediaLibraryConfigTemplate","dependencies","react","sanity","devDependencies","eslint","typescript","scripts","build","deploy","dev","type","typescriptOnly"],"mappings":"AAEA;;;;;;;;;;CAUC,GACD,MAAMA,6BAA8C;IAClDC,cAAc;QACZ,uEAAuE;QACvE,gEAAgE;QAChE,cAAc;QACd,6DAA6D;QAC7DC,OAAO;QACP,aAAa;QACbC,QAAQ;QACR,oEAAoE;QACpE,0EAA0E;QAC1E,yEAAyE;QACzE,oEAAoE;QACpE,uEAAuE;QACvE,YAAY;QACZ,qBAAqB;IACvB;IACAC,iBAAiB;QACf,gCAAgC;QAChC,gBAAgB;QAChB,oBAAoB;QACpBC,QAAQ;QACR,oFAAoF;QACpFC,YAAY;QACZ,2EAA2E;QAC3E,wEAAwE;QACxE,sEAAsE;QACtE,gCAAgC;QAChC,qBAAqB;IACvB;IACAC,SAAS;QACPC,OAAO;QACPC,QAAQ;QACRC,KAAK;IACP;IACAC,MAAM;IACNC,gBAAgB;AAClB;AAEA,eAAeZ,2BAA0B"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape an aspect/field name must have — the single source for every
|
|
3
|
+
* consumer. Names double as the stored `sanity.asset.aspect` document `_id`
|
|
4
|
+
* and as the `aspects.<name>` data key on asset documents, and get
|
|
5
|
+
* interpolated into GROQ by tooling, so the same rule guards all three.
|
|
6
|
+
*/ export const VALID_ASPECT_NAME = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=aspectName.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/media/aspectName.ts"],"sourcesContent":["/**\n * The shape an aspect/field name must have — the single source for every\n * consumer. Names double as the stored `sanity.asset.aspect` document `_id`\n * and as the `aspects.<name>` data key on asset documents, and get\n * interpolated into GROQ by tooling, so the same rule guards all three.\n */\nexport const VALID_ASPECT_NAME = /^[a-zA-Z][a-zA-Z0-9_]*$/\n"],"names":["VALID_ASPECT_NAME"],"mappings":"AAAA;;;;;CAKC,GACD,OAAO,MAAMA,oBAAoB,0BAAyB"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Media Library configuration lifecycle — pure serialization and mirror
|
|
3
|
+
* semantics, vendored from huey's `@repo/media-library-config` (the package
|
|
4
|
+
* that proved the contract). Keep the two copies behaviorally identical until
|
|
5
|
+
* huey's standalone sync bin is retired in favor of `sanity deploy`.
|
|
6
|
+
*
|
|
7
|
+
* A media library's aspect fields are declared as *field modules* in a config
|
|
8
|
+
* app's `sanity.cli.ts` (`unstable_defineMediaLibrary({fields: [...]})`).
|
|
9
|
+
* Environments that evaluate the config app live (the Workbench) get the full
|
|
10
|
+
* definitions — custom inputs, validation callbacks, initial-value resolvers.
|
|
11
|
+
* Everything else (Studio plugin mode, standalone dashboard, Gradient) reads
|
|
12
|
+
* stored `sanity.asset.aspect` documents instead.
|
|
13
|
+
*
|
|
14
|
+
* This module owns the contract between those two worlds:
|
|
15
|
+
*
|
|
16
|
+
* - **Serialization** (`serializeField`): field modules are live JS, stored
|
|
17
|
+
* aspect documents are JSON. Literal values pass through; functions and
|
|
18
|
+
* component references are stripped deterministically, with every stripped
|
|
19
|
+
* path reported so the capability gap is explicit.
|
|
20
|
+
* - **Mirror semantics** (`computeMirrorDiff`, `buildSyncMutations`): the
|
|
21
|
+
* config app is the source of truth for the aspect namespace. A sync
|
|
22
|
+
* upserts every config field; stored aspect documents without a matching
|
|
23
|
+
* config field are *stale* and are deleted to complete the mirror. Deleting
|
|
24
|
+
* an aspect document removes the definition, not asset data — values remain
|
|
25
|
+
* on assets under `aspects.<name>` and reappear if the field is restored.
|
|
26
|
+
*
|
|
27
|
+
* This module is pure (no I/O); composition with the CLI's API services lives
|
|
28
|
+
* in `./syncAspects.ts`.
|
|
29
|
+
*/ import { VALID_ASPECT_NAME } from '../aspectName.js';
|
|
30
|
+
/** The document type stored aspect definitions use. */ export const ASPECT_TYPE = 'sanity.asset.aspect';
|
|
31
|
+
/**
|
|
32
|
+
* Serialize a field module's live `defineField(...)` value into a stored
|
|
33
|
+
* aspect document.
|
|
34
|
+
*
|
|
35
|
+
* The `_id` convention is the field/aspect name, matching the legacy
|
|
36
|
+
* `sanity media deploy-aspect` command (its delete targets `id == name`).
|
|
37
|
+
*/ export function serializeField(entry, definition) {
|
|
38
|
+
if (!VALID_ASPECT_NAME.test(entry.name)) {
|
|
39
|
+
throw new Error(`"${entry.name}" is not a valid aspect name (${VALID_ASPECT_NAME}) — it becomes the document _id and the aspects.<name> key on assets`);
|
|
40
|
+
}
|
|
41
|
+
if (!definition || typeof definition !== 'object' || typeof definition.name !== 'string' || typeof definition.type !== 'string') {
|
|
42
|
+
throw new Error(`Field "${entry.name}" must be a defineField(...) value (an object with name and type)`);
|
|
43
|
+
}
|
|
44
|
+
const definitionName = definition.name;
|
|
45
|
+
// A mismatch must not serialize: `entry.name` becomes the document _id
|
|
46
|
+
// (mirror upserts/deletes) while `definition.name` becomes the
|
|
47
|
+
// aspects.<name> data key on assets — a document carrying both would have
|
|
48
|
+
// its data under one name and its lifecycle under another.
|
|
49
|
+
if (definitionName !== entry.name) {
|
|
50
|
+
throw new Error(`Field "${entry.name}": its field module defines name "${definitionName}" — the names must match`);
|
|
51
|
+
}
|
|
52
|
+
const stripped = [];
|
|
53
|
+
const serialized = serializeValue(definition, 'definition', stripped);
|
|
54
|
+
return {
|
|
55
|
+
document: {
|
|
56
|
+
_id: entry.name,
|
|
57
|
+
_type: ASPECT_TYPE,
|
|
58
|
+
definition: serialized,
|
|
59
|
+
// Strict boolean check, mirroring the read-side guard in the Media
|
|
60
|
+
// Library app: `entry` is a runtime value from a config app, and a
|
|
61
|
+
// mistyped `public: 'false'` must not serialize into the stored doc
|
|
62
|
+
// where later Boolean(...) coercion would flip the aspect to public.
|
|
63
|
+
...typeof entry.public === 'boolean' ? {
|
|
64
|
+
public: entry.public
|
|
65
|
+
} : {}
|
|
66
|
+
},
|
|
67
|
+
stripped
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Deterministic strip rule: literal values pass through; functions (initial
|
|
72
|
+
* value resolvers, validation callbacks, input components) and other
|
|
73
|
+
* non-JSON values are dropped with their path recorded. The `components` key
|
|
74
|
+
* is dropped wholesale — it is UI-only and never meaningful in a stored doc.
|
|
75
|
+
*
|
|
76
|
+
* `ancestors` holds the objects on the current recursion path: revisiting one
|
|
77
|
+
* means a cycle, which is stripped like any other non-JSON value (instead of
|
|
78
|
+
* overflowing the stack). Shared acyclic references serialize at every site.
|
|
79
|
+
*/ function serializeValue(value, atPath, stripped, ancestors = new Set()) {
|
|
80
|
+
if (value === null) return null;
|
|
81
|
+
const kind = typeof value;
|
|
82
|
+
if (kind === 'string' || kind === 'number' || kind === 'boolean') return value;
|
|
83
|
+
if (kind === 'function' || kind === 'symbol' || kind === 'bigint' || kind === 'undefined') {
|
|
84
|
+
stripped.push(atPath);
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (ancestors.has(value)) {
|
|
88
|
+
stripped.push(atPath);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
ancestors.add(value);
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const [index, item] of value.entries()){
|
|
95
|
+
const serialized = serializeValue(item, `${atPath}[${index}]`, stripped, ancestors);
|
|
96
|
+
if (serialized !== undefined) out.push(serialized);
|
|
97
|
+
}
|
|
98
|
+
ancestors.delete(value);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
if (kind === 'object') {
|
|
102
|
+
// React elements / memo components etc. are tagged objects, not plain data
|
|
103
|
+
if (value.$$typeof) {
|
|
104
|
+
stripped.push(atPath);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
// Dates, RegExps, Maps etc. would walk to `{}` (no enumerable own keys)
|
|
108
|
+
// without ever being reported — strip anything that isn't a plain object
|
|
109
|
+
// so the "every stripped path reported" contract holds.
|
|
110
|
+
const proto = Object.getPrototypeOf(value);
|
|
111
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
112
|
+
stripped.push(atPath);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
ancestors.add(value);
|
|
116
|
+
const out = {};
|
|
117
|
+
for (const [key, entryValue] of Object.entries(value)){
|
|
118
|
+
if (key === 'components') {
|
|
119
|
+
stripped.push(`${atPath}.components`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const serialized = serializeValue(entryValue, `${atPath}.${key}`, stripped, ancestors);
|
|
123
|
+
if (serialized !== undefined) out[key] = serialized;
|
|
124
|
+
}
|
|
125
|
+
ancestors.delete(value);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
stripped.push(atPath);
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Mirror semantics: given the documents a config declares and the ids stored
|
|
133
|
+
* in the library, compute which stored documents are stale (present in the
|
|
134
|
+
* library, absent from the config) and must be deleted for the library to
|
|
135
|
+
* mirror the config.
|
|
136
|
+
*/ export function computeMirrorDiff(configDocuments, storedIds) {
|
|
137
|
+
const configIds = new Set(configDocuments.map((doc)=>doc._id));
|
|
138
|
+
return {
|
|
139
|
+
staleIds: storedIds.filter((id)=>!configIds.has(id))
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Build the mutation transaction for one sync: upsert every config document,
|
|
144
|
+
* delete every stale stored document. The delete shape matches the CLI's
|
|
145
|
+
* `deleteAspect` (`sanity media delete-aspect`).
|
|
146
|
+
*/ export function buildSyncMutations(configDocuments, staleIds) {
|
|
147
|
+
return [
|
|
148
|
+
...configDocuments.map((document)=>({
|
|
149
|
+
createOrReplace: document
|
|
150
|
+
})),
|
|
151
|
+
...staleIds.map((id)=>({
|
|
152
|
+
delete: {
|
|
153
|
+
params: {
|
|
154
|
+
id,
|
|
155
|
+
type: ASPECT_TYPE
|
|
156
|
+
},
|
|
157
|
+
query: `*[_type == $type && _id == $id]`
|
|
158
|
+
}
|
|
159
|
+
}))
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/actions/media/aspectSync/contract.ts"],"sourcesContent":["/**\n * The Media Library configuration lifecycle — pure serialization and mirror\n * semantics, vendored from huey's `@repo/media-library-config` (the package\n * that proved the contract). Keep the two copies behaviorally identical until\n * huey's standalone sync bin is retired in favor of `sanity deploy`.\n *\n * A media library's aspect fields are declared as *field modules* in a config\n * app's `sanity.cli.ts` (`unstable_defineMediaLibrary({fields: [...]})`).\n * Environments that evaluate the config app live (the Workbench) get the full\n * definitions — custom inputs, validation callbacks, initial-value resolvers.\n * Everything else (Studio plugin mode, standalone dashboard, Gradient) reads\n * stored `sanity.asset.aspect` documents instead.\n *\n * This module owns the contract between those two worlds:\n *\n * - **Serialization** (`serializeField`): field modules are live JS, stored\n * aspect documents are JSON. Literal values pass through; functions and\n * component references are stripped deterministically, with every stripped\n * path reported so the capability gap is explicit.\n * - **Mirror semantics** (`computeMirrorDiff`, `buildSyncMutations`): the\n * config app is the source of truth for the aspect namespace. A sync\n * upserts every config field; stored aspect documents without a matching\n * config field are *stale* and are deleted to complete the mirror. Deleting\n * an aspect document removes the definition, not asset data — values remain\n * on assets under `aspects.<name>` and reappear if the field is restored.\n *\n * This module is pure (no I/O); composition with the CLI's API services lives\n * in `./syncAspects.ts`.\n */\n\nimport {VALID_ASPECT_NAME} from '../aspectName.js'\n\n/** The document type stored aspect definitions use. */\nexport const ASPECT_TYPE = 'sanity.asset.aspect'\n\n/**\n * A field entry as declared in a media-library config app's `sanity.cli.ts`.\n */\nexport interface FieldEntry {\n name: string\n src: string\n\n public?: boolean\n title?: string\n}\n\n/**\n * The `sanity.asset.aspect` *write payload* produced by a sync.\n *\n * This is deliberately not `MediaLibraryAssetAspectDocument` from\n * `@sanity/types` — that is the read-side type: it carries required\n * server-assigned `_rev`/`_createdAt`/`_updatedAt` (which a to-be-written\n * document does not have) and types `definition` as a full `FieldDefinition`,\n * which the serializer's stripped-JSON output is only guaranteed to be after\n * schema validation. Read-side documents can be passed to\n * {@link computeMirrorDiff} (it only needs ids), but upserts via\n * {@link buildSyncMutations} require this write shape — echoing read-side\n * server metadata back into a `createOrReplace` would be a bug.\n */\nexport interface AspectDocument {\n _id: string\n _type: typeof ASPECT_TYPE\n definition: Record<string, unknown>\n\n public?: boolean\n}\n\n/**\n * Result of serializing a live field definition into a stored document.\n */\nexport interface SerializedField {\n document: AspectDocument\n /** Dot-paths of values dropped because they are not JSON-serializable. */\n stripped: string[]\n}\n\n/** One mutation in a sync transaction (see {@link buildSyncMutations}). */\nexport type SyncMutation =\n | {createOrReplace: AspectDocument}\n | {delete: {params: {id: string; type: string}; query: string}}\n\n/**\n * Serialize a field module's live `defineField(...)` value into a stored\n * aspect document.\n *\n * The `_id` convention is the field/aspect name, matching the legacy\n * `sanity media deploy-aspect` command (its delete targets `id == name`).\n */\nexport function serializeField(entry: FieldEntry, definition: unknown): SerializedField {\n if (!VALID_ASPECT_NAME.test(entry.name)) {\n throw new Error(\n `\"${entry.name}\" is not a valid aspect name (${VALID_ASPECT_NAME}) — it becomes the document _id and the aspects.<name> key on assets`,\n )\n }\n if (\n !definition ||\n typeof definition !== 'object' ||\n typeof (definition as {name?: unknown}).name !== 'string' ||\n typeof (definition as {type?: unknown}).type !== 'string'\n ) {\n throw new Error(\n `Field \"${entry.name}\" must be a defineField(...) value (an object with name and type)`,\n )\n }\n\n const definitionName = (definition as {name: string}).name\n // A mismatch must not serialize: `entry.name` becomes the document _id\n // (mirror upserts/deletes) while `definition.name` becomes the\n // aspects.<name> data key on assets — a document carrying both would have\n // its data under one name and its lifecycle under another.\n if (definitionName !== entry.name) {\n throw new Error(\n `Field \"${entry.name}\": its field module defines name \"${definitionName}\" — the names must match`,\n )\n }\n\n const stripped: string[] = []\n const serialized = serializeValue(definition, 'definition', stripped) as Record<string, unknown>\n\n return {\n document: {\n _id: entry.name,\n _type: ASPECT_TYPE,\n definition: serialized,\n // Strict boolean check, mirroring the read-side guard in the Media\n // Library app: `entry` is a runtime value from a config app, and a\n // mistyped `public: 'false'` must not serialize into the stored doc\n // where later Boolean(...) coercion would flip the aspect to public.\n ...(typeof entry.public === 'boolean' ? {public: entry.public} : {}),\n },\n stripped,\n }\n}\n\n/**\n * Deterministic strip rule: literal values pass through; functions (initial\n * value resolvers, validation callbacks, input components) and other\n * non-JSON values are dropped with their path recorded. The `components` key\n * is dropped wholesale — it is UI-only and never meaningful in a stored doc.\n *\n * `ancestors` holds the objects on the current recursion path: revisiting one\n * means a cycle, which is stripped like any other non-JSON value (instead of\n * overflowing the stack). Shared acyclic references serialize at every site.\n */\nfunction serializeValue(\n value: unknown,\n atPath: string,\n stripped: string[],\n ancestors: Set<object> = new Set(),\n): unknown {\n if (value === null) return null\n const kind = typeof value\n if (kind === 'string' || kind === 'number' || kind === 'boolean') return value\n if (kind === 'function' || kind === 'symbol' || kind === 'bigint' || kind === 'undefined') {\n stripped.push(atPath)\n return undefined\n }\n if (ancestors.has(value as object)) {\n stripped.push(atPath)\n return undefined\n }\n if (Array.isArray(value)) {\n ancestors.add(value)\n const out: unknown[] = []\n for (const [index, item] of value.entries()) {\n const serialized = serializeValue(item, `${atPath}[${index}]`, stripped, ancestors)\n if (serialized !== undefined) out.push(serialized)\n }\n ancestors.delete(value)\n return out\n }\n if (kind === 'object') {\n // React elements / memo components etc. are tagged objects, not plain data\n if ((value as {$$typeof?: unknown}).$$typeof) {\n stripped.push(atPath)\n return undefined\n }\n // Dates, RegExps, Maps etc. would walk to `{}` (no enumerable own keys)\n // without ever being reported — strip anything that isn't a plain object\n // so the \"every stripped path reported\" contract holds.\n const proto = Object.getPrototypeOf(value) as object | null\n if (proto !== Object.prototype && proto !== null) {\n stripped.push(atPath)\n return undefined\n }\n ancestors.add(value as object)\n const out: Record<string, unknown> = {}\n for (const [key, entryValue] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'components') {\n stripped.push(`${atPath}.components`)\n continue\n }\n const serialized = serializeValue(entryValue, `${atPath}.${key}`, stripped, ancestors)\n if (serialized !== undefined) out[key] = serialized\n }\n ancestors.delete(value as object)\n return out\n }\n stripped.push(atPath)\n return undefined\n}\n\n/**\n * Mirror semantics: given the documents a config declares and the ids stored\n * in the library, compute which stored documents are stale (present in the\n * library, absent from the config) and must be deleted for the library to\n * mirror the config.\n */\nexport function computeMirrorDiff(\n configDocuments: readonly {_id: string}[],\n storedIds: readonly string[],\n): {staleIds: string[]} {\n const configIds = new Set(configDocuments.map((doc) => doc._id))\n return {staleIds: storedIds.filter((id) => !configIds.has(id))}\n}\n\n/**\n * Build the mutation transaction for one sync: upsert every config document,\n * delete every stale stored document. The delete shape matches the CLI's\n * `deleteAspect` (`sanity media delete-aspect`).\n */\nexport function buildSyncMutations(\n configDocuments: readonly AspectDocument[],\n staleIds: readonly string[],\n): SyncMutation[] {\n return [\n ...configDocuments.map((document) => ({createOrReplace: document})),\n ...staleIds.map((id) => ({\n delete: {\n params: {id, type: ASPECT_TYPE},\n query: `*[_type == $type && _id == $id]`,\n },\n })),\n ]\n}\n"],"names":["VALID_ASPECT_NAME","ASPECT_TYPE","serializeField","entry","definition","test","name","Error","type","definitionName","stripped","serialized","serializeValue","document","_id","_type","public","value","atPath","ancestors","Set","kind","push","undefined","has","Array","isArray","add","out","index","item","entries","delete","$$typeof","proto","Object","getPrototypeOf","prototype","key","entryValue","computeMirrorDiff","configDocuments","storedIds","configIds","map","doc","staleIds","filter","id","buildSyncMutations","createOrReplace","params","query"],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composition of the pure aspect-sync contract (`./contract.ts`) with the
|
|
3
|
+
* CLI's Media Library API services, for `sanity deploy` of a media-library
|
|
4
|
+
* config app.
|
|
5
|
+
*
|
|
6
|
+
* Split into a read-only **plan** and a mutating **apply** so the deploy flow
|
|
7
|
+
* can report the plan — including the deletion list — in both dry-run and
|
|
8
|
+
* real mode before anything is written:
|
|
9
|
+
*
|
|
10
|
+
* - {@link planAspectSync}: evaluate + serialize + validate every config
|
|
11
|
+
* field module, resolve the target media library, and diff against the
|
|
12
|
+
* stored aspect ids. No mutations.
|
|
13
|
+
* - {@link applyAspectSync}: apply the plan's upserts and deletes as one
|
|
14
|
+
* mutation transaction.
|
|
15
|
+
*/ import path from 'node:path';
|
|
16
|
+
import { exitCodes, importModule } from '@sanity/cli-core';
|
|
17
|
+
import { validateMediaLibraryAssetAspect } from '@sanity/schema/_internal';
|
|
18
|
+
import { getStoredAspectIds, mutateAspects } from '../../../services/mediaLibraries.js';
|
|
19
|
+
import { resolveMediaLibrary } from '../resolveMediaLibraryId.js';
|
|
20
|
+
import { buildSyncMutations, computeMirrorDiff, serializeField } from './contract.js';
|
|
21
|
+
/**
|
|
22
|
+
* Build the sync plan: evaluate, serialize, and schema-validate every field
|
|
23
|
+
* module before touching the network, then resolve the target library and
|
|
24
|
+
* diff against its stored aspect ids. Read-only.
|
|
25
|
+
*/ export async function planAspectSync(options) {
|
|
26
|
+
const { fields, mediaLibraryId: explicitMediaLibraryId, organizationId, workDir } = options;
|
|
27
|
+
// Evaluate + serialize + validate every field module before touching the
|
|
28
|
+
// network, collecting all failures so one broken module doesn't hide the next.
|
|
29
|
+
const documents = [];
|
|
30
|
+
const strippedByField = {};
|
|
31
|
+
const failures = [];
|
|
32
|
+
for (const entry of fields){
|
|
33
|
+
try {
|
|
34
|
+
// jsx: fields may be .tsx modules (custom input components) — the
|
|
35
|
+
// template ships one — and jiti does not parse JSX by default.
|
|
36
|
+
const definition = await importModule(path.resolve(workDir, entry.src), {
|
|
37
|
+
jsx: true
|
|
38
|
+
});
|
|
39
|
+
const { document, stripped } = serializeField(entry, definition);
|
|
40
|
+
const [valid, validationErrors] = validateMediaLibraryAssetAspect(document.definition);
|
|
41
|
+
if (!valid) {
|
|
42
|
+
const messages = validationErrors.flat().map((problem)=>problem.message);
|
|
43
|
+
failures.push(`${entry.name}: schema validation failed: ${messages.join('; ')}`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
documents.push(document);
|
|
47
|
+
if (stripped.length > 0) strippedByField[entry.name] = stripped;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
failures.push(`${entry.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (failures.length > 0) {
|
|
53
|
+
return {
|
|
54
|
+
failure: {
|
|
55
|
+
exitCode: exitCodes.USAGE_ERROR,
|
|
56
|
+
message: `${failures.length === 1 ? 'A config field' : `${failures.length} config fields`} cannot sync to stored aspect documents:\n${failures.map((failure)=>` - ${failure}`).join('\n')}`,
|
|
57
|
+
solution: 'Fix the field modules listed above, then deploy again'
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const library = await resolveTargetMediaLibrary(organizationId, explicitMediaLibraryId);
|
|
62
|
+
if ('failure' in library) return library;
|
|
63
|
+
const storedIds = await getStoredAspectIds(library.mediaLibraryId);
|
|
64
|
+
const { staleIds } = computeMirrorDiff(documents, storedIds);
|
|
65
|
+
return {
|
|
66
|
+
plan: {
|
|
67
|
+
documents,
|
|
68
|
+
mediaLibraryId: library.mediaLibraryId,
|
|
69
|
+
staleIds,
|
|
70
|
+
strippedByField
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Apply the plan as one mutation transaction: upsert every config document,
|
|
76
|
+
* delete every stale stored document. Definitions only — asset data under
|
|
77
|
+
* `aspects.<name>` is never touched.
|
|
78
|
+
*/ export async function applyAspectSync(plan) {
|
|
79
|
+
await mutateAspects({
|
|
80
|
+
mediaLibraryId: plan.mediaLibraryId,
|
|
81
|
+
mutations: buildSyncMutations(plan.documents, plan.staleIds)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Shares the one resolution rule (`resolveMediaLibrary`) with the media
|
|
86
|
+
* commands, mapped to deploy-check failures. Ambiguity always fails here —
|
|
87
|
+
* deploy can run unattended (CI), so it never prompts, and a sync that
|
|
88
|
+
* guesses the target would mirror the config into the wrong library. Misuse
|
|
89
|
+
* (a bad flag value, a missing required flag) carries the usage exit code;
|
|
90
|
+
* a missing library is an environment problem and keeps the default.
|
|
91
|
+
*/ async function resolveTargetMediaLibrary(organizationId, explicitMediaLibraryId) {
|
|
92
|
+
const resolution = await resolveMediaLibrary({
|
|
93
|
+
mediaLibraryIdFlag: explicitMediaLibraryId,
|
|
94
|
+
organizationId
|
|
95
|
+
});
|
|
96
|
+
if (resolution.type === 'resolved') {
|
|
97
|
+
return {
|
|
98
|
+
mediaLibraryId: resolution.mediaLibraryId
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Deploy-specific wording over the shared resolution's generic remedies:
|
|
102
|
+
// the install matters *before deploying the config*, and deploy never
|
|
103
|
+
// prompts, so the ambiguity message says the sync needs an explicit target.
|
|
104
|
+
return {
|
|
105
|
+
failure: {
|
|
106
|
+
...resolution.type === 'none-active' ? {} : {
|
|
107
|
+
exitCode: exitCodes.USAGE_ERROR
|
|
108
|
+
},
|
|
109
|
+
message: resolution.type === 'ambiguous' ? `${resolution.message} — the sync needs an explicit target` : resolution.message,
|
|
110
|
+
solution: resolution.type === 'none-active' ? 'Install the Media Library for the organization before deploying its config' : resolution.solution
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//# sourceMappingURL=syncAspects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/actions/media/aspectSync/syncAspects.ts"],"sourcesContent":["/**\n * Composition of the pure aspect-sync contract (`./contract.ts`) with the\n * CLI's Media Library API services, for `sanity deploy` of a media-library\n * config app.\n *\n * Split into a read-only **plan** and a mutating **apply** so the deploy flow\n * can report the plan — including the deletion list — in both dry-run and\n * real mode before anything is written:\n *\n * - {@link planAspectSync}: evaluate + serialize + validate every config\n * field module, resolve the target media library, and diff against the\n * stored aspect ids. No mutations.\n * - {@link applyAspectSync}: apply the plan's upserts and deletes as one\n * mutation transaction.\n */\n\nimport path from 'node:path'\n\nimport {exitCodes, importModule} from '@sanity/cli-core'\nimport {validateMediaLibraryAssetAspect} from '@sanity/schema/_internal'\n\nimport {getStoredAspectIds, mutateAspects} from '../../../services/mediaLibraries.js'\nimport {resolveMediaLibrary} from '../resolveMediaLibraryId.js'\nimport {\n type AspectDocument,\n buildSyncMutations,\n computeMirrorDiff,\n type FieldEntry,\n serializeField,\n} from './contract.js'\n\n/** The read-only sync plan `sanity deploy` reports before mutating. */\nexport interface AspectSyncPlan {\n /** The documents the sync upserts, one per config field. */\n documents: AspectDocument[]\n mediaLibraryId: string\n /** Stored aspect ids with no matching config field — deleted by the sync. */\n staleIds: string[]\n /** Per-field dot-paths stripped during serialization, for fields that had any. */\n strippedByField: Record<string, string[]>\n}\n\ninterface AspectSyncFailure {\n message: string\n\n /** Exit code a real deploy uses for this failure; defaults to 1. */\n exitCode?: number\n solution?: string\n}\n\nexport type AspectSyncPlanResult = {failure: AspectSyncFailure} | {plan: AspectSyncPlan}\n\n/**\n * Build the sync plan: evaluate, serialize, and schema-validate every field\n * module before touching the network, then resolve the target library and\n * diff against its stored aspect ids. Read-only.\n */\nexport async function planAspectSync(options: {\n fields: readonly FieldEntry[]\n mediaLibraryId?: string\n organizationId: string\n workDir: string\n}): Promise<AspectSyncPlanResult> {\n const {fields, mediaLibraryId: explicitMediaLibraryId, organizationId, workDir} = options\n\n // Evaluate + serialize + validate every field module before touching the\n // network, collecting all failures so one broken module doesn't hide the next.\n const documents: AspectDocument[] = []\n const strippedByField: Record<string, string[]> = {}\n const failures: string[] = []\n\n for (const entry of fields) {\n try {\n // jsx: fields may be .tsx modules (custom input components) — the\n // template ships one — and jiti does not parse JSX by default.\n const definition = await importModule(path.resolve(workDir, entry.src), {jsx: true})\n const {document, stripped} = serializeField(entry, definition)\n\n const [valid, validationErrors] = validateMediaLibraryAssetAspect(document.definition)\n if (!valid) {\n const messages = validationErrors.flat().map((problem) => problem.message)\n failures.push(`${entry.name}: schema validation failed: ${messages.join('; ')}`)\n continue\n }\n\n documents.push(document)\n if (stripped.length > 0) strippedByField[entry.name] = stripped\n } catch (error) {\n failures.push(`${entry.name}: ${error instanceof Error ? error.message : String(error)}`)\n }\n }\n\n if (failures.length > 0) {\n return {\n failure: {\n exitCode: exitCodes.USAGE_ERROR,\n message: `${failures.length === 1 ? 'A config field' : `${failures.length} config fields`} cannot sync to stored aspect documents:\\n${failures.map((failure) => ` - ${failure}`).join('\\n')}`,\n solution: 'Fix the field modules listed above, then deploy again',\n },\n }\n }\n\n const library = await resolveTargetMediaLibrary(organizationId, explicitMediaLibraryId)\n if ('failure' in library) return library\n\n const storedIds = await getStoredAspectIds(library.mediaLibraryId)\n const {staleIds} = computeMirrorDiff(documents, storedIds)\n\n return {plan: {documents, mediaLibraryId: library.mediaLibraryId, staleIds, strippedByField}}\n}\n\n/**\n * Apply the plan as one mutation transaction: upsert every config document,\n * delete every stale stored document. Definitions only — asset data under\n * `aspects.<name>` is never touched.\n */\nexport async function applyAspectSync(plan: AspectSyncPlan): Promise<void> {\n await mutateAspects({\n mediaLibraryId: plan.mediaLibraryId,\n mutations: buildSyncMutations(plan.documents, plan.staleIds),\n })\n}\n\n/**\n * Shares the one resolution rule (`resolveMediaLibrary`) with the media\n * commands, mapped to deploy-check failures. Ambiguity always fails here —\n * deploy can run unattended (CI), so it never prompts, and a sync that\n * guesses the target would mirror the config into the wrong library. Misuse\n * (a bad flag value, a missing required flag) carries the usage exit code;\n * a missing library is an environment problem and keeps the default.\n */\nasync function resolveTargetMediaLibrary(\n organizationId: string,\n explicitMediaLibraryId: string | undefined,\n): Promise<{failure: AspectSyncFailure} | {mediaLibraryId: string}> {\n const resolution = await resolveMediaLibrary({\n mediaLibraryIdFlag: explicitMediaLibraryId,\n organizationId,\n })\n if (resolution.type === 'resolved') {\n return {mediaLibraryId: resolution.mediaLibraryId}\n }\n // Deploy-specific wording over the shared resolution's generic remedies:\n // the install matters *before deploying the config*, and deploy never\n // prompts, so the ambiguity message says the sync needs an explicit target.\n return {\n failure: {\n ...(resolution.type === 'none-active' ? {} : {exitCode: exitCodes.USAGE_ERROR}),\n message:\n resolution.type === 'ambiguous'\n ? `${resolution.message} — the sync needs an explicit target`\n : resolution.message,\n solution:\n resolution.type === 'none-active'\n ? 'Install the Media Library for the organization before deploying its config'\n : resolution.solution,\n },\n }\n}\n"],"names":["path","exitCodes","importModule","validateMediaLibraryAssetAspect","getStoredAspectIds","mutateAspects","resolveMediaLibrary","buildSyncMutations","computeMirrorDiff","serializeField","planAspectSync","options","fields","mediaLibraryId","explicitMediaLibraryId","organizationId","workDir","documents","strippedByField","failures","entry","definition","resolve","src","jsx","document","stripped","valid","validationErrors","messages","flat","map","problem","message","push","name","join","length","error","Error","String","failure","exitCode","USAGE_ERROR","solution","library","resolveTargetMediaLibrary","storedIds","staleIds","plan","applyAspectSync","mutations","resolution","mediaLibraryIdFlag","type"],"mappings":"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"}
|