@remkoj/optimizely-cms-cli 5.2.0 → 5.3.1

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/dist/index.js CHANGED
@@ -3,11 +3,12 @@ import dotenv from 'dotenv';
3
3
  import { expand } from 'dotenv-expand';
4
4
  import path from 'node:path';
5
5
  import fs from 'node:fs';
6
- import { getCmsIntegrationApiConfigFromEnvironment, createClient, OptiCmsVersion, IntegrationApi, ContentRoots } from '@remkoj/optimizely-cms-api';
6
+ import { getCmsIntegrationApiConfigFromEnvironment, createClient, ContentBaseType, OptiCmsVersion, PropertyDataType, ContentRoots } from '@remkoj/optimizely-cms-api';
7
7
  import yargs from 'yargs';
8
8
  import chalk from 'chalk';
9
9
  import figures from 'figures';
10
10
  import Table from 'cli-table3';
11
+ import { diff } from 'deep-object-diff';
11
12
  import fsAsync from 'node:fs/promises';
12
13
  import { input, select, confirm } from '@inquirer/prompts';
13
14
 
@@ -79,7 +80,7 @@ function createOptiCmsApp(scriptName, version, epilogue, envFiles, projectDir) {
79
80
  console.error(msg + "\n");
80
81
  if (error)
81
82
  console.error(`[${chalk.bold(error.name ?? 'Error')}]: ${error.message ?? ''}\n`);
82
- args.showHelp("error");
83
+ //args.showHelp("error")
83
84
  });
84
85
  }
85
86
  function isDemanded(value) {
@@ -138,79 +139,6 @@ function createCmsClient(args) {
138
139
  return client;
139
140
  }
140
141
 
141
- const StylesPushCommand = {
142
- command: "styles:push",
143
- describe: "Push Visual Builder style definitions into the CMS (create/replace)",
144
- builder: (yargs) => {
145
- yargs.option('excludeTemplates', { alias: 'e', description: "Exclude these templates", string: true, type: 'array', demandOption: false, default: [] });
146
- yargs.option("templates", { alias: 't', description: "Select only these templates", string: true, type: 'array', demandOption: false, default: [] });
147
- return yargs;
148
- },
149
- handler: async (args) => {
150
- const { _config: cfg, excludeTemplates, templates, ...opts } = parseArgs(args);
151
- const client = createCmsClient(args);
152
- if (client.runtimeCmsVersion == OptiCmsVersion.CMS12) {
153
- process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
154
- return;
155
- }
156
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/replace) DisplayStyles into Optimizely CMS\n`));
157
- const styleDefinitionFiles = await glob("./**/*.opti-style.json", {
158
- cwd: opts.components
159
- });
160
- const results = (await Promise.all(styleDefinitionFiles.map(async (styleDefinitionFile) => {
161
- const filePath = path.normalize(path.join(opts.components, styleDefinitionFile));
162
- const styleDefinition = tryReadJsonFile(filePath, cfg.debug);
163
- const styleKey = styleDefinition.key;
164
- if (!styleKey) {
165
- process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The style definition in ${path.relative(opts.path, filePath)} does not have a key defined\n`));
166
- return undefined;
167
- }
168
- if (excludeTemplates.includes(styleKey))
169
- return undefined; // Skip excluded styles
170
- if (templates.length > 0 && !templates.includes(styleKey))
171
- return undefined; // Only include defined styles, if any
172
- if (cfg.debug)
173
- process.stdout.write(chalk.gray(`${figures.arrowRight} Pushing: ${styleKey}\n`));
174
- const newTemplate = await client.displayTemplates.displayTemplatesPut(styleKey, styleDefinition);
175
- return newTemplate;
176
- }))).filter(isNotNullOrUndefined);
177
- const styles = new Table({
178
- head: [
179
- chalk.yellow(chalk.bold("Name")),
180
- chalk.yellow(chalk.bold("Key")),
181
- chalk.yellow(chalk.bold("Default")),
182
- chalk.yellow(chalk.bold("Target"))
183
- ],
184
- colWidths: [31, 20, 9, 20],
185
- colAligns: ["left", "left", "center", "left"]
186
- });
187
- results.forEach(tpl => {
188
- styles.push([
189
- tpl.displayName,
190
- tpl.key,
191
- tpl.isDefault ? figures.tick : figures.cross,
192
- tpl.contentType ? `${tpl.contentType} (C)` : tpl.baseType ? `${tpl.baseType} (B)` : `${tpl.nodeType} (N)`
193
- ]);
194
- });
195
- process.stdout.write(styles.toString() + "\n");
196
- process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
197
- }
198
- };
199
- function tryReadJsonFile(filePath, debug = false) {
200
- try {
201
- if (debug)
202
- process.stdout.write(chalk.gray(`${figures.arrowRight} Reading style definition from ${filePath}\n`));
203
- return JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' }));
204
- }
205
- catch (e) {
206
- process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} Error while reading ${filePath}\n`));
207
- }
208
- return undefined;
209
- }
210
- function isNotNullOrUndefined(i) {
211
- return i ? true : false;
212
- }
213
-
214
142
  const ContentTypesArgsDefaults = {
215
143
  excludeBaseTypes: [],
216
144
  excludeTypes: ['folder', 'media', 'image', 'video'],
@@ -226,10 +154,8 @@ const contentTypesBuilder = (yargs, defaults = ContentTypesArgsDefaults) => {
226
154
  yargs.option('all', { alias: 'a', description: "Include non-supported base types", boolean: true, type: 'boolean', demandOption: false, default: defaults.all });
227
155
  return yargs;
228
156
  };
229
- function getEnumOptions(enumObject) {
230
- return Object.keys(enumObject).filter((item) => {
231
- return isNaN(Number(item));
232
- });
157
+ function getEnumValues(enumObject) {
158
+ return Object.values(enumObject);
233
159
  }
234
160
  async function getContentTypes(client, args, pageSize = 100, allowSystem = false) {
235
161
  const { _config: cfg, excludeBaseTypes, excludeTypes, baseTypes, types, all } = parseArgs(args);
@@ -250,7 +176,7 @@ async function getContentTypes(client, args, pageSize = 100, allowSystem = false
250
176
  process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
251
177
  process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Content-Types based upon arguments\n`));
252
178
  }
253
- const validBaseTypes = getEnumOptions(IntegrationApi.ContentBaseType).map(x => x.toLowerCase());
179
+ const validBaseTypes = getEnumValues(ContentBaseType);
254
180
  if (cfg.debug)
255
181
  process.stdout.write(`Allowing base types: ${validBaseTypes.join(', ')}\n`);
256
182
  const allContentTypes = all ?
@@ -409,6 +335,269 @@ async function getStyleFilePath(definition, opts) {
409
335
  throw new Error(`Unable to resolve the target for the DisplayTemplate: ${definition.key}`);
410
336
  }
411
337
 
338
+ function normalizeMergePatch(value, omitKeys, requiredKeys = [], requiredSource) {
339
+ if (value === undefined || value === null)
340
+ return null;
341
+ if (Array.isArray(value))
342
+ return value.map(item => normalizeMergePatch(item, omitKeys));
343
+ if (typeof value !== 'object')
344
+ return value;
345
+ const output = Object.entries(value).reduce((acc, [key, entry]) => {
346
+ if (!omitKeys.includes(key))
347
+ acc[key] = normalizeMergePatch(entry, omitKeys);
348
+ return acc;
349
+ }, {});
350
+ const withRequiredKeys = requiredKeys.reduce((acc, key) => {
351
+ if (requiredSource && !Object.keys(acc).includes(key))
352
+ acc[key] = requiredSource[key];
353
+ return acc;
354
+ }, output);
355
+ return withRequiredKeys;
356
+ }
357
+ /**
358
+ * Computes a JSON Merge Patch (RFC 7396) that transforms `currentValue` into `newValue`.
359
+ *
360
+ * The resulting patch can be sent directly as the request body of an `application/merge-patch+json`
361
+ * request. Changed and added properties are included with their new value; removed properties are
362
+ * included with a `null` value (as required by RFC 7396); unchanged properties are omitted entirely.
363
+ *
364
+ * @see https://datatracker.ietf.org/doc/html/rfc7396
365
+ *
366
+ * @example Basic usage
367
+ * ```ts
368
+ * const current = { name: 'Alice', age: 30, role: 'user' }
369
+ * const updated = { name: 'Alice', age: 31 }
370
+ *
371
+ * generatePatch(current, updated)
372
+ * // => { age: 31, role: null }
373
+ * // `age` is replaced, `role` is removed (null), `name` is unchanged (omitted)
374
+ * ```
375
+ *
376
+ * @example Excluding keys from the patch
377
+ * ```ts
378
+ * const current = { id: '123', name: 'Alice', version: 1 }
379
+ * const updated = { id: '123', name: 'Bob', version: 2 }
380
+ *
381
+ * generatePatch(current, updated, { readonlyFields: ['id', 'version'] })
382
+ * // => { name: 'Bob' }
383
+ * // `id` and `version` are excluded even though they are present in the diff
384
+ * ```
385
+ *
386
+ * @example Always include required fields
387
+ * ```ts
388
+ * const current = { id: '123', name: 'Alice', version: 1 }
389
+ * const updated = { id: '123', name: 'Alice', version: 2 }
390
+ *
391
+ * generatePatch(current, updated, { requiredFields: ['id'] })
392
+ * // => { id: '123', version: 2 }
393
+ * // `id` is included even though it did not change
394
+ * ```
395
+ *
396
+ * @example Nested objects
397
+ * ```ts
398
+ * const current = { address: { city: 'Amsterdam', zip: '1000AA' } }
399
+ * const updated = { address: { city: 'Utrecht' } }
400
+ *
401
+ * generatePatch(current, updated)
402
+ * // => { address: { city: 'Utrecht', zip: null } }
403
+ * ```
404
+ *
405
+ * @param currentValue - The current state of the resource.
406
+ * @param newValue - The desired state of the resource.
407
+ * @param options - Optional configuration.
408
+ * @param options.readonlyFields - Keys that should never appear in the generated patch,
409
+ * regardless of whether they changed.
410
+ * @param options.requiredFields - Keys that should always appear in the generated patch,
411
+ * using values from `newValue`.
412
+ * @returns A {@link MergePatch} object ready to be serialised as an `application/merge-patch+json` body.
413
+ */
414
+ function generatePatch(currentValue, newValue, options = {}) {
415
+ const requiredKeys = options.requiredFields ?? [];
416
+ const omitKeys = (options.readonlyFields ?? []).filter(key => !requiredKeys.includes(key));
417
+ const patch = diff(currentValue, newValue);
418
+ return normalizeMergePatch(patch, omitKeys, requiredKeys, newValue);
419
+ }
420
+ /**
421
+ * Extracts all field paths from a {@link MergePatch} as dot-separated strings.
422
+ * Nested objects are flattened recursively; leaf values (including `null`) produce a path entry.
423
+ *
424
+ * @example
425
+ * ```ts
426
+ * getPatchFields({ a: { b: 0 }, c: 'value' })
427
+ * // => ['a.b', 'c']
428
+ * ```
429
+ *
430
+ * @param patch - The merge patch to extract fields from.
431
+ * @param prefix - Internal prefix used during recursion; omit when calling directly.
432
+ * @returns An array of dot-separated field path strings.
433
+ */
434
+ function getPatchFields(patch, prefix = '') {
435
+ return Object.entries(patch).flatMap(([key, value]) => {
436
+ const path = prefix ? `${prefix}.${key}` : key;
437
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
438
+ ? getPatchFields(value, path)
439
+ : [path];
440
+ });
441
+ }
442
+
443
+ /**
444
+ * CLI command that synchronizes local `.opti-style.json` definitions with Optimizely CMS.
445
+ *
446
+ * Existing styles are patched, missing styles are created, and command options allow
447
+ * selecting or excluding template keys during the push.
448
+ */
449
+ const StylesPushCommand = {
450
+ command: 'styles:push',
451
+ describe: 'Push Visual Builder style definitions into the CMS (create/patch)',
452
+ builder: (yargs) => {
453
+ yargs.option('excludeTemplates', {
454
+ alias: 'e',
455
+ description: 'Exclude these templates',
456
+ string: true,
457
+ type: 'array',
458
+ demandOption: false,
459
+ default: [],
460
+ });
461
+ yargs.option('templates', {
462
+ alias: 't',
463
+ description: 'Select only these templates',
464
+ string: true,
465
+ type: 'array',
466
+ demandOption: false,
467
+ default: [],
468
+ });
469
+ return yargs;
470
+ },
471
+ handler: async (args) => {
472
+ const { _config: { debug = false }, excludeTemplates, templates, ...opts } = parseArgs(args);
473
+ const client = createCmsClient(args);
474
+ if (client.runtimeCmsVersion == OptiCmsVersion.CMS12) {
475
+ process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
476
+ return;
477
+ }
478
+ const { styles: displayTemplates } = await getStyles(client, {
479
+ // Defaults
480
+ all: true,
481
+ baseTypes: [],
482
+ excludeBaseTypes: [],
483
+ excludeNodeTypes: [],
484
+ excludeTemplates: [],
485
+ excludeTypes: [],
486
+ nodes: [],
487
+ templates: [],
488
+ templateTypes: [],
489
+ types: [],
490
+ // Arguments from this method
491
+ ...args,
492
+ }, 50);
493
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/patch) DisplayStyles into Optimizely CMS\n`));
494
+ // Listing all style files
495
+ const styleDefinitionFiles = await glob('./**/*.opti-style.json', {
496
+ cwd: opts.components,
497
+ });
498
+ // Helper function to read the files from disk
499
+ function readStyleFile(styleDefinitionFile) {
500
+ const filePath = path.normalize(path.join(opts.components, styleDefinitionFile));
501
+ const styleDefinition = tryReadJsonFile(filePath, debug);
502
+ return styleDefinition ? {
503
+ styleKey: styleDefinition.key,
504
+ styleDefinition,
505
+ styleFile: filePath
506
+ } : undefined;
507
+ }
508
+ // Helper function to filter the files read from disk
509
+ function filterStyleDefinition(data) {
510
+ if (!data || !data.styleKey)
511
+ return false;
512
+ const { styleKey } = data;
513
+ if (excludeTemplates.includes(styleKey)) {
514
+ if (debug)
515
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${styleKey} - explicitly excluded\n`));
516
+ return false;
517
+ }
518
+ // Skip if there a selectin and this template is not in it
519
+ if (templates.length > 0 && !templates.includes(styleKey)) {
520
+ if (debug)
521
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${styleKey} - not in selected list of templates\n`));
522
+ return false; // Only include defined styles, if any
523
+ }
524
+ return true;
525
+ }
526
+ // Processing each file
527
+ const results = await Promise.allSettled(styleDefinitionFiles
528
+ .map(readStyleFile)
529
+ .filter(filterStyleDefinition)
530
+ .map(async ({ styleKey, styleDefinition, styleFile }) => {
531
+ const displayTemplate = displayTemplates.find(dt => dt.key === styleKey);
532
+ // Confirm we're including
533
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} ${displayTemplate ? 'Updating' : 'Creating'} ${styleKey}\n`));
534
+ if (debug)
535
+ process.stdout.write(chalk.gray(`${figures.arrowRight} from ${styleFile || 'unknown source'}\n`));
536
+ // Patch or create the template
537
+ const newTemplate = await (displayTemplate ? (async () => {
538
+ const patch = generatePatch(displayTemplate, styleDefinition, {
539
+ readonlyFields: ['key', 'created', 'createdBy', 'lastModified', 'lastModifiedBy'],
540
+ });
541
+ if (!path || Object.entries(patch).length === 0)
542
+ return displayTemplate;
543
+ // @ts-expect-error: We're expecting an error here due to the CMS Logic not matching
544
+ // the OpenAPI Specification.
545
+ return client.displayTemplates.displayTemplatesPatch(styleKey, patch);
546
+ })() : client.displayTemplates.displayTemplatesCreate(displayTemplate));
547
+ // Validate the result
548
+ const unpatchedFields = generatePatch(newTemplate, styleDefinition, {
549
+ readonlyFields: ['key', 'created', 'createdBy', 'lastModified', 'lastModifiedBy']
550
+ });
551
+ if (unpatchedFields && Object.entries(unpatchedFields).length > 0)
552
+ throw new Error(`Creating/patching of displayTemplate failed, the following fields failed: ${getPatchFields(unpatchedFields).join('; ')}`);
553
+ // Return the template after Create/Patch
554
+ return newTemplate;
555
+ }));
556
+ const styles = new Table({
557
+ head: [
558
+ chalk.yellow(chalk.bold('Name')),
559
+ chalk.yellow(chalk.bold('Key')),
560
+ chalk.yellow(chalk.bold('Default')),
561
+ chalk.yellow(chalk.bold('Target')),
562
+ ],
563
+ colWidths: [31, 20, 9, 20],
564
+ colAligns: ['left', 'left', 'center', 'left'],
565
+ });
566
+ results.forEach((result) => {
567
+ if (result.status === 'fulfilled') {
568
+ const tpl = result.value;
569
+ styles.push([
570
+ tpl.displayName,
571
+ tpl.key,
572
+ tpl.isDefault ? figures.tick : figures.cross,
573
+ tpl.contentType
574
+ ? `${tpl.contentType} (C)`
575
+ : tpl.baseType
576
+ ? `${tpl.baseType} (B)`
577
+ : `${tpl.nodeType} (N)`,
578
+ ]);
579
+ }
580
+ else {
581
+ const error = result.reason;
582
+ process.stderr.write(`Error creating/updating style: ${error}`);
583
+ }
584
+ });
585
+ process.stdout.write(styles.toString() + '\n');
586
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + ' Done')) + '\n');
587
+ },
588
+ };
589
+ function tryReadJsonFile(filePath, debug = false) {
590
+ try {
591
+ if (debug)
592
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Reading style definition from ${filePath}\n`));
593
+ return JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' }));
594
+ }
595
+ catch (e) {
596
+ process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} Error while reading ${filePath}\n`));
597
+ }
598
+ return undefined;
599
+ }
600
+
412
601
  const StylesListCommand = {
413
602
  command: "styles:list",
414
603
  describe: "List Visual Builder style definitions from the CMS",
@@ -442,6 +631,17 @@ const StylesListCommand = {
442
631
  }
443
632
  };
444
633
 
634
+ function ucFirst$1(current) {
635
+ if (typeof current != 'string')
636
+ throw new Error("Only strings can be transformed");
637
+ if (current == "")
638
+ return current;
639
+ return current[0]?.toUpperCase() + current.substring(1);
640
+ }
641
+ function trimLeadingUnderscore(value) {
642
+ return value?.startsWith('_') ? value.substring(1) : value;
643
+ }
644
+
445
645
  const StylesPullCommand = {
446
646
  command: "styles:pull",
447
647
  describe: "Create Visual Builder style definitions from the CMS",
@@ -511,7 +711,7 @@ const StylesPullCommand = {
511
711
  process.stdout.write(chalk.green(chalk.bold(figures.tick + ` Created/updated style definitions for ${updatedTemplates.join(', ')}`)) + "\n");
512
712
  }
513
713
  };
514
- function ucFirst$1(input) {
714
+ function ucFirst(input) {
515
715
  if (typeof (input) != 'string' || input.length < 1)
516
716
  return input;
517
717
  return input[0].toUpperCase() + input.substring(1);
@@ -542,14 +742,14 @@ async function createTemplateMetadata(client, displayTemplate, basePath, createP
542
742
  targetType = targetPrefix + '/' + displayTemplate.nodeType;
543
743
  break;
544
744
  case 'base':
545
- itemPath = path.join(basePath, displayTemplate.baseType, 'styles', displayTemplate.key);
546
- typesPath = path.join(basePath, displayTemplate.baseType, 'styles');
547
- targetType = targetPrefix + '/' + displayTemplate.baseType;
745
+ itemPath = path.join(basePath, trimLeadingUnderscore(displayTemplate.baseType), 'styles', displayTemplate.key);
746
+ typesPath = path.join(basePath, trimLeadingUnderscore(displayTemplate.baseType), 'styles');
747
+ targetType = targetPrefix + '/' + trimLeadingUnderscore(displayTemplate.baseType);
548
748
  break;
549
749
  case 'content':
550
750
  const contentType = await client.contentTypes.contentTypesGet(displayTemplate.contentType ?? '-');
551
- itemPath = path.join(basePath, contentType.baseType, contentType.key.split(':').pop());
552
- typesPath = path.join(basePath, contentType.baseType, contentType.key.split(':').pop());
751
+ itemPath = path.join(basePath, trimLeadingUnderscore(contentType.baseType), contentType.key.split(':').pop());
752
+ typesPath = path.join(basePath, trimLeadingUnderscore(contentType.baseType), contentType.key.split(':').pop());
553
753
  targetType = targetPrefix + '/' + displayTemplate.contentType;
554
754
  break;
555
755
  default:
@@ -624,7 +824,7 @@ async function createDisplayTemplateHelper(typeFile, typeFileId, force = false,
624
824
  typeId = displayTemplate.nodeType ?? displayTemplate.baseType ?? displayTemplate.contentType;
625
825
  });
626
826
  if (typeId) {
627
- typeId = ucFirst$1(typeId);
827
+ typeId = ucFirst(typeId);
628
828
  typeContents.push('');
629
829
  typeContents.push(`export type ${typeId}LayoutProps = ${props.join(' | ')}
630
830
  export type ${typeId}ComponentProps<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = {
@@ -831,7 +1031,8 @@ const TypesPullCommand = {
831
1031
  const client = createCmsClient(args);
832
1032
  const { contentTypes } = await getContentTypes(client, args);
833
1033
  const updatedTypes = contentTypes.map(contentType => {
834
- const typePath = path.join(basePath, contentType.baseType, contentType.key.split(':').pop());
1034
+ const baseType = contentType.baseType?.startsWith('_') ? contentType.baseType?.substring(1) : contentType.baseType;
1035
+ const typePath = path.join(basePath, baseType, contentType.key.split(':').pop());
835
1036
  const typeFile = path.join(typePath, `${contentType.key.split(':').pop()}.opti-type.json`);
836
1037
  if (!fs.existsSync(typePath))
837
1038
  fs.mkdirSync(typePath, { recursive: true });
@@ -864,7 +1065,7 @@ const TypesPullCommand = {
864
1065
 
865
1066
  const TypesPushCommand = {
866
1067
  command: "types:push",
867
- describe: "Push content type definition into Optimizely CMS (create / replace)",
1068
+ describe: "Push content type definition into Optimizely CMS (create / patch)",
868
1069
  builder: (yargs) => {
869
1070
  yargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
870
1071
  yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
@@ -877,7 +1078,7 @@ const TypesPushCommand = {
877
1078
  const { _config: cfg, components: basePath, excludeBaseTypes, excludeTypes, baseTypes, types, force } = parseArgs(args);
878
1079
  const client = createClient(cfg);
879
1080
  // Find all type files
880
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/replace) Content Types into Optimizely CMS\n`));
1081
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/patch) Content Types into Optimizely CMS\n`));
881
1082
  const typeDefinitionFiles = await glob("./**/*.opti-type.json", { cwd: basePath });
882
1083
  // Read & filter all identified type files
883
1084
  const typeDefinitions = typeDefinitionFiles.map(file => {
@@ -907,7 +1108,7 @@ const TypesPushCommand = {
907
1108
  delete outType.features;
908
1109
  if (outType.usage)
909
1110
  delete outType.usage;
910
- return client.contentTypes.contentTypesPut(outType.key, outType, force)
1111
+ return client.contentTypes.contentTypesPatch(outType.key, outType, force)
911
1112
  .then(ct => { return { key: type.definition.key, type: ct, file: type.file }; })
912
1113
  .catch(e => { return { key: type.definition.key, type: type.definition, file: type.file, error: e }; });
913
1114
  }));
@@ -942,7 +1143,7 @@ const builder = yargs => {
942
1143
  };
943
1144
  function createTypeFolders(contentTypes, basePath, debug = false) {
944
1145
  const folders = contentTypes.map(contentType => {
945
- const baseType = contentType.baseType ?? 'default';
1146
+ const baseType = trimLeadingUnderscore(contentType?.baseType ?? 'default');
946
1147
  // Create the type folder
947
1148
  const typePath = path.join(basePath, baseType, contentType.key.split(':').pop());
948
1149
  if (!fs.existsSync(typePath)) {
@@ -1013,7 +1214,7 @@ const NextJsFragmentsCommand = {
1013
1214
  };
1014
1215
  function createGraphFragments(contentType, typePath, basePath, force, debug, contentTypes, generatedProps = [], forCms12 = false) {
1015
1216
  const returnValue = [];
1016
- const baseType = contentType.baseType ?? 'default';
1217
+ const baseType = trimLeadingUnderscore(contentType.baseType ?? 'default');
1017
1218
  const baseQueryFile = path.join(typePath, `${contentType.key.split(':').pop()}.${baseType}.graphql`);
1018
1219
  //console.log('Mapping', contentType.key, baseQueryFile);
1019
1220
  if (fs.existsSync(baseQueryFile)) {
@@ -1043,7 +1244,7 @@ function createGraphFragments(contentType, typePath, basePath, force, debug, con
1043
1244
  return;
1044
1245
  }
1045
1246
  const fullTypeName = forCms12 ? contentType.key + propContentType.key : propContentType.key;
1046
- const propertyFragmentFile = path.join(basePath, propContentType.baseType, propContentType.key.split(':').pop(), `${fullTypeName.split(':').pop()}.property.graphql`);
1247
+ const propertyFragmentFile = path.join(basePath, trimLeadingUnderscore(propContentType.baseType), propContentType.key.split(':').pop(), `${fullTypeName.split(':').pop()}.property.graphql`);
1047
1248
  const propertyFragmentDir = path.dirname(propertyFragmentFile);
1048
1249
  if (!fs.existsSync(propertyFragmentDir))
1049
1250
  fs.mkdirSync(propertyFragmentDir, { recursive: true });
@@ -1090,11 +1291,11 @@ function renderProperties(contentType, generatedProps = [], forCms12 = false) {
1090
1291
  const propName = isConflict ? `${contentType.key}${propKey}: ${propKey}` : propKey;
1091
1292
  // Write the property
1092
1293
  switch (propType) {
1093
- case IntegrationApi.PropertyDataType.ARRAY:
1294
+ case PropertyDataType.ARRAY:
1094
1295
  {
1095
1296
  const typeData = typeProps[propKey];
1096
1297
  switch (typeData.items.type) {
1097
- case IntegrationApi.PropertyDataType.INTEGER:
1298
+ case PropertyDataType.INTEGER:
1098
1299
  if (typeData.format == 'categorization') {
1099
1300
  //fragmentFields.push(`${propName} { Id, Name, Description }`)
1100
1301
  console.warn(chalk.redBright(`❗ Property ${propName} is a 'categorization', this is not supported. If you need to use this property, add it manually into the generated files`));
@@ -1102,16 +1303,16 @@ function renderProperties(contentType, generatedProps = [], forCms12 = false) {
1102
1303
  else
1103
1304
  fragmentFields.push(propName);
1104
1305
  break;
1105
- case IntegrationApi.PropertyDataType.STRING:
1306
+ case PropertyDataType.STRING:
1106
1307
  fragmentFields.push(propName);
1107
1308
  break;
1108
- case IntegrationApi.PropertyDataType.CONTENT:
1309
+ case PropertyDataType.CONTENT:
1109
1310
  if (contentType.baseType == 'page' || contentType.baseType == 'experience')
1110
1311
  fragmentFields.push(`${propName} { ...${forCms12 ? 'PageIContentListItem' : 'BlockData'} }`);
1111
1312
  else
1112
1313
  fragmentFields.push(`${propName} { ...IContentListItem }`);
1113
1314
  break;
1114
- case IntegrationApi.PropertyDataType.COMPONENT:
1315
+ case PropertyDataType.COMPONENT:
1115
1316
  const componentType = typeData.items.contentType.split(':').pop();
1116
1317
  switch (componentType) {
1117
1318
  case 'link':
@@ -1126,7 +1327,7 @@ function renderProperties(contentType, generatedProps = [], forCms12 = false) {
1126
1327
  }
1127
1328
  }
1128
1329
  break;
1129
- case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
1330
+ case PropertyDataType.CONTENT_REFERENCE:
1130
1331
  fragmentFields.push(`${propName} { ...ReferenceData }`);
1131
1332
  break;
1132
1333
  default:
@@ -1135,7 +1336,11 @@ function renderProperties(contentType, generatedProps = [], forCms12 = false) {
1135
1336
  }
1136
1337
  break;
1137
1338
  }
1138
- case IntegrationApi.PropertyDataType.STRING: {
1339
+ case 'richText': {
1340
+ fragmentFields.push(forCms12 ? `${propName} { Structure, Html }` : `${propName} { json, html }`);
1341
+ break;
1342
+ }
1343
+ case PropertyDataType.STRING: {
1139
1344
  const propDetails = typeProps[propKey];
1140
1345
  switch (propDetails.format ?? "") {
1141
1346
  case 'html':
@@ -1153,13 +1358,17 @@ function renderProperties(contentType, generatedProps = [], forCms12 = false) {
1153
1358
  }
1154
1359
  break;
1155
1360
  }
1156
- case IntegrationApi.PropertyDataType.URL:
1361
+ case PropertyDataType.URL:
1157
1362
  fragmentFields.push(forCms12 ? propName : `${propName} { ...LinkData }`);
1158
1363
  break;
1159
- case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
1364
+ case PropertyDataType.CONTENT_REFERENCE:
1160
1365
  fragmentFields.push(`${propName} { ...ReferenceData }`);
1161
1366
  break;
1162
- case IntegrationApi.PropertyDataType.COMPONENT: {
1367
+ case "link": {
1368
+ fragmentFields.push(`${propName} { ...LinkItemData }`);
1369
+ break;
1370
+ }
1371
+ case PropertyDataType.COMPONENT: {
1163
1372
  const componentType = typeProps[propKey].contentType.split(':').pop();
1164
1373
  if (componentType == "link") {
1165
1374
  fragmentFields.push(`${propName} { ...LinkItemData }`);
@@ -1171,10 +1380,10 @@ function renderProperties(contentType, generatedProps = [], forCms12 = false) {
1171
1380
  }
1172
1381
  break;
1173
1382
  }
1174
- case IntegrationApi.PropertyDataType.BINARY:
1383
+ case PropertyDataType.BINARY:
1175
1384
  fragmentFields.push(propName);
1176
1385
  break;
1177
- case IntegrationApi.PropertyDataType.CONTENT:
1386
+ case PropertyDataType.CONTENT:
1178
1387
  fragmentFields.push(`${propName} { ...${forCms12 ? 'PageIContentListItem' : 'BlockData'} }`);
1179
1388
  break;
1180
1389
  default:
@@ -1210,14 +1419,6 @@ function getPropDataType(baseInfo) {
1210
1419
  }
1211
1420
  }
1212
1421
 
1213
- function ucFirst(current) {
1214
- if (typeof current != 'string')
1215
- throw new Error("Only strings can be transformed");
1216
- if (current == "")
1217
- return current;
1218
- return current[0]?.toUpperCase() + current.substring(1);
1219
- }
1220
-
1221
1422
  const NextJsComponentsCommand = {
1222
1423
  command: "nextjs:components",
1223
1424
  describe: "Create the React Components for a Next.JS / Optimizely Graph structure",
@@ -1259,7 +1460,7 @@ function createComponent(contentType, typePath, force, debug = false) {
1259
1460
  // Get type information & short-hands
1260
1461
  const displayTemplate = getDisplayTemplateInfo$1(contentType, typePath);
1261
1462
  const baseDisplayTemplate = getBaseTypeTemplateInfo(contentType, typePath);
1262
- const varName = `${contentType.key.split(':').pop()}${ucFirst(contentType.baseType ?? 'part')}`;
1463
+ const varName = `${contentType.key.split(':').pop()}${ucFirst$1(contentType.baseType ?? 'part')}`;
1263
1464
  const tplFn = Templates[contentType.baseType] ?? Templates['default'];
1264
1465
  if (!tplFn) {
1265
1466
  if (debug)
@@ -1316,12 +1517,12 @@ export const ${varName} : CmsComponent<${contentType.key.split(':').pop()}DataFr
1316
1517
  { children }` : ''}
1317
1518
  </CmsEditable>
1318
1519
  }
1319
- ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1520
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst$1(contentType.baseType)}/${contentType.key})"
1320
1521
  ${varName}.getDataFragment = () => ['${contentType.key.split(':').pop()}Data', ${contentType.key.split(':').pop()}DataFragmentDoc]
1321
1522
 
1322
1523
  export default ${varName}`,
1323
1524
  // Template for all page component types
1324
- page: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
1525
+ _page: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
1325
1526
  import { ${contentType.key.split(':').pop()}DataFragmentDoc, type ${contentType.key.split(':').pop()}DataFragment } from "@/gql/graphql";${displayTemplate ? `
1326
1527
  import { ${displayTemplate} } from "./displayTemplates";` : ''}
1327
1528
  import { getSdk } from "@/gql"
@@ -1339,7 +1540,7 @@ export const ${varName} : CmsComponent<${contentType.key.split(':').pop()}DataFr
1339
1540
  { Object.getOwnPropertyNames(data).length > 0 && <pre className="w-full overflow-x-hidden font-mono text-sm bg-slate-200 p-2 rounded-sm border border-solid border-slate-900 text-slate-900">{ JSON.stringify(data, undefined, 4) }</pre> }
1340
1541
  </div>
1341
1542
  }
1342
- ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1543
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst$1(contentType.baseType)}/${contentType.key})"
1343
1544
  ${varName}.getDataFragment = () => ['${contentType.key.split(':').pop()}Data', ${contentType.key.split(':').pop()}DataFragmentDoc]
1344
1545
  ${varName}.getMetaData = async (contentLink, locale, client) => {
1345
1546
  const sdk = getSdk(client);
@@ -1349,7 +1550,7 @@ ${varName}.getMetaData = async (contentLink, locale, client) => {
1349
1550
 
1350
1551
  export default ${varName}`,
1351
1552
  // Template for all experience component types
1352
- experience: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
1553
+ _experience: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
1353
1554
  import { getFragmentData } from "@/gql/fragment-masking";
1354
1555
  import { ExperienceDataFragmentDoc, ${contentType.key.split(':').pop()}DataFragmentDoc, type ${contentType.key.split(':').pop()}DataFragment } from "@/gql/graphql";
1355
1556
  import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
@@ -1366,7 +1567,7 @@ export const ${varName} : CmsComponent<${contentType.key.split(':').pop()}DataFr
1366
1567
  { composition && isNode(composition) && <OptimizelyComposition node={composition} ctx={ctx} /> }
1367
1568
  </CmsEditable>
1368
1569
  }
1369
- ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1570
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst$1(contentType.baseType)}/${contentType.key})"
1370
1571
  ${varName}.getDataFragment = () => ['${contentType.key.split(':').pop()}Data', ${contentType.key}DataFragmentDoc]
1371
1572
  ${varName}.getMetaData = async (contentLink, locale, client) => {
1372
1573
  const sdk = getSdk(client);
@@ -1396,7 +1597,7 @@ const NextJsVisualBuilderCommand = {
1396
1597
  });
1397
1598
  // Process base styles
1398
1599
  styles.filter(x => typeof (x.baseType) == 'string' && x.baseType.length > 0).map(styleDefinition => {
1399
- const templatePath = path.join(basePath, styleDefinition.baseType, 'styles', styleDefinition.key);
1600
+ const templatePath = path.join(basePath, trimLeadingUnderscore(styleDefinition.baseType), 'styles', styleDefinition.key);
1400
1601
  createSpecificNode(styleDefinition, templatePath, force, debug);
1401
1602
  });
1402
1603
  }
@@ -1522,7 +1723,7 @@ const NextJsFactoryCommand = {
1522
1723
  const componentFactoryDefintions = new Map();
1523
1724
  components.forEach(component => {
1524
1725
  // Determine component target
1525
- const componentKey = processName(component.length == 1 ? ucFirst(path.basename(component[0], path.extname(component[0]))) : component.at(component.length - 2));
1726
+ const componentKey = processName(component.length == 1 ? ucFirst$1(path.basename(component[0], path.extname(component[0]))) : component.at(component.length - 2));
1526
1727
  let componentVariant = (component.length > 1 ? component.at(component.length - 1) ?? 'default' : 'default').replace('index', 'default');
1527
1728
  componentVariant = path.basename(componentVariant, path.extname(componentVariant));
1528
1729
  const componentDir = path.dirname(path.join(...component));
@@ -1624,7 +1825,7 @@ function processName(input) {
1624
1825
  if (input == ROOT_FACTORY_KEY)
1625
1826
  return "Cms";
1626
1827
  const nameSegements = input.split(/[-\_]/g);
1627
- return nameSegements.map(ucFirst).join('');
1828
+ return nameSegements.map(ucFirst$1).join('');
1628
1829
  }
1629
1830
  function generateFactory(factoryInfo, factoryKey) {
1630
1831
  // Get the factory name
@@ -2169,7 +2370,7 @@ const commands = [
2169
2370
  CmsVersionCommand
2170
2371
  ];
2171
2372
 
2172
- var version = "5.2.0";
2373
+ var version = "5.3.1";
2173
2374
  var name = "opti-cms";
2174
2375
  var APP = {
2175
2376
  version: version,