contentful-export 8.3.0 → 8.5.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 CHANGED
@@ -186,7 +186,7 @@ Content Delivery API.
186
186
 
187
187
  #### `includeArchived` [boolean] [default: false]
188
188
 
189
- Include archived entries in the exported entries
189
+ Include archived entries and assets in the exported data. This option does not include archived Releases.
190
190
 
191
191
  #### `skipContentModel` [boolean] [default: false]
192
192
 
@@ -212,6 +212,10 @@ Skip exporting tags
212
212
 
213
213
  Skip exporting webhooks
214
214
 
215
+ #### `skipReleases` [boolean] [default: false]
216
+
217
+ Skip exporting [Releases](https://www.contentful.com/help/releases/). See the "Releases" section below for what's exported when this is on.
218
+
215
219
  #### `stripTags` [boolean] [default: false]
216
220
 
217
221
  Untag assets and entries
@@ -339,6 +343,7 @@ This is an overview of the exported data:
339
343
  "webhooks": [],
340
344
  "roles": [],
341
345
  "editorInterfaces": [],
346
+ "releases": [],
342
347
  "designTokens": [],
343
348
  "components": [],
344
349
  "experienceTemplates": [],
@@ -365,6 +370,8 @@ _Note:_ Tags feature is not available for all users. If you do not have access t
365
370
 
366
371
  _Note:_ `designTokens`, `components`, `experienceTemplates`, `dataAssemblies`, `experienceFragments`, and `experiences` are Experience Orchestration (ExO) entities — present by default; absent only if you explicitly set `includeExperienceOrchestration: false` — see the "Experience Orchestration (ExO) entities" section below.
367
372
 
373
+ _Note:_ `releases` is present by default; absent only if you explicitly set `skipReleases: true` — see the "Releases" section below. It's a separate, GA Contentful feature, not part of ExO.
374
+
368
375
  ## :test_tube: Experience Orchestration (ExO) entities
369
376
 
370
377
  Experience Orchestration (ExO) is Contentful's system for composing and rendering structured page experiences. It sits above the traditional entry/content-type layer and provides six dedicated entity types — Design Tokens, Components, Experience Templates, Data Assemblies, Experience Fragments, and Experiences — that together describe how content is fetched, assembled, and laid out.
@@ -389,10 +396,41 @@ If the source space has no ExO entities, or lacks the `exoM1` entitlement, each
389
396
 
390
397
  Requires the `exoM1` entitlement on the source space's organization. [contentful-cli](https://github.com/contentful/contentful-cli)'s `space export` command doesn't expose this option at all yet, so ExO export isn't reachable through that separate CLI regardless of default.
391
398
 
399
+ ### Optimization Variants
400
+
401
+ Experiences and Experience Fragments each support **Optimization Variants** — alternate versions used for personalization. Variants are a separate opt-in on top of `includeExperienceOrchestration`, defaulting to `false`:
402
+
403
+ ```javascript
404
+ const options = {
405
+ spaceId: '<space_id>',
406
+ managementToken: '<content_management_api_key>',
407
+ includeExperienceOrchestration: true,
408
+ includeExoVariants: true
409
+ }
410
+
411
+ await contentfulExport(options)
412
+ ```
413
+
414
+ Unlike the six entity types above, variants are **not** exported as a seventh top-level array — they're nested onto their parent as `experience.optimizationVariants` / `experienceFragment.optimizationVariants`, because a variant has no globally-unique `sys.id` of its own (the API's variant response reuses the parent's `sys.id`; the variant is identified by `sys.variant`/`sys.variantType`/`sys.variantDimension` instead). When `includeExoVariants` is omitted or `false`, the `optimizationVariants` field is absent entirely — not an empty array — so default export output is unaffected. See [`docs/exo-export.md`](./docs/exo-export.md#optimization-variants) for the full shape, the ADR behind the nested-storage decision, and the corresponding `contentful-import` behavior.
415
+
392
416
  ### Round-tripping into `contentful-import`
393
417
 
394
418
  The ExO entities exported here are designed to be fed directly into [`contentful-import`](https://github.com/contentful/contentful-import), which preserves source IDs, applies dependency ordering (a topological sort for Components and Experience Fragments, since either can reference others of the same type), and upgrades entities from older, pre-rename export files automatically. See `contentful-import`'s README "Experience Orchestration (ExO) entities" section for the import-side details.
395
419
 
420
+ ## :package: Releases
421
+
422
+ [Releases](https://www.contentful.com/help/releases/) (Timeline) is a separate, GA Contentful feature — not part of Experience Orchestration, and gated by its own organization entitlement rather than `exoM1`.
423
+
424
+ Releases export is on by default (`skipReleases: false`) — for the CLI and the module API alike. Pass `skipReleases: true` (`--skip-releases` on the CLI) to opt out.
425
+
426
+ Only releases with `sys.schemaVersion: "Release.v2"` ("Releases") are fetched. `Release.v1` ("Launch") releases are excluded by the export query itself, since [`contentful-import`](https://github.com/contentful/contentful-import) only supports `Release.v2` on the import side.
427
+
428
+ Only active Releases are exported. Archived Releases are omitted, regardless of `includeArchived`, because that option currently applies only to entries and assets.
429
+
430
+ If the source space's organization lacks the Releases entitlement, or the fetch otherwise fails, a `Skipping Releases export` warning is logged and `releases` exports as an empty array — it does not fail the export.
431
+
432
+ Round-trips into [`contentful-import`](https://github.com/contentful/contentful-import), which imports each release found in the exported data. See `contentful-import`'s README "Releases" section for the import-side details, including a real limitation worth knowing before you rely on this for repeated imports: Releases have no ID-preserving create, so re-importing the same export creates additional releases rather than updating existing ones.
433
+
396
434
  ## :warning: Limitations
397
435
 
398
436
  - This tool currently does **not** support the export of space memberships.
package/dist/index.js CHANGED
@@ -80,8 +80,10 @@ function runContentfulExport(params) {
80
80
  skipWebhooks: options.skipWebhooks,
81
81
  skipRoles: options.skipRoles,
82
82
  skipTags: options.skipTags,
83
+ skipReleases: options.skipReleases,
83
84
  stripTags: options.stripTags,
84
85
  includeExperienceOrchestration: options.includeExperienceOrchestration,
86
+ includeExoVariants: options.includeExoVariants,
85
87
  listrOptions,
86
88
  queryEntries: options.queryEntries,
87
89
  queryAssets: options.queryAssets
@@ -131,6 +133,7 @@ function runContentfulExport(params) {
131
133
  }).then(ctx => {
132
134
  const resultTypes = Object.keys(ctx.data);
133
135
  if (resultTypes.length) {
136
+ var _ctx$data$experiences, _ctx$optimizationVari, _ctx$data$experienceF, _ctx$optimizationVari2;
134
137
  const resultTable = new _cliTable.default(tableOptions);
135
138
  resultTable.push([{
136
139
  colSpan: 2,
@@ -139,6 +142,33 @@ function runContentfulExport(params) {
139
142
  resultTypes.forEach(type => {
140
143
  resultTable.push([(0, _lodash.default)(type), ctx.data[type].length]);
141
144
  });
145
+
146
+ // Optimization Variants are nested onto their parent Experience/Experience
147
+ // Fragment rather than exported as their own top-level field, so their count
148
+ // isn't free from resultTypes above and is computed here instead. A per-parent
149
+ // fetch failure falls back to an empty optimizationVariants array, which is
150
+ // indistinguishable from a parent that genuinely has none -- so failures are
151
+ // surfaced as their own row rather than folded silently into the count.
152
+ if ((_ctx$data$experiences = ctx.data.experiences) !== null && _ctx$data$experiences !== void 0 && _ctx$data$experiences.some(experience => experience.optimizationVariants)) {
153
+ const count = ctx.data.experiences.reduce((sum, experience) => {
154
+ var _experience$optimizat, _experience$optimizat2;
155
+ return sum + ((_experience$optimizat = (_experience$optimizat2 = experience.optimizationVariants) === null || _experience$optimizat2 === void 0 ? void 0 : _experience$optimizat2.length) !== null && _experience$optimizat !== void 0 ? _experience$optimizat : 0);
156
+ }, 0);
157
+ resultTable.push(['Experience Optimization Variants', count]);
158
+ }
159
+ if ((_ctx$optimizationVari = ctx.optimizationVariantFailures) !== null && _ctx$optimizationVari !== void 0 && _ctx$optimizationVari.experiences) {
160
+ resultTable.push(['Experience Optimization Variant Fetch Failures', ctx.optimizationVariantFailures.experiences]);
161
+ }
162
+ if ((_ctx$data$experienceF = ctx.data.experienceFragments) !== null && _ctx$data$experienceF !== void 0 && _ctx$data$experienceF.some(experienceFragment => experienceFragment.optimizationVariants)) {
163
+ const count = ctx.data.experienceFragments.reduce((sum, experienceFragment) => {
164
+ var _experienceFragment$o, _experienceFragment$o2;
165
+ return sum + ((_experienceFragment$o = (_experienceFragment$o2 = experienceFragment.optimizationVariants) === null || _experienceFragment$o2 === void 0 ? void 0 : _experienceFragment$o2.length) !== null && _experienceFragment$o !== void 0 ? _experienceFragment$o : 0);
166
+ }, 0);
167
+ resultTable.push(['Experience Fragment Optimization Variants', count]);
168
+ }
169
+ if ((_ctx$optimizationVari2 = ctx.optimizationVariantFailures) !== null && _ctx$optimizationVari2 !== void 0 && _ctx$optimizationVari2.experienceFragments) {
170
+ resultTable.push(['Experience Fragment Optimization Variant Fetch Failures', ctx.optimizationVariantFailures.experienceFragments]);
171
+ }
142
172
  console.log(resultTable.toString());
143
173
  } else {
144
174
  console.log('No data was exported');
@@ -29,9 +29,11 @@ function parseOptions(params) {
29
29
  skipAssets: false,
30
30
  skipWebhooks: false,
31
31
  skipTags: false,
32
+ skipReleases: false,
32
33
  stripTags: false,
33
34
  maxAllowedLimit: 1000,
34
35
  includeExperienceOrchestration: true,
36
+ includeExoVariants: false,
35
37
  saveFile: true,
36
38
  useVerboseRenderer: false,
37
39
  rawProxy: false
@@ -87,6 +89,7 @@ function parseOptions(params) {
87
89
  options.skipRoles = true;
88
90
  options.skipContentModel = true;
89
91
  options.skipWebhooks = true;
92
+ options.skipReleases = true;
90
93
  }
91
94
  options.application = options.managementApplication || `contentful.export/${_package.version}`;
92
95
  options.feature = options.managementFeature || 'library-export';
@@ -33,11 +33,13 @@ function getFullSourceSpace({
33
33
  skipRoles,
34
34
  skipEditorInterfaces,
35
35
  skipTags,
36
+ skipReleases,
36
37
  stripTags,
37
38
  includeDrafts,
38
39
  includeArchived,
39
40
  maxAllowedLimit,
40
41
  includeExperienceOrchestration,
42
+ includeExoVariants,
41
43
  listrOptions,
42
44
  queryEntries,
43
45
  queryAssets
@@ -170,6 +172,29 @@ function getFullSourceSpace({
170
172
  });
171
173
  }),
172
174
  skip: () => skipRoles || environmentId !== 'master' && 'Roles can only be exported from master environment'
175
+ }, {
176
+ title: 'Fetching Releases data',
177
+ task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
178
+ try {
179
+ // Excludes releases created internally by other Contentful features (rather than by a
180
+ // user directly), so they don't leak into a customer's export as if they were
181
+ // user-authored Releases.
182
+ const releases = await cursorPagedGet(query => client.release.query({
183
+ environmentId,
184
+ spaceId,
185
+ query: _objectSpread({
186
+ 'metadata.annotations.Contentful:Timeline.type[nin]': 'Staging,Hidden',
187
+ 'sys.schemaVersion': 'Release.v2',
188
+ 'sys.status[in]': 'active'
189
+ }, query)
190
+ }));
191
+ ctx.data.releases = releases;
192
+ } catch (err) {
193
+ _contentfulBatchLibs.logEmitter.emit('warning', `Skipping Releases export: ${err.message}`);
194
+ ctx.data.releases = [];
195
+ }
196
+ }),
197
+ skip: () => skipReleases
173
198
  }, {
174
199
  title: 'Fetching Design Tokens data',
175
200
  task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
@@ -260,6 +285,40 @@ function getFullSourceSpace({
260
285
  }
261
286
  }),
262
287
  skip: () => !includeExperienceOrchestration
288
+ }, {
289
+ title: 'Fetching Experience Optimization Variants data',
290
+ task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
291
+ const experienceFailureCount = await attachOptimizationVariants({
292
+ parents: ctx.data.experiences,
293
+ fetchFn: (parentId, query) => client.experienceVariant.getMany({
294
+ spaceId,
295
+ environmentId,
296
+ experienceId: parentId,
297
+ query
298
+ }),
299
+ entityLabel: 'Experience Optimization Variants'
300
+ });
301
+ ctx.optimizationVariantFailures = ctx.optimizationVariantFailures || {};
302
+ ctx.optimizationVariantFailures.experiences = experienceFailureCount;
303
+ }),
304
+ skip: () => !includeExperienceOrchestration || !includeExoVariants
305
+ }, {
306
+ title: 'Fetching Experience Fragment Optimization Variants data',
307
+ task: (0, _contentfulBatchLibs.wrapTask)(async ctx => {
308
+ const experienceFragmentFailureCount = await attachOptimizationVariants({
309
+ parents: ctx.data.experienceFragments,
310
+ fetchFn: (parentId, query) => client.experienceFragmentVariant.getMany({
311
+ spaceId,
312
+ environmentId,
313
+ experienceFragmentId: parentId,
314
+ query
315
+ }),
316
+ entityLabel: 'Experience Fragment Optimization Variants'
317
+ });
318
+ ctx.optimizationVariantFailures = ctx.optimizationVariantFailures || {};
319
+ ctx.optimizationVariantFailures.experienceFragments = experienceFragmentFailureCount;
320
+ }),
321
+ skip: () => !includeExperienceOrchestration || !includeExoVariants
263
322
  }], listrOptions);
264
323
  }
265
324
  function getEditorInterfaces(client, spaceId, environmentId, contentTypes) {
@@ -305,6 +364,59 @@ async function cursorPagedGet(fetchFn, entityLabel) {
305
364
  return allItems;
306
365
  }
307
366
 
367
+ /**
368
+ * Fetches and attaches Optimization Variants onto each of a parent entity type's
369
+ * items (Experience or ExperienceFragment), mutating them in place as
370
+ * `parent.optimizationVariants`. Variants are nested onto their parent rather than
371
+ * exported as their own top-level field because a variant's `sys.id` is borrowed
372
+ * from its parent (not unique to the variant), so a flat array would collide the
373
+ * way none of the other ExO entity types do.
374
+ *
375
+ * Unlike the top-level ExO entities, the optimization_variants list endpoint is NOT
376
+ * cursor-paginated — it takes no limit/pageNext query params and its response has no
377
+ * total/pages fields, just { sys: { type: 'Array' }, items }. So this is a single
378
+ * request per parent, not a paging loop.
379
+ *
380
+ * The API always leads the list with a `sys.variantType: 'default'` item representing
381
+ * the parent's own base view (either a real record, or the baseline Experience/Fragment
382
+ * itself if none exists) — not a real personalization variant. That entity is already
383
+ * exported as its own top-level Experience/ExperienceFragment, so it's filtered out here
384
+ * to avoid double-counting it as a variant.
385
+ *
386
+ * Runs with bounded concurrency (mirrors getEditorInterfaces' pattern) since this
387
+ * issues one request per parent entity (N+1).
388
+ *
389
+ * Returns the count of parents whose variant fetch failed, so callers can
390
+ * surface that distinctly from a parent that genuinely has zero variants
391
+ * (both look identical as an empty `optimizationVariants` array otherwise).
392
+ */
393
+ async function attachOptimizationVariants({
394
+ parents,
395
+ fetchFn,
396
+ entityLabel
397
+ }) {
398
+ if (!(parents !== null && parents !== void 0 && parents.length)) {
399
+ return 0;
400
+ }
401
+ let totalFetched = 0;
402
+ let failureCount = 0;
403
+ await _bluebird.default.map(parents, async parent => {
404
+ try {
405
+ const response = await fetchFn(parent.sys.id, {});
406
+ parent.optimizationVariants = response.items.filter(item => item.sys.variantType !== 'default');
407
+ totalFetched += parent.optimizationVariants.length;
408
+ _contentfulBatchLibs.logEmitter.emit('info', `Fetched ${totalFetched} ${entityLabel} items`);
409
+ } catch (err) {
410
+ _contentfulBatchLibs.logEmitter.emit('warning', `Skipping ${entityLabel} export for ${parent.sys.id}: ${err.message}`);
411
+ parent.optimizationVariants = [];
412
+ failureCount++;
413
+ }
414
+ }, {
415
+ concurrency: 6
416
+ });
417
+ return failureCount;
418
+ }
419
+
308
420
  /**
309
421
  * Gets all roles. Roles are scheduled to switch from skip/limit to
310
422
  * cursor-based (pages.next/pagePrev) pagination on Feb 15, 2027:
@@ -57,6 +57,10 @@ var _default = exports.default = _yargs.default.version(_package.default.version
57
57
  describe: 'Skip exporting webhooks',
58
58
  type: 'boolean',
59
59
  default: false
60
+ }).option('skip-releases', {
61
+ describe: 'Skip exporting Releases',
62
+ type: 'boolean',
63
+ default: false
60
64
  }).options('strip-tags', {
61
65
  describe: 'Untag assets and entries',
62
66
  type: 'boolean',
@@ -111,6 +115,10 @@ var _default = exports.default = _yargs.default.version(_package.default.version
111
115
  describe: 'Include Experience Orchestration entities (Components, Experience Templates, Data Assemblies, Experience Fragments, Experiences, Design Tokens). Requires exo_m1 entitlement on the source space.',
112
116
  type: 'boolean',
113
117
  default: true
118
+ }).option('include-exo-variants', {
119
+ describe: 'Include Optimization Variants, nested onto their parent Experience/Experience Fragment as `optimizationVariants`. Requires include-experience-orchestration. Only has an effect if the Contentful Personalization app is installed on the source space.',
120
+ type: 'boolean',
121
+ default: false
114
122
  }).option('header', {
115
123
  alias: 'H',
116
124
  type: 'string',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentful-export",
3
- "version": "8.3.0",
3
+ "version": "8.5.0",
4
4
  "description": "this tool allows you to export a space to a JSON dump",
5
5
  "main": "dist/index.js",
6
6
  "types": "types.d.ts",
@@ -52,7 +52,7 @@
52
52
  "cli-table3": "^0.6.0",
53
53
  "contentful": "^11.5.10",
54
54
  "contentful-batch-libs": "^12.0.0",
55
- "contentful-management": "^12.14.0",
55
+ "contentful-management": "^12.17.0",
56
56
  "date-fns": "^4.1.0",
57
57
  "figures": "^3.2.0",
58
58
  "jsonwebtoken": "^9.0.0",