@sanity/cli 8.22.0 → 8.23.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.
Files changed (89) hide show
  1. package/dist/actions/context/watchJob.js +9 -3
  2. package/dist/actions/context/watchJob.js.map +1 -1
  3. package/dist/actions/deploy/blueprintManaged.js +59 -0
  4. package/dist/actions/deploy/blueprintManaged.js.map +1 -0
  5. package/dist/actions/deploy/deployApp.js +117 -1
  6. package/dist/actions/deploy/deployApp.js.map +1 -1
  7. package/dist/actions/deploy/deployRunner.js.map +1 -1
  8. package/dist/actions/deploy/deployStudio.js +6 -31
  9. package/dist/actions/deploy/deployStudio.js.map +1 -1
  10. package/dist/actions/deploy/deployStudioSchemasAndManifestsWorker.js +6 -15
  11. package/dist/actions/deploy/deployStudioSchemasAndManifestsWorker.js.map +1 -1
  12. package/dist/actions/exec/runScript.worker.js +6 -2
  13. package/dist/actions/exec/runScript.worker.js.map +1 -1
  14. package/dist/actions/init/bootstrapLocalTemplate.js +35 -11
  15. package/dist/actions/init/bootstrapLocalTemplate.js.map +1 -1
  16. package/dist/actions/init/createMediaLibraryCliConfig.js +12 -0
  17. package/dist/actions/init/createMediaLibraryCliConfig.js.map +1 -0
  18. package/dist/actions/init/determineMediaLibraryTemplate.js +14 -0
  19. package/dist/actions/init/determineMediaLibraryTemplate.js.map +1 -0
  20. package/dist/actions/init/initAction.js +35 -6
  21. package/dist/actions/init/initAction.js.map +1 -1
  22. package/dist/actions/init/initApp.js +81 -6
  23. package/dist/actions/init/initApp.js.map +1 -1
  24. package/dist/actions/init/offerAspectPull.js +81 -0
  25. package/dist/actions/init/offerAspectPull.js.map +1 -0
  26. package/dist/actions/init/project/getProjectDetails.js +25 -2
  27. package/dist/actions/init/project/getProjectDetails.js.map +1 -1
  28. package/dist/actions/init/templates/index.js +3 -0
  29. package/dist/actions/init/templates/index.js.map +1 -1
  30. package/dist/actions/init/templates/mediaLibraryConfig.js +51 -0
  31. package/dist/actions/init/templates/mediaLibraryConfig.js.map +1 -0
  32. package/dist/actions/media/aspectName.js +8 -0
  33. package/dist/actions/media/aspectName.js.map +1 -0
  34. package/dist/actions/media/aspectSync/contract.js +163 -0
  35. package/dist/actions/media/aspectSync/contract.js.map +1 -0
  36. package/dist/actions/media/aspectSync/syncAspects.js +115 -0
  37. package/dist/actions/media/aspectSync/syncAspects.js.map +1 -0
  38. package/dist/actions/media/cliConfigFields.js +192 -0
  39. package/dist/actions/media/cliConfigFields.js.map +1 -0
  40. package/dist/actions/media/pullAspects.js +225 -0
  41. package/dist/actions/media/pullAspects.js.map +1 -0
  42. package/dist/actions/media/resolveMediaLibraryId.js +83 -0
  43. package/dist/actions/media/resolveMediaLibraryId.js.map +1 -0
  44. package/dist/actions/schema/deploySchemas.js.map +1 -1
  45. package/dist/actions/schema/extractSchemaWatcher.js +69 -14
  46. package/dist/actions/schema/extractSchemaWatcher.js.map +1 -1
  47. package/dist/actions/schema/updateWorkspaceSchema.js.map +1 -1
  48. package/dist/commands/context/build.js +2 -2
  49. package/dist/commands/context/build.js.map +1 -1
  50. package/dist/commands/context/entries/get.js +83 -0
  51. package/dist/commands/context/entries/get.js.map +1 -0
  52. package/dist/commands/context/entries/list.js +82 -0
  53. package/dist/commands/context/entries/list.js.map +1 -0
  54. package/dist/commands/context/entries/rebuild.js +95 -0
  55. package/dist/commands/context/entries/rebuild.js.map +1 -0
  56. package/dist/commands/context/issues/apply.js +86 -0
  57. package/dist/commands/context/issues/apply.js.map +1 -0
  58. package/dist/commands/context/issues/dismiss.js +52 -0
  59. package/dist/commands/context/issues/dismiss.js.map +1 -0
  60. package/dist/commands/context/issues/get.js +105 -0
  61. package/dist/commands/context/issues/get.js.map +1 -0
  62. package/dist/commands/context/issues/list.js +128 -0
  63. package/dist/commands/context/issues/list.js.map +1 -0
  64. package/dist/commands/context/issues/reopen.js +53 -0
  65. package/dist/commands/context/issues/reopen.js.map +1 -0
  66. package/dist/commands/context/issues/resolve.js +137 -0
  67. package/dist/commands/context/issues/resolve.js.map +1 -0
  68. package/dist/commands/context/jobs/get.js +2 -2
  69. package/dist/commands/context/jobs/get.js.map +1 -1
  70. package/dist/commands/context/query.js +92 -0
  71. package/dist/commands/context/query.js.map +1 -0
  72. package/dist/commands/deploy.js +13 -0
  73. package/dist/commands/deploy.js.map +1 -1
  74. package/dist/commands/media/prune-aspect-data.js +170 -0
  75. package/dist/commands/media/prune-aspect-data.js.map +1 -0
  76. package/dist/commands/media/pull-aspects.js +309 -0
  77. package/dist/commands/media/pull-aspects.js.map +1 -0
  78. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +17 -0
  79. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
  80. package/dist/services/context.js +96 -3
  81. package/dist/services/context.js.map +1 -1
  82. package/dist/services/mediaLibraries.js +175 -8
  83. package/dist/services/mediaLibraries.js.map +1 -1
  84. package/oclif.manifest.json +1050 -361
  85. package/package.json +6 -5
  86. package/templates/media-library-config/README.md +52 -0
  87. package/templates/media-library-config/eslint.config.txt +52 -0
  88. package/templates/media-library-config/src/fields/caption.tsx +29 -0
  89. package/templates/media-library-config/src/fields/notes.tsx +54 -0
@@ -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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"}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Locates and edits the `fields: [...]` array in a Media Library config app's
3
+ * `sanity.cli.ts` — appending entries (`sanity media pull-aspects`) or
4
+ * replacing them (the init-time pull). Deploying mirrors the declared fields
5
+ * into the library and deletes stored aspects left undeclared, so unsafe
6
+ * edits are refused rather than risked: the AST (vite's `parseAst`) locates
7
+ * the one array literal the deploy will read, the edit is a byte-preserving
8
+ * splice at its offsets, and any other shape returns a reason the caller
9
+ * turns into a paste-ready fallback.
10
+ */ import { parseAst } from 'vite';
11
+ const CALL_NAME = 'unstable_defineMediaLibrary';
12
+ /**
13
+ * Depth-first collection of every `unstable_defineMediaLibrary(...)` call in
14
+ * the (sub)tree at `value`. The one cast is guarded by the `type`
15
+ * discriminant it narrows on.
16
+ */ function collectCalls(value, calls) {
17
+ if (Array.isArray(value)) {
18
+ for (const item of value)collectCalls(item, calls);
19
+ return;
20
+ }
21
+ if (typeof value !== 'object' || value === null) return;
22
+ if (value.type === 'CallExpression') {
23
+ const call = value;
24
+ if (call.callee.type === 'Identifier' && call.callee.name === CALL_NAME) {
25
+ calls.push(call);
26
+ }
27
+ }
28
+ for (const child of Object.values(value)){
29
+ if (typeof child === 'object') collectCalls(child, calls);
30
+ }
31
+ }
32
+ /** Non-computed `fields` key — `fields:` or `'fields':`. */ function isFieldsProperty(prop) {
33
+ if (prop.type !== 'Property' || prop.computed) return false;
34
+ const { key } = prop;
35
+ return key.type === 'Identifier' && key.name === 'fields' || key.type === 'Literal' && key.value === 'fields';
36
+ }
37
+ /**
38
+ * The index of the `,` between a trailing array element ending at `from` and
39
+ * the `]` at `to`, or -1 when the element has no trailing comma. Only
40
+ * whitespace, comments, and at most one comma can sit in that gap, and the
41
+ * comments terminate (the source parsed), so this scan just has to not read a
42
+ * comma out of a comment.
43
+ */ function indexOfTrailingComma(source, from, to) {
44
+ let index = from;
45
+ while(index < to){
46
+ const char = source[index];
47
+ if (char === ',') return index;
48
+ if (char === '/' && source[index + 1] === '/') {
49
+ const newline = source.indexOf('\n', index);
50
+ if (newline === -1 || newline >= to) return -1;
51
+ index = newline + 1;
52
+ continue;
53
+ }
54
+ if (char === '/' && source[index + 1] === '*') {
55
+ index = source.indexOf('*/', index + 2) + 2;
56
+ continue;
57
+ }
58
+ index++;
59
+ }
60
+ return -1;
61
+ }
62
+ function locateFieldsArray(source) {
63
+ // Node offsets are UTF-16 string indices, so splicing `source` with them is
64
+ // exact even around non-ASCII content.
65
+ let program;
66
+ try {
67
+ program = parseAst(source, {
68
+ lang: 'ts'
69
+ });
70
+ } catch {
71
+ return {
72
+ reason: 'the config does not parse as TypeScript'
73
+ };
74
+ }
75
+ // The import statement also mentions the identifier, but only call sites
76
+ // show up as CallExpressions.
77
+ const calls = [];
78
+ collectCalls(program.body, calls);
79
+ if (calls.length === 0) {
80
+ return {
81
+ reason: 'no `unstable_defineMediaLibrary(...)` call found'
82
+ };
83
+ }
84
+ if (calls.length > 1) {
85
+ return {
86
+ reason: 'more than one `unstable_defineMediaLibrary(...)` call found'
87
+ };
88
+ }
89
+ const [config] = calls[0].arguments;
90
+ if (config?.type !== 'ObjectExpression') {
91
+ return {
92
+ reason: 'the `unstable_defineMediaLibrary(...)` argument is not an object literal'
93
+ };
94
+ }
95
+ // Only the config object's own `fields` property may be edited — deploy
96
+ // evaluates exactly that one, so a `fields` array nested elsewhere (or
97
+ // sitting in a helper outside the call) must neither be edited nor block
98
+ // the real one.
99
+ const fieldsProps = config.properties.filter((prop)=>isFieldsProperty(prop));
100
+ if (fieldsProps.length === 0) {
101
+ return {
102
+ reason: 'no `fields` property in the `unstable_defineMediaLibrary({...})` config'
103
+ };
104
+ }
105
+ if (fieldsProps.length > 1) {
106
+ return {
107
+ reason: 'more than one `fields` property in the `unstable_defineMediaLibrary({...})` config'
108
+ };
109
+ }
110
+ const fields = fieldsProps[0];
111
+ const array = fields.value;
112
+ if (array.type !== 'ArrayExpression') {
113
+ // A computed value (`fields: sharedFields`) stays the user's to manage —
114
+ // deploy keeps evaluating it, so an "edit" elsewhere would be a lie.
115
+ return {
116
+ reason: 'the `fields` value is not an array literal'
117
+ };
118
+ }
119
+ const open = array.start;
120
+ const close = array.end - 1;
121
+ const last = array.elements.at(-1);
122
+ if (last === null) {
123
+ return {
124
+ reason: 'the `fields` array ends in an elision'
125
+ };
126
+ }
127
+ let lastContentEnd = open + 1;
128
+ if (last) {
129
+ const comma = indexOfTrailingComma(source, last.end, close);
130
+ lastContentEnd = comma === -1 ? last.end : comma + 1;
131
+ }
132
+ const lineStart = source.lastIndexOf('\n', fields.start) + 1;
133
+ const indent = /^[\t ]*/.exec(source.slice(lineStart, fields.start))?.[0] ?? '';
134
+ return {
135
+ close,
136
+ indent,
137
+ lastContentEnd,
138
+ open
139
+ };
140
+ }
141
+ /**
142
+ * Append `entryLines` (one rendered `{name, src, title},` object per line) to
143
+ * the config's `fields` array, preserving the existing entries and adding a
144
+ * trailing comma to the last one when needed.
145
+ */ export function appendFieldEntries(source, entryLines) {
146
+ const located = locateFieldsArray(source);
147
+ if ('reason' in located) return {
148
+ ok: false,
149
+ reason: located.reason
150
+ };
151
+ const { indent, lastContentEnd, open } = located;
152
+ const entryIndent = `${indent} `;
153
+ const block = entryLines.map((line)=>entryIndent + line).join('\n');
154
+ if (lastContentEnd === open + 1) {
155
+ const inner = source.slice(open + 1, located.close);
156
+ if (inner.trim() !== '') {
157
+ // Only comments inside: rewriting would silently drop them.
158
+ return {
159
+ ok: false,
160
+ reason: 'the `fields` array contains only comments'
161
+ };
162
+ }
163
+ const updated = `${source.slice(0, open + 1)}\n${block}\n${indent}${source.slice(located.close)}`;
164
+ return {
165
+ ok: true,
166
+ updated
167
+ };
168
+ }
169
+ const comma = source[lastContentEnd - 1] === ',' ? '' : ',';
170
+ const updated = `${source.slice(0, lastContentEnd)}${comma}\n${block}${source.slice(lastContentEnd)}`;
171
+ return {
172
+ ok: true,
173
+ updated
174
+ };
175
+ }
176
+ /** Replace the config's `fields` array contents with `entryLines` wholesale. */ export function replaceFieldEntries(source, entryLines) {
177
+ const located = locateFieldsArray(source);
178
+ if ('reason' in located) return {
179
+ ok: false,
180
+ reason: located.reason
181
+ };
182
+ const { close, indent, open } = located;
183
+ const entryIndent = `${indent} `;
184
+ const block = entryLines.map((line)=>entryIndent + line).join('\n');
185
+ const updated = `${source.slice(0, open + 1)}\n${block}\n${indent}${source.slice(close)}`;
186
+ return {
187
+ ok: true,
188
+ updated
189
+ };
190
+ }
191
+
192
+ //# sourceMappingURL=cliConfigFields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/actions/media/cliConfigFields.ts"],"sourcesContent":["/**\n * Locates and edits the `fields: [...]` array in a Media Library config app's\n * `sanity.cli.ts` — appending entries (`sanity media pull-aspects`) or\n * replacing them (the init-time pull). Deploying mirrors the declared fields\n * into the library and deletes stored aspects left undeclared, so unsafe\n * edits are refused rather than risked: the AST (vite's `parseAst`) locates\n * the one array literal the deploy will read, the edit is a byte-preserving\n * splice at its offsets, and any other shape returns a reason the caller\n * turns into a paste-ready fallback.\n */\n\nimport {type CallExpression, type ObjectProperty, type ObjectPropertyKind} from '@oxc-project/types'\nimport {parseAst} from 'vite'\n\nexport type EditResult = {ok: false; reason: string} | {ok: true; updated: string}\n\nconst CALL_NAME = 'unstable_defineMediaLibrary'\n\ninterface LocatedArray {\n /** Index of the `]`. */\n close: number\n /** Leading whitespace of the line containing the `fields` property. */\n indent: string\n /**\n * Index just past the last element — past its trailing comma when there is\n * one — or `open + 1` for an empty array. Appending inserts here, so a\n * trailing comment never ends up with a comma after it (which would create\n * a sparse-array hole).\n */\n lastContentEnd: number\n /** Index of the `[`. */\n open: number\n}\n\n/**\n * Depth-first collection of every `unstable_defineMediaLibrary(...)` call in\n * the (sub)tree at `value`. The one cast is guarded by the `type`\n * discriminant it narrows on.\n */\nfunction collectCalls(value: unknown, calls: CallExpression[]): void {\n if (Array.isArray(value)) {\n for (const item of value) collectCalls(item, calls)\n return\n }\n if (typeof value !== 'object' || value === null) return\n if ((value as {type?: unknown}).type === 'CallExpression') {\n const call = value as CallExpression\n if (call.callee.type === 'Identifier' && call.callee.name === CALL_NAME) {\n calls.push(call)\n }\n }\n for (const child of Object.values(value)) {\n if (typeof child === 'object') collectCalls(child, calls)\n }\n}\n\n/** Non-computed `fields` key — `fields:` or `'fields':`. */\nfunction isFieldsProperty(prop: ObjectPropertyKind): prop is ObjectProperty {\n if (prop.type !== 'Property' || prop.computed) return false\n const {key} = prop\n return (\n (key.type === 'Identifier' && key.name === 'fields') ||\n (key.type === 'Literal' && key.value === 'fields')\n )\n}\n\n/**\n * The index of the `,` between a trailing array element ending at `from` and\n * the `]` at `to`, or -1 when the element has no trailing comma. Only\n * whitespace, comments, and at most one comma can sit in that gap, and the\n * comments terminate (the source parsed), so this scan just has to not read a\n * comma out of a comment.\n */\nfunction indexOfTrailingComma(source: string, from: number, to: number): number {\n let index = from\n while (index < to) {\n const char = source[index]\n if (char === ',') return index\n if (char === '/' && source[index + 1] === '/') {\n const newline = source.indexOf('\\n', index)\n if (newline === -1 || newline >= to) return -1\n index = newline + 1\n continue\n }\n if (char === '/' && source[index + 1] === '*') {\n index = source.indexOf('*/', index + 2) + 2\n continue\n }\n index++\n }\n return -1\n}\n\nfunction locateFieldsArray(source: string): LocatedArray | {reason: string} {\n // Node offsets are UTF-16 string indices, so splicing `source` with them is\n // exact even around non-ASCII content.\n let program: ReturnType<typeof parseAst>\n try {\n program = parseAst(source, {lang: 'ts'})\n } catch {\n return {reason: 'the config does not parse as TypeScript'}\n }\n\n // The import statement also mentions the identifier, but only call sites\n // show up as CallExpressions.\n const calls: CallExpression[] = []\n collectCalls(program.body, calls)\n if (calls.length === 0) {\n return {reason: 'no `unstable_defineMediaLibrary(...)` call found'}\n }\n if (calls.length > 1) {\n return {reason: 'more than one `unstable_defineMediaLibrary(...)` call found'}\n }\n\n const [config] = calls[0].arguments\n if (config?.type !== 'ObjectExpression') {\n return {reason: 'the `unstable_defineMediaLibrary(...)` argument is not an object literal'}\n }\n\n // Only the config object's own `fields` property may be edited — deploy\n // evaluates exactly that one, so a `fields` array nested elsewhere (or\n // sitting in a helper outside the call) must neither be edited nor block\n // the real one.\n const fieldsProps = config.properties.filter((prop) => isFieldsProperty(prop))\n if (fieldsProps.length === 0) {\n return {reason: 'no `fields` property in the `unstable_defineMediaLibrary({...})` config'}\n }\n if (fieldsProps.length > 1) {\n return {\n reason: 'more than one `fields` property in the `unstable_defineMediaLibrary({...})` config',\n }\n }\n\n const fields = fieldsProps[0]\n const array = fields.value\n if (array.type !== 'ArrayExpression') {\n // A computed value (`fields: sharedFields`) stays the user's to manage —\n // deploy keeps evaluating it, so an \"edit\" elsewhere would be a lie.\n return {reason: 'the `fields` value is not an array literal'}\n }\n\n const open = array.start\n const close = array.end - 1\n const last = array.elements.at(-1)\n\n if (last === null) {\n return {reason: 'the `fields` array ends in an elision'}\n }\n\n let lastContentEnd = open + 1\n if (last) {\n const comma = indexOfTrailingComma(source, last.end, close)\n lastContentEnd = comma === -1 ? last.end : comma + 1\n }\n\n const lineStart = source.lastIndexOf('\\n', fields.start) + 1\n const indent = /^[\\t ]*/.exec(source.slice(lineStart, fields.start))?.[0] ?? ''\n\n return {close, indent, lastContentEnd, open}\n}\n\n/**\n * Append `entryLines` (one rendered `{name, src, title},` object per line) to\n * the config's `fields` array, preserving the existing entries and adding a\n * trailing comma to the last one when needed.\n */\nexport function appendFieldEntries(source: string, entryLines: string[]): EditResult {\n const located = locateFieldsArray(source)\n if ('reason' in located) return {ok: false, reason: located.reason}\n\n const {indent, lastContentEnd, open} = located\n const entryIndent = `${indent} `\n const block = entryLines.map((line) => entryIndent + line).join('\\n')\n\n if (lastContentEnd === open + 1) {\n const inner = source.slice(open + 1, located.close)\n if (inner.trim() !== '') {\n // Only comments inside: rewriting would silently drop them.\n return {ok: false, reason: 'the `fields` array contains only comments'}\n }\n const updated = `${source.slice(0, open + 1)}\\n${block}\\n${indent}${source.slice(located.close)}`\n return {ok: true, updated}\n }\n\n const comma = source[lastContentEnd - 1] === ',' ? '' : ','\n const updated = `${source.slice(0, lastContentEnd)}${comma}\\n${block}${source.slice(lastContentEnd)}`\n return {ok: true, updated}\n}\n\n/** Replace the config's `fields` array contents with `entryLines` wholesale. */\nexport function replaceFieldEntries(source: string, entryLines: string[]): EditResult {\n const located = locateFieldsArray(source)\n if ('reason' in located) return {ok: false, reason: located.reason}\n\n const {close, indent, open} = located\n const entryIndent = `${indent} `\n const block = entryLines.map((line) => entryIndent + line).join('\\n')\n\n const updated = `${source.slice(0, open + 1)}\\n${block}\\n${indent}${source.slice(close)}`\n return {ok: true, updated}\n}\n"],"names":["parseAst","CALL_NAME","collectCalls","value","calls","Array","isArray","item","type","call","callee","name","push","child","Object","values","isFieldsProperty","prop","computed","key","indexOfTrailingComma","source","from","to","index","char","newline","indexOf","locateFieldsArray","program","lang","reason","body","length","config","arguments","fieldsProps","properties","filter","fields","array","open","start","close","end","last","elements","at","lastContentEnd","comma","lineStart","lastIndexOf","indent","exec","slice","appendFieldEntries","entryLines","located","ok","entryIndent","block","map","line","join","inner","trim","updated","replaceFieldEntries"],"mappings":"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"}