@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/README.md +232 -32
- package/dist/index.js +325 -124
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
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,
|
|
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
|
|
230
|
-
return Object.
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 /
|
|
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/
|
|
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.
|
|
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
|
|
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
|
|
1294
|
+
case PropertyDataType.ARRAY:
|
|
1094
1295
|
{
|
|
1095
1296
|
const typeData = typeProps[propKey];
|
|
1096
1297
|
switch (typeData.items.type) {
|
|
1097
|
-
case
|
|
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
|
|
1306
|
+
case PropertyDataType.STRING:
|
|
1106
1307
|
fragmentFields.push(propName);
|
|
1107
1308
|
break;
|
|
1108
|
-
case
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1361
|
+
case PropertyDataType.URL:
|
|
1157
1362
|
fragmentFields.push(forCms12 ? propName : `${propName} { ...LinkData }`);
|
|
1158
1363
|
break;
|
|
1159
|
-
case
|
|
1364
|
+
case PropertyDataType.CONTENT_REFERENCE:
|
|
1160
1365
|
fragmentFields.push(`${propName} { ...ReferenceData }`);
|
|
1161
1366
|
break;
|
|
1162
|
-
case
|
|
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
|
|
1383
|
+
case PropertyDataType.BINARY:
|
|
1175
1384
|
fragmentFields.push(propName);
|
|
1176
1385
|
break;
|
|
1177
|
-
case
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2373
|
+
var version = "5.3.1";
|
|
2173
2374
|
var name = "opti-cms";
|
|
2174
2375
|
var APP = {
|
|
2175
2376
|
version: version,
|