@remkoj/optimizely-cms-cli 6.0.0-pre8 → 6.0.0-pre9

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
@@ -1,18 +1,19 @@
1
1
  import { globSync, glob, globIterate } from 'glob';
2
2
  import { config } from 'dotenv';
3
3
  import { expand } from 'dotenv-expand';
4
- import { readPartialEnvConfig, getCmsIntegrationApiConfigFromEnvironment, createClient, OptiCmsVersion, IntegrationApi, ContentRoots } from '@remkoj/optimizely-cms-api';
4
+ import { readPartialEnvConfig, createClient, OptiCmsVersion, ContentRoots } from '@remkoj/optimizely-cms-api';
5
5
  import yargs from 'yargs';
6
6
  import chalk from 'chalk';
7
7
  import path from 'node:path';
8
8
  import fs from 'node:fs';
9
9
  import figures from 'figures';
10
10
  import Table from 'cli-table3';
11
+ import fsAsync from 'node:fs/promises';
11
12
  import { input, select, confirm } from '@inquirer/prompts';
12
- import fs$1 from 'node:fs/promises';
13
13
  import createDeepMerge from '@fastify/deepmerge';
14
14
  import { Ajv } from 'ajv';
15
15
  import addFormats from 'ajv-formats';
16
+ import GraphQLGen from '@remkoj/optimizely-graph-functions/contenttype-loader';
16
17
  import deepEqual from 'fast-deep-equal';
17
18
 
18
19
  /**
@@ -50,7 +51,7 @@ function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
50
51
  .usage('$0 <cmd> [args]')
51
52
  .option("path", { alias: "p", description: "Application root folder", string: true, type: "string", demandOption: false, default: process.cwd() })
52
53
  .option("components", { alias: "c", description: "Path to components folder", string: true, type: "string", demandOption: false, default: "./src/components/cms" })
53
- .option("cms_url", { alias: "cu", description: "Optimizely CMS URL", string: true, type: "string", demandOption: isDemanded(config.base), default: config.base, coerce: (val) => new URL(val) })
54
+ .option("cms_url", { alias: "cu", description: "Optimizely CMS URL", string: true, type: "string", demandOption: isDemanded(config.base), default: config.base, coerce: (val) => val ? new URL(val) : undefined })
54
55
  .option("client_id", { alias: "ci", description: "API Client ID", string: true, type: "string", demandOption: isDemanded(config.clientId), default: config.clientId })
55
56
  .option('client_secret', { alias: "cs", description: "API Client Secrent", string: true, type: "string", demandOption: isDemanded(config.clientSecret), default: config.clientSecret })
56
57
  .option('user_id', { alias: "u", description: "Impersonate user id", string: true, type: "string", demandOption: false, default: config.actAs })
@@ -115,13 +116,13 @@ function parseArgs({ client_id, client_secret, cms_url, user_id, verbose, path:
115
116
 
116
117
  function createCmsClient(args) {
117
118
  const cfg = getCmsIntegrationApiOptions(args);
118
- const baseConfig = getCmsIntegrationApiConfigFromEnvironment();
119
+ const baseConfig = readPartialEnvConfig();
119
120
  const client = createClient({
120
121
  ...baseConfig,
121
122
  ...cfg
122
123
  });
123
124
  if (cfg.debug)
124
- process.stdout.write(chalk.gray(`${figures.arrowRight} Connecting to ${cfg.base.href} as ${cfg.actAs ?? cfg.clientId}\n`));
125
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Connecting to ${client.cmsUrl} as ${cfg.actAs ?? cfg.clientId}\n`));
125
126
  return client;
126
127
  }
127
128
  function getCmsIntegrationApiOptions(args) {
@@ -165,7 +166,12 @@ const StylesPushCommand = {
165
166
  return undefined; // Only include defined styles, if any
166
167
  if (cfg.debug)
167
168
  process.stdout.write(chalk.gray(`${figures.arrowRight} Pushing: ${styleKey}\n`));
168
- const newTemplate = await client.displayTemplatesPut({ path: { key: styleKey }, body: styleDefinition });
169
+ // Try to fetch the current template
170
+ const currentTemplate = await client.displayTemplatesGet({ path: { key: styleKey } }).catch(() => { return undefined; });
171
+ // Create / Replace the current template
172
+ const newTemplate = await (currentTemplate ?
173
+ client.displayTemplatesPatch({ path: { key: styleKey }, body: styleDefinition }) :
174
+ client.displayTemplatesCreate({ body: styleDefinition }));
169
175
  return newTemplate;
170
176
  }))).filter(isNotNullOrUndefined);
171
177
  const styles = new Table({
@@ -205,64 +211,112 @@ function isNotNullOrUndefined(i) {
205
211
  return i ? true : false;
206
212
  }
207
213
 
208
- const contentTypesBuilder = yargs => {
209
- yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
210
- yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
211
- yargs.option("baseTypes", { alias: 'b', description: "Select only these base types", string: true, type: 'array', demandOption: false, default: [] });
212
- yargs.option("types", { alias: 't', description: "Select only these types", string: true, type: 'array', demandOption: false, default: [] });
213
- yargs.option('all', { alias: 'a', description: "Include non-supported base types", boolean: true, type: 'boolean', demandOption: false, default: false });
214
+ /**
215
+ * This file contains tools that allow using the project that we're targeting
216
+ */
217
+ /**
218
+ * Get all the file paths for the content type, taking the current project configuration into
219
+ * account.
220
+ *
221
+ * @param contentType
222
+ * @param basePath
223
+ * @returns
224
+ */
225
+ function getContentTypePaths(contentType, basePath, createFolder = false, debug = false) {
226
+ const baseTypeSlug = typeToSlug(contentType.baseType);
227
+ const typePath = path.join(basePath, baseTypeSlug, contentType.key);
228
+ if (createFolder) {
229
+ if (!fs.existsSync(typePath)) {
230
+ fs.mkdirSync(typePath, { recursive: true });
231
+ if (debug)
232
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Created folder ${typePath} for Content-Type ${contentType.key}\n`));
233
+ }
234
+ // Check folders
235
+ if (!fs.statSync(typePath).isDirectory())
236
+ throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
237
+ }
238
+ const typeFile = path.join(typePath, `${contentType.key}.opti-type.json`);
239
+ const fragmentFile = path.join(typePath, `${contentType.key}.${baseTypeSlug}.graphql`);
240
+ const propertyFragmentFile = path.join(typePath, `${contentType.key}.property.graphql`);
241
+ const queryFile = path.join(typePath, `get${contentType.key}Data.query.graphql`);
242
+ const componentFile = path.join(typePath, `index.tsx`);
243
+ return {
244
+ type: contentType.key,
245
+ path: typePath,
246
+ typePath,
247
+ typeFile,
248
+ fragmentFile,
249
+ componentFile,
250
+ propertyFragmentFile,
251
+ queryFile
252
+ };
253
+ }
254
+ /**
255
+ *
256
+ * @param typeKey
257
+ * @returns
258
+ */
259
+ function typeToSlug(typeKey) {
260
+ let typeKeySlug = typeKey.toLowerCase();
261
+ if (typeKeySlug.startsWith('_'))
262
+ typeKeySlug = typeKeySlug.substring(1);
263
+ return typeKeySlug;
264
+ }
265
+
266
+ const ContentTypesArgsDefaults = {
267
+ excludeBaseTypes: ['folder', 'media', 'image', 'video'],
268
+ excludeTypes: [],
269
+ baseTypes: [],
270
+ types: [],
271
+ all: false
272
+ };
273
+ const contentTypesBuilder = (yargs, defaults = ContentTypesArgsDefaults) => {
274
+ yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: defaults.excludeTypes });
275
+ yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: defaults.excludeBaseTypes });
276
+ yargs.option("baseTypes", { alias: 'b', description: "Select only these base types", string: true, type: 'array', demandOption: false, default: defaults.baseTypes });
277
+ yargs.option("types", { alias: 't', description: "Select only these types", string: true, type: 'array', demandOption: false, default: defaults.types });
278
+ yargs.option('all', { alias: 'a', description: "Include non-supported base types", boolean: true, type: 'boolean', demandOption: false, default: defaults.all });
214
279
  return yargs;
215
280
  };
216
- function getEnumOptions(enumObject) {
217
- return Object.keys(enumObject).filter((item) => {
218
- return isNaN(Number(item));
219
- });
220
- }
221
- async function getContentTypes(client, args, pageSize = 5, allowSystem = false, customFilter) {
281
+ async function getContentTypes(client, args, pageSize = 25, allowSystem = false, customFilter) {
222
282
  const { _config: cfg, excludeBaseTypes, excludeTypes, baseTypes, types, all } = parseArgs(args);
223
- const validBaseTypes = getEnumOptions(IntegrationApi.ContentBaseType).map(x => x.toLowerCase());
224
283
  const allContentTypes = [];
225
284
  const filteredContentTypes = [];
226
285
  for await (const contentType of getAllContentTypes(client, cfg.debug, pageSize)) {
227
- const normalizedContentType = { ...contentType, baseType: contentType.baseType ?? 'default' };
228
- // Build the "All" data set
229
- if (all)
230
- allContentTypes.push(normalizedContentType);
231
- else if (validBaseTypes.includes(normalizedContentType.baseType.toLowerCase()))
232
- allContentTypes.push(normalizedContentType);
233
- else if (cfg.debug) {
234
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${normalizedContentType.key} as it has an unsupported base type: ${normalizedContentType.baseType}\n`));
235
- continue;
236
- }
237
- else {
286
+ // Skip content types mapped against Graph data, these should be used with their source type in Graph, not the reference in CMS
287
+ if (!all && contentType.key.toLowerCase().startsWith('graph:')) {
288
+ if (cfg.debug)
289
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it is a reference to external data in Optimizely Graph\n`));
238
290
  continue;
239
291
  }
292
+ // Build the unfiltered array
293
+ allContentTypes.push(contentType);
240
294
  // Skip based upon base type filters
241
- if (!shouldInclude(normalizedContentType.baseType, baseTypes, excludeBaseTypes)) {
295
+ if (!shouldInclude(typeToSlug(contentType.baseType), baseTypes.map(x => typeToSlug(x)), excludeBaseTypes.map(x => typeToSlug(x)))) {
242
296
  if (cfg.debug)
243
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${normalizedContentType.key} as it has a restricted base type: ${normalizedContentType.baseType}\n`));
297
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it has a restricted base type: ${contentType.baseType}\n`));
244
298
  continue;
245
299
  }
246
300
  // Skip based upon type filters
247
- if (!shouldInclude(normalizedContentType.key, types, excludeTypes)) {
301
+ if (!shouldInclude(contentType.key, types, excludeTypes)) {
248
302
  if (cfg.debug)
249
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${normalizedContentType.key} as it has a restricted type: ${normalizedContentType.key}\n`));
303
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it has a restricted type: ${contentType.key}\n`));
250
304
  continue;
251
305
  }
252
306
  // Skip based upon system filter
253
- if (normalizedContentType.source == 'system' && !allowSystem) {
307
+ if (contentType.source == 'system' && !allowSystem) {
254
308
  if (cfg.debug)
255
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${normalizedContentType.key} due to it being a system type\n`));
309
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${contentType.key} due to it being a system type\n`));
256
310
  continue;
257
311
  }
258
312
  // Skip based upon custom filter
259
- if (customFilter && !await customFilter(normalizedContentType)) {
313
+ if (customFilter && !await customFilter(contentType)) {
260
314
  if (cfg.debug)
261
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${normalizedContentType.key} due to the custom filter\n`));
315
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${contentType.key} due to the custom filter\n`));
262
316
  continue;
263
317
  }
264
318
  // Add to list
265
- filteredContentTypes.push(normalizedContentType);
319
+ filteredContentTypes.push(contentType);
266
320
  }
267
321
  if (cfg.debug)
268
322
  process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${allContentTypes.length} to ${filteredContentTypes.length} items\n`));
@@ -420,18 +474,9 @@ async function getStyleFilePath(definition, opts) {
420
474
  return `${opts?.contentBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
421
475
  if (!opts?.client)
422
476
  throw new Error("Neither the contentBaseType, nor the ApiClient has been provided for a definition for a specific ContentType - unable to generate the path");
423
- const pageSize = 50;
424
- let resultsPage = await opts.client.contentTypesList({ query: { pageIndex: 0, pageSize } });
425
- const contentTypes = resultsPage.items ?? [];
426
- let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
427
- while (pagesRemaining > 0 && contentTypes.length < resultsPage.totalItemCount) {
428
- resultsPage = await opts.client.contentTypesList({ query: { pageIndex: resultsPage.pageIndex + 1, pageSize: resultsPage.pageSize } });
429
- contentTypes.push(...resultsPage.items);
430
- pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
431
- }
432
- const fetchedBaseType = contentTypes.filter(x => x.key == definition.contentType).map(x => x.baseType).at(0);
433
- if (fetchedBaseType)
434
- return `${fetchedBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
477
+ const contentType = await opts.client.contentTypesGet({ path: { key: definition.contentType } }).catch(() => undefined);
478
+ if (contentType)
479
+ return `${contentType.baseType}/${definition.contentType}/${definition.key}.opti-style.json`;
435
480
  }
436
481
  throw new Error(`Unable to resolve the target for the DisplayTemplate: ${definition.key}`);
437
482
  }
@@ -475,7 +520,7 @@ const StylesPullCommand = {
475
520
  builder: (yargs) => {
476
521
  const newYargs = stylesBuilder(yargs);
477
522
  newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
478
- newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true });
523
+ newYargs.option("definitions", { alias: 'u', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true });
479
524
  return newYargs;
480
525
  },
481
526
  handler: async (args) => {
@@ -490,29 +535,9 @@ const StylesPullCommand = {
490
535
  process.stdout.write(chalk.gray(`${figures.arrowRight} Start creating .opti-style.json files\n`));
491
536
  const typeFiles = {};
492
537
  const updatedTemplates = (await Promise.all(filteredResults.map(async (displayTemplate) => {
493
- let itemPath = undefined;
494
- let targetType;
495
- let typesPath;
496
- if (displayTemplate.nodeType) {
497
- itemPath = path.join(basePath, 'nodes', displayTemplate.nodeType, displayTemplate.key);
498
- typesPath = path.join(basePath, 'nodes', displayTemplate.nodeType);
499
- targetType = 'node/' + displayTemplate.nodeType;
500
- }
501
- else if (displayTemplate.baseType) {
502
- itemPath = path.join(basePath, displayTemplate.baseType, 'styles', displayTemplate.key);
503
- typesPath = path.join(basePath, displayTemplate.baseType, 'styles');
504
- targetType = 'base/' + displayTemplate.baseType;
505
- }
506
- else if (displayTemplate.contentType) {
507
- const contentType = await client.contentTypesGet({ path: { key: displayTemplate.contentType ?? '-' } });
508
- itemPath = path.join(basePath, contentType.baseType, contentType.key);
509
- typesPath = path.join(basePath, contentType.baseType, contentType.key);
510
- targetType = 'content/' + displayTemplate.contentType;
511
- }
512
- if (!fs.existsSync(itemPath))
513
- fs.mkdirSync(itemPath, { recursive: true });
514
- // Write Style JSON
515
- const filePath = path.join(itemPath, `${displayTemplate.key}.opti-style.json`);
538
+ // Create metadata
539
+ const { targetType, styleFilePath: filePath, helperFilePath: helperPath } = await createTemplateMetadata(client, displayTemplate, basePath, true);
540
+ // Build local style data
516
541
  const outputTemplate = { ...displayTemplate };
517
542
  if (outputTemplate.createdBy)
518
543
  delete outputTemplate.createdBy;
@@ -522,6 +547,7 @@ const StylesPullCommand = {
522
547
  delete outputTemplate.created;
523
548
  if (outputTemplate.lastModified)
524
549
  delete outputTemplate.lastModified;
550
+ // Write file to disk
525
551
  if (fs.existsSync(filePath)) {
526
552
  if (!force) {
527
553
  if (cfg.debug)
@@ -539,80 +565,20 @@ const StylesPullCommand = {
539
565
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating style file for ${displayTemplate.key} in ${filePath}\n`));
540
566
  fs.writeFileSync(filePath, JSON.stringify(outputTemplate, undefined, 2));
541
567
  }
568
+ // Ensure we're tracking all files
542
569
  if (!typeFiles[targetType]) {
543
570
  typeFiles[targetType] = {
544
- filePath: path.join(typesPath, 'displayTemplates.ts'),
571
+ filePath: helperPath,
545
572
  templates: []
546
573
  };
547
574
  }
548
- typeFiles[targetType].templates.push({ file: filePath, data: displayTemplate });
575
+ typeFiles[targetType].templates.push({ key: displayTemplate.key, file: filePath, data: displayTemplate });
549
576
  return displayTemplate.key;
550
577
  }))).filter(x => x);
551
578
  //#endregion
552
579
  //#region Create needed definition files
553
- if (definitions) {
554
- process.stdout.write(chalk.gray(`${figures.arrowRight} Start creating displayTemplates.ts files\n`));
555
- for (const targetId of Object.getOwnPropertyNames(typeFiles)) {
556
- const { filePath: typeFilePath, templates } = typeFiles[targetId];
557
- if (fs.existsSync(typeFilePath)) {
558
- if (!force) {
559
- if (cfg.debug)
560
- process.stdout.write(chalk.gray(`${figures.cross} Skipped writing definition file for ${targetId} - it already exists\n`));
561
- continue;
562
- }
563
- if (cfg.debug)
564
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting definition file for ${targetId} - ${typeFilePath}\n`));
565
- }
566
- else if (cfg.debug)
567
- process.stdout.write(chalk.gray(`${figures.arrowRight} Creating definition file for ${targetId} - ${typeFilePath}\n`));
568
- // Write Style definition
569
- const imports = [
570
- 'import type { LayoutProps } from "@remkoj/optimizely-cms-react"',
571
- 'import type { ReactNode, JSX } from "react"'
572
- ];
573
- const typeContents = [];
574
- const props = [];
575
- let typeId = targetId.split('/', 2)[1];
576
- templates.forEach(({ file: displayTemplateFile, data: displayTemplate }) => {
577
- const importPath = path.relative(path.dirname(typeFilePath), displayTemplateFile).replaceAll('\\', '/');
578
- imports.push(`import type ${displayTemplate.key}Styles from "./${importPath}"`);
579
- typeContents.push(`export type ${displayTemplate.key}Props = LayoutProps<typeof ${displayTemplate.key}Styles>`);
580
- typeContents.push(`export type ${displayTemplate.key}ComponentProps<DT extends Record<string, any> = Record<string, any>> = {
581
- data: DT
582
- layoutProps: ${displayTemplate.key}Props | undefined
583
- } & JSX.IntrinsicElements['div']`);
584
- typeContents.push(`export type ${displayTemplate.key}Component<DT extends Record<string, any> = Record<string, any>> = (props: ${displayTemplate.key}ComponentProps<DT>) => ReactNode`);
585
- typeContents.push('');
586
- props.push(`${displayTemplate.key}Props`);
587
- if (!typeId)
588
- typeId = displayTemplate.nodeType ?? displayTemplate.baseType ?? displayTemplate.contentType;
589
- });
590
- if (typeId) {
591
- typeId = ucFirst$1(typeId);
592
- typeContents.push('');
593
- typeContents.push(`export type ${typeId}LayoutProps = ${props.join(' | ')}
594
- export type ${typeId}ComponentProps<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = {
595
- data: DT
596
- layoutProps: LP | undefined
597
- } & JSX.IntrinsicElements['div']
598
-
599
- export type ${typeId}Component<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = (props: ${typeId}ComponentProps<DT,LP>) => ReactNode
600
-
601
- export function isDefaultProps(props?: ${typeId}LayoutProps | null) : props is ${templates.filter(t => t.data.isDefault).at(0)?.data?.key}Props
602
- {
603
- return props?.template == "${templates.filter(t => t.data.isDefault).at(0)?.data?.key}"
604
- }`);
605
- templates.forEach(t => {
606
- typeContents.push(`
607
- export function is${t.data.key}Props(props?: ${typeId}LayoutProps | null) : props is ${t.data.key}Props
608
- {
609
- return props?.template == "${t.data.key}"
610
- }`);
611
- });
612
- }
613
- fs.writeFileSync(typeFilePath, imports.join("\n") + "\n\n" + typeContents.join("\n"));
614
- }
615
- }
580
+ if (definitions)
581
+ await createDisplayTemplateHelpers(typeFiles);
616
582
  //#endregion
617
583
  process.stdout.write(chalk.green(chalk.bold(figures.tick + ` Created/updated style definitions for ${updatedTemplates.join(', ')}`)) + "\n");
618
584
  }
@@ -622,6 +588,131 @@ function ucFirst$1(input) {
622
588
  return input;
623
589
  return input[0].toUpperCase() + input.substring(1);
624
590
  }
591
+ async function createTemplateMetadata(client, displayTemplate, basePath, createPaths = true) {
592
+ let itemPath = undefined;
593
+ let targetType;
594
+ let typesPath;
595
+ const targetPrefix = getTemplateTarget(displayTemplate);
596
+ switch (targetPrefix) {
597
+ case 'node':
598
+ itemPath = path.join(basePath, 'nodes', displayTemplate.nodeType, displayTemplate.key);
599
+ typesPath = path.join(basePath, 'nodes', displayTemplate.nodeType);
600
+ targetType = targetPrefix + '/' + displayTemplate.nodeType;
601
+ break;
602
+ case 'base': {
603
+ const baseTypeSlug = baseTypeToSlug(displayTemplate.baseType);
604
+ itemPath = path.join(basePath, baseTypeSlug, 'styles', displayTemplate.key);
605
+ typesPath = path.join(basePath, baseTypeSlug, 'styles');
606
+ targetType = targetPrefix + '/' + baseTypeSlug;
607
+ break;
608
+ }
609
+ case 'content': {
610
+ const contentType = await client.contentTypesGet({ path: { key: displayTemplate.contentType ?? '-' } });
611
+ const baseTypeSlug = baseTypeToSlug(contentType.baseType);
612
+ itemPath = path.join(basePath, baseTypeSlug, contentType.key);
613
+ typesPath = path.join(basePath, baseTypeSlug, contentType.key);
614
+ targetType = targetPrefix + '/' + displayTemplate.contentType;
615
+ break;
616
+ }
617
+ default:
618
+ throw new Error("Unsupported display template type");
619
+ }
620
+ if (createPaths)
621
+ void await fsAsync.mkdir(itemPath, { recursive: true }).catch(e => {
622
+ console.log(e);
623
+ });
624
+ const styleFilePath = path.join(itemPath, `${displayTemplate.key}.opti-style.json`);
625
+ const helperFilePath = path.join(typesPath, 'displayTemplates.ts');
626
+ return { key: displayTemplate.key, itemPath, typesPath, targetType, styleFilePath, helperFilePath };
627
+ }
628
+ function baseTypeToSlug(baseType) {
629
+ return baseType.replace(/^\_*/, "").toLowerCase();
630
+ }
631
+ function getTemplateTarget(displayTemplate) {
632
+ if (displayTemplate.baseType && displayTemplate.baseType.length > 0)
633
+ return 'base';
634
+ if (displayTemplate.nodeType && displayTemplate.nodeType.length > 0)
635
+ return 'node';
636
+ if (displayTemplate.contentType && displayTemplate.contentType.length > 0)
637
+ return 'content';
638
+ return null;
639
+ }
640
+ async function createDisplayTemplateHelpers(typeFiles, force = false, debug = false) {
641
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Start creating displayTemplates.ts files\n`));
642
+ for (const targetId in typeFiles) {
643
+ await createDisplayTemplateHelper(typeFiles[targetId], targetId, force, debug);
644
+ }
645
+ }
646
+ async function createDisplayTemplateHelper(typeFile, typeFileId, force = false, debug = false) {
647
+ const prefix = '//not-modified - Remove this line when making change to prevent it from being updated by the CLI tools';
648
+ const { filePath: typeFilePath, templates } = typeFile;
649
+ const shouldWrite = await fsAsync.readFile(typeFilePath, { encoding: "utf-8" }).then(data => data.startsWith('//not-modified')).catch((e) => {
650
+ if (e?.code === 'ENOENT')
651
+ return true;
652
+ if (debug)
653
+ process.stdout.write(chalk.redBright(chalk.bold(`${figures.cross} Unexpected error while reading display template file\n`)));
654
+ return false;
655
+ });
656
+ if (!shouldWrite) {
657
+ if (!force) {
658
+ if (debug)
659
+ process.stdout.write(chalk.gray(`${figures.cross} Skipped writing definition file for ${typeFileId} - it already exists and has been modified\n`));
660
+ return false;
661
+ }
662
+ else {
663
+ if (debug)
664
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Forcefully overwriting definition file for ${typeFileId} - ${typeFilePath}\n`));
665
+ }
666
+ }
667
+ else if (debug)
668
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating or updating definition file for ${typeFileId} - ${typeFilePath}\n`));
669
+ // Write Style definition
670
+ const imports = [
671
+ 'import type { LayoutProps, LayoutPropsSettingKeys, LayoutPropsSettingValues, CmsComponentProps } from "@remkoj/optimizely-cms-react"',
672
+ 'import type { JSX, ComponentType } from "react"'
673
+ ];
674
+ const typeContents = [];
675
+ const props = [];
676
+ let typeId = typeFileId.split('/', 2)[1];
677
+ templates.forEach(({ file: displayTemplateFile, data: displayTemplate }) => {
678
+ const importPath = path.relative(path.dirname(typeFilePath), displayTemplateFile).replaceAll('\\', '/');
679
+ imports.push(`import type ${displayTemplate.key}Styles from "./${importPath}"`);
680
+ typeContents.push(`export type ${displayTemplate.key}Props = LayoutProps<typeof ${displayTemplate.key}Styles>`);
681
+ typeContents.push(`export type ${displayTemplate.key}Keys = LayoutPropsSettingKeys<${displayTemplate.key}Props>`);
682
+ typeContents.push(`export type ${displayTemplate.key}Options<K extends ${displayTemplate.key}Keys> = LayoutPropsSettingValues<${displayTemplate.key}Props, K>`);
683
+ typeContents.push(`export type ${displayTemplate.key}ComponentProps<DT extends Record<string, any> = Record<string, any>> = Omit<CmsComponentProps<DT, ${displayTemplate.key}Props>,'children'> & JSX.IntrinsicElements['div']`);
684
+ typeContents.push(`export type ${displayTemplate.key}Component<DT extends Record<string, any> = Record<string, any>> = ComponentType<${displayTemplate.key}ComponentProps<DT>>`);
685
+ typeContents.push('');
686
+ props.push(`${displayTemplate.key}Props`);
687
+ if (!typeId)
688
+ typeId = displayTemplate.nodeType ?? displayTemplate.baseType ?? displayTemplate.contentType;
689
+ });
690
+ if (typeId) {
691
+ typeId = ucFirst$1(typeId);
692
+ typeContents.push(`export type ${typeId}LayoutProps = ${props.join(' | ')}
693
+ export type ${typeId}LayoutKeys = LayoutPropsSettingKeys<${typeId}LayoutProps>
694
+ export type ${typeId}LayoutOptions<K extends ${typeId}LayoutKeys> = LayoutPropsSettingValues<${typeId}LayoutProps,K>
695
+ export type ${typeId}ComponentProps<DT extends Record<string, any> = Record<string, any>> = Omit<CmsComponentProps<DT, ${typeId}LayoutProps>,'children'> & JSX.IntrinsicElements['div']
696
+ export type ${typeId}Component<DT extends Record<string, any> = Record<string, any>> = ComponentType<${typeId}ComponentProps<DT>>`);
697
+ const defaultTemplate = templates.find(t => t.data.isDefault);
698
+ if (defaultTemplate) {
699
+ typeContents.push(`
700
+ export function isDefaultProps(props?: ${typeId}LayoutProps | null) : props is ${defaultTemplate.data?.key}Props
701
+ {
702
+ return props?.template == "${defaultTemplate.data?.key}"
703
+ }`);
704
+ }
705
+ templates.forEach(t => {
706
+ typeContents.push(`
707
+ export function is${t.data.key}Props(props?: ${typeId}LayoutProps | null) : props is ${t.data.key}Props
708
+ {
709
+ return props?.template == "${t.data.key}"
710
+ }`);
711
+ });
712
+ }
713
+ void await fsAsync.writeFile(typeFilePath, prefix + "\n" + imports.join("\n") + "\n\n" + typeContents.join("\n"));
714
+ return true;
715
+ }
625
716
 
626
717
  // Node JS and 3rd Party libraries
627
718
  const StylesCreateCommand = {
@@ -700,7 +791,7 @@ const StylesCreateCommand = {
700
791
  fs.writeFileSync(path.join(basePath, styleFilePath), JSON.stringify(definition, undefined, 4));
701
792
  process.stdout.write(chalk.yellowBright(chalk.bold(`\nWritten style defintion template to: ${path.normalize(path.join(basePath, styleFilePath))}\n`)));
702
793
  if (await confirm({ message: "Do you want to publish this style into Optimizely CMS?" })) {
703
- const response = await client.displayTemplatesPut({ body: definition, path: { key: definition.key } }).catch(_ => undefined);
794
+ const response = await client.displayTemplatesCreate({ body: definition }).catch(_ => undefined);
704
795
  if (!response) {
705
796
  process.stderr.write(chalk.redBright(chalk.bold(`\[ERROR] Unable to create style definition ${definition.displayName} in Optimizely CMS\n`)));
706
797
  }
@@ -733,8 +824,7 @@ const TypesPullCommand = {
733
824
  const client = createCmsClient(args);
734
825
  const { contentTypes } = await getContentTypes(client, args);
735
826
  const updatedTypes = contentTypes.map(contentType => {
736
- const typePath = path.join(basePath, contentType.baseType, contentType.key);
737
- const typeFile = path.join(typePath, `${contentType.key}.opti-type.json`);
827
+ const { typePath, typeFile } = getContentTypePaths(contentType, basePath);
738
828
  if (!fs.existsSync(typePath))
739
829
  fs.mkdirSync(typePath, { recursive: true });
740
830
  if (fs.existsSync(typeFile) && !force) {
@@ -745,16 +835,28 @@ const TypesPullCommand = {
745
835
  const outContentType = { ...contentType };
746
836
  if (outContentType.source || outContentType.source == "")
747
837
  delete outContentType.source;
748
- if (outContentType.features)
749
- delete outContentType.features;
750
- if (outContentType.usage)
751
- delete outContentType.usage;
838
+ //if (outContentType.features) delete outContentType.features
839
+ //if (outContentType.usage) delete outContentType.usage
752
840
  if (outContentType.lastModifiedBy)
753
841
  delete outContentType.lastModifiedBy;
754
842
  if (outContentType.lastModified)
755
843
  delete outContentType.lastModified;
756
844
  if (outContentType.created)
757
845
  delete outContentType.created;
846
+ for (const propName of Object.getOwnPropertyNames(outContentType.properties ?? {})) {
847
+ if (!["content", "contentReference"].includes(outContentType.properties[propName].type)) {
848
+ if (isEmptyArray(outContentType.properties[propName].allowedTypes))
849
+ delete outContentType.properties[propName].allowedTypes;
850
+ if (isEmptyArray(outContentType.properties[propName].restrictedTypes))
851
+ delete outContentType.properties[propName].restrictedTypes;
852
+ }
853
+ if (outContentType.properties[propName].type == 'array' && !["content", "contentReference"].includes(outContentType.properties[propName].items?.type)) {
854
+ if (isEmptyArray(outContentType.properties[propName].items.allowedTypes))
855
+ delete outContentType.properties[propName].items.allowedTypes;
856
+ if (isEmptyArray(outContentType.properties[propName].items.restrictedTypes))
857
+ delete outContentType.properties[propName].items.restrictedTypes;
858
+ }
859
+ }
758
860
  if (debug)
759
861
  process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key})\n`));
760
862
  fs.writeFileSync(typeFile, JSON.stringify(outContentType, undefined, 2));
@@ -763,6 +865,197 @@ const TypesPullCommand = {
763
865
  process.stdout.write(chalk.green(chalk.bold(`${figures.tick} Created/updated type definitions for ${updatedTypes.join(', ')}\n`)));
764
866
  }
765
867
  };
868
+ function isEmptyArray(toTest) {
869
+ return toTest === null || !Array.isArray(toTest) || toTest.length === 0;
870
+ }
871
+
872
+ const deepmerge$1 = createDeepMerge();
873
+ async function loadSchema(client, schemaName) {
874
+ const schemas = Array.isArray(schemaName) ? schemaName : [schemaName];
875
+ process.stdout.write(`${figures.arrowRight} Downloading current JSON Schema\n`);
876
+ const spec = await client.getOpenApiSpec();
877
+ const specSchemas = spec.components?.schemas ?? {};
878
+ process.stdout.write(` ${figures.tick} Complete\n`);
879
+ const result = [];
880
+ process.stdout.write(`\n${figures.arrowRight} Constructing schema for ${schemas.join(', ')}\n`);
881
+ for await (const schema of schemas)
882
+ if (specSchemas[schema]) {
883
+ const definitions = {};
884
+ const processedSchema = processSchema(specSchemas[schema], definitions, spec);
885
+ const jsonSchema = {
886
+ //"$schema": "https://json-schema.org/draft-07/schema",
887
+ "$id": new URL(`schema/${schema}`, client.getSchemaItemBase()).href,
888
+ type: "object",
889
+ title: schema,
890
+ ...processedSchema,
891
+ definitions
892
+ };
893
+ result.push({
894
+ title: schema,
895
+ schema: postProcessDefintions(jsonSchema)
896
+ });
897
+ process.stdout.write(` ${figures.tick} Constructed schema of ${schema}\n`);
898
+ }
899
+ return result;
900
+ }
901
+ function getValidator(schemaObject) {
902
+ const ajv = new Ajv({
903
+ discriminator: true
904
+ });
905
+ addFormats(ajv);
906
+ return ajv.compile(schemaObject);
907
+ }
908
+ function postProcessDefintions(jsonSchema) {
909
+ if (!jsonSchema.definitions)
910
+ return jsonSchema;
911
+ for (const definitionName in jsonSchema.definitions) {
912
+ if ((definitionName.endsWith("Property") || definitionName.endsWith("ListItem")) && jsonSchema.definitions[definitionName].properties) {
913
+ const type = jsonSchema.definitions[definitionName].properties?.type;
914
+ if (isRefSchema(type)) {
915
+ const typeSchema = resolveRefSchema(type, jsonSchema);
916
+ if (typeSchema && typeSchema.type === 'string' && Array.isArray(typeSchema.enum)) {
917
+ let typeValue = definitionName.substring(0, definitionName.length - 8);
918
+ typeValue = typeValue[0].toLowerCase() + typeValue.substring(1);
919
+ typeValue = typeValue === 'list' ? 'array' : typeValue;
920
+ typeValue = typeValue === 'jsonString' ? 'json' : typeValue;
921
+ if (typeSchema.enum.includes(typeValue)) {
922
+ const newTypeDef = deepmerge$1({}, typeSchema);
923
+ newTypeDef.enum = newTypeDef.enum.filter((x) => x === typeValue);
924
+ jsonSchema.definitions[definitionName].properties.type = newTypeDef;
925
+ }
926
+ }
927
+ }
928
+ }
929
+ }
930
+ return jsonSchema;
931
+ }
932
+ /**
933
+ * Schema normalization to tranform the schema from a .Net generated OpenAPI Schema
934
+ * to a AJV compatible JSON Schema
935
+ *
936
+ * @see https://ajv.js.org/
937
+ * @param schema
938
+ * @param defs
939
+ * @param spec
940
+ * @returns
941
+ */
942
+ function processSchema(schema, defs, spec, mergeAllOf = true) {
943
+ if (Array.isArray(schema))
944
+ return schema.map(s => processSchema(s, defs, spec, mergeAllOf));
945
+ if (typeof schema !== 'object' || schema === null)
946
+ return schema;
947
+ const props = Object.getOwnPropertyNames(schema);
948
+ if (props.length === 1 && props[0] === '$ref') {
949
+ const ref = schema['$ref'];
950
+ if (isLocalRef(ref)) {
951
+ const refName = getLocalRefName(ref);
952
+ if (!defs[refName]) {
953
+ const refItem = resolveLocalRef(ref, spec);
954
+ defs[refName] = processSchema(refItem, defs, spec, mergeAllOf);
955
+ }
956
+ const newRef = `#/definitions/${refName}`;
957
+ return { "$ref": newRef };
958
+ }
959
+ else {
960
+ return schema;
961
+ }
962
+ }
963
+ else if (mergeAllOf && props.includes('allOf') && Array.isArray(schema['allOf'])) {
964
+ // process all of
965
+ const newObject = props.reduce((generated, propName) => {
966
+ if (propName != 'allOf')
967
+ generated[propName] = schema[propName];
968
+ return generated;
969
+ }, {});
970
+ const allOfSchemas = schema['allOf'].map((subschema) => {
971
+ const resolvedSchema = isRefSchema(subschema) ? resolveRefSchema(subschema, spec) : subschema;
972
+ const resolved = processSchema(resolvedSchema, defs, spec, mergeAllOf);
973
+ return resolved;
974
+ });
975
+ const merged = allOfSchemas.reduce((previous, current) => deepmerge$1(previous, current), newObject);
976
+ if (newObject['description'])
977
+ merged['description'] = newObject['description'];
978
+ if (newObject['title'])
979
+ merged['title'] = newObject['title'];
980
+ return merged;
981
+ }
982
+ else {
983
+ const newSchema = {};
984
+ for (const key of props) {
985
+ if (key === 'pattern' && typeof schema[key] === 'string') {
986
+ newSchema[key] = safeCreateUnicodeRegExp(schema[key]);
987
+ /*} else if (key === 'readOnly' && schema[key] === true) {
988
+ return undefined*/
989
+ }
990
+ else if (['additionalProperties', 'required', 'properties', 'discriminator'].includes(key) && typeof schema[key] !== 'object') ;
991
+ else if (key === 'discriminator' && !Array.isArray(schema['oneOf'])) ;
992
+ else if (key === 'discriminator' && !schema['type']) {
993
+ newSchema.type = 'object';
994
+ }
995
+ else {
996
+ const keyVal = processSchema(schema[key], defs, spec, mergeAllOf);
997
+ if (keyVal !== undefined)
998
+ newSchema[key] = keyVal;
999
+ }
1000
+ }
1001
+ if (newSchema['oneOf'] && Array.isArray(newSchema['oneOf']) && newSchema['nullable']) {
1002
+ delete newSchema['nullable'];
1003
+ }
1004
+ return newSchema;
1005
+ }
1006
+ }
1007
+ function isRefSchema(schema) {
1008
+ if (typeof schema != 'object' || schema == null)
1009
+ return false;
1010
+ return Object.getOwnPropertyNames(schema).length === 1 && typeof schema['$ref'] === 'string';
1011
+ }
1012
+ function resolveRefSchema(schema, spec) {
1013
+ if (!isRefSchema(schema))
1014
+ return undefined;
1015
+ return resolveLocalRef(schema['$ref'], spec);
1016
+ }
1017
+ function isLocalRef(ref) {
1018
+ return typeof ref === 'string' && ref.startsWith('#');
1019
+ }
1020
+ function getLocalRefName(ref) {
1021
+ if (!isLocalRef(ref))
1022
+ return undefined;
1023
+ const refPath = ref.substring(2).split('/');
1024
+ return refPath.at(refPath.length - 1);
1025
+ }
1026
+ function resolveLocalRef(ref, spec) {
1027
+ const refPath = ref.substring(2).split('/');
1028
+ return refPath.reduce((integrator, current) => {
1029
+ return integrator ? integrator[current] : undefined;
1030
+ }, spec);
1031
+ }
1032
+ const replaceEscapedChars = [
1033
+ [new RegExp('\\\\ ', 'g'), ' '],
1034
+ [new RegExp('\\\\!', 'g'), '!'],
1035
+ [new RegExp('\\\\"', 'g'), '"'],
1036
+ [new RegExp('\\\\#', 'g'), '#'],
1037
+ [new RegExp('\\\\%', 'g'), '%'],
1038
+ [new RegExp('\\\\&', 'g'), '&'],
1039
+ [new RegExp("\\\\'", 'g'), "'"],
1040
+ [new RegExp('\\\\,', 'g'), ','],
1041
+ [new RegExp('\\\\-', 'g'), '-'],
1042
+ [new RegExp('\\\\:', 'g'), ':'],
1043
+ [new RegExp('\\\\;', 'g'), ';'],
1044
+ [new RegExp('\\\\<', 'g'), '<'],
1045
+ [new RegExp('\\\\=', 'g'), '='],
1046
+ [new RegExp('\\\\>', 'g'), '>'],
1047
+ [new RegExp('\\\\@', 'g'), '@'],
1048
+ [new RegExp('\\\\_', 'g'), '_'],
1049
+ [new RegExp('\\\\`', 'g'), '`'],
1050
+ [new RegExp('\\\\~', 'g'), '~'],
1051
+ [new RegExp('\\\\[zZ]', 'g'), '$'],
1052
+ ];
1053
+ function safeCreateUnicodeRegExp(pattern) {
1054
+ for (const unEscape of replaceEscapedChars) {
1055
+ pattern = pattern.replace(unEscape[0], unEscape[1]);
1056
+ }
1057
+ return pattern;
1058
+ }
766
1059
 
767
1060
  const TypesPushCommand = {
768
1061
  command: "types:push",
@@ -793,9 +1086,18 @@ const TypesPushCommand = {
793
1086
  (!baseTypes || baseTypes.length == 0 || baseTypes.includes(data.definition.baseType)) &&
794
1087
  (!types || types.length == 0 || types.includes(data.definition.key));
795
1088
  });
1089
+ // Create validator
1090
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Loading OpenAPI Specification for validation\n`));
1091
+ const typeSchema = (await loadSchema(client, 'ContentType')).at(0)?.schema;
1092
+ const typeValidator = await getValidator(typeSchema);
796
1093
  // Output selected types
797
- const results = await Promise.all(typeDefinitions.map(type => {
1094
+ const results = await Promise.all(typeDefinitions.map(async (type) => {
1095
+ if (!type.definition.key) {
1096
+ process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Content Type has no key in ${type.file}\n`));
1097
+ return;
1098
+ }
798
1099
  process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Pushing ${type.definition.displayName} from ${type.file}\n`));
1100
+ // Filter unwanted fields from the ContentType definition
799
1101
  const outType = { ...type.definition };
800
1102
  if (outType.source)
801
1103
  delete outType.source;
@@ -805,13 +1107,31 @@ const TypesPushCommand = {
805
1107
  delete outType.lastModified;
806
1108
  if (outType.lastModifiedBy || outType.lastModifiedBy == "")
807
1109
  delete outType.lastModifiedBy;
808
- if (outType.features)
809
- delete outType.features;
810
- if (outType.usage)
811
- delete outType.usage;
812
- return client.contentTypesPut({ path: { key: outType.key }, body: outType, query: { ignoreDataLossWarnings: force } })
813
- .then(ct => { return { key: type.definition.key, type: ct, file: type.file }; })
814
- .catch(e => { return { key: type.definition.key, type: type.definition, file: type.file, error: e }; });
1110
+ //if (outType.features) delete outType.features
1111
+ //if (outType.usage) delete outType.usage
1112
+ const validationResult = typeValidator(outType);
1113
+ if (validationResult || force) {
1114
+ if (!validationResult)
1115
+ process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Ignoring validation errors in ${type.file}\n`));
1116
+ const currentContentType = (await client.contentTypesGet({ path: { key: type.definition.key } }).catch(() => null));
1117
+ function buildError(e) {
1118
+ return { key: type.definition.key, type: type.definition, file: type.file, error: e };
1119
+ }
1120
+ function buildData(ct) {
1121
+ return { key: type.definition.key, type: ct, file: type.file };
1122
+ }
1123
+ return (currentContentType ?
1124
+ client.contentTypesPatch({ path: { key: type.definition.key }, body: outType }) :
1125
+ client.contentTypesCreate({ body: outType })).then(buildData).catch(buildError);
1126
+ }
1127
+ else {
1128
+ return {
1129
+ key: type.definition.key,
1130
+ type: type.definition,
1131
+ file: type.file,
1132
+ error: typeValidator.errors.map(x => x.message).join(', ')
1133
+ };
1134
+ }
815
1135
  }));
816
1136
  const overview = new Table({
817
1137
  head: [
@@ -843,233 +1163,10 @@ const builder = yargs => {
843
1163
  return updatedArgs;
844
1164
  };
845
1165
  function createTypeFolders(contentTypes, basePath, debug = false) {
846
- const folders = contentTypes.map(contentType => {
847
- const baseType = contentType.baseType ?? 'default';
848
- // Create the type folder
849
- const typePath = path.join(basePath, baseType, contentType.key);
850
- if (!fs.existsSync(typePath)) {
851
- fs.mkdirSync(typePath, { recursive: true });
852
- if (debug)
853
- process.stdout.write(chalk.gray(`${figures.arrowRight} Created folder ${typePath} for Content-Type ${contentType.key}\n`));
854
- }
855
- // Check folders
856
- if (!fs.statSync(typePath).isDirectory())
857
- throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
858
- return {
859
- type: contentType.key,
860
- path: typePath,
861
- };
862
- });
863
- return folders;
1166
+ return contentTypes.map(contentType => getContentTypePaths(contentType, basePath, true, debug));
864
1167
  }
865
1168
  function getTypeFolder(list, type) {
866
- return list.filter(x => x.type == type).at(0)?.path;
867
- }
868
-
869
- /**
870
- * Keep track of all generated properties
871
- */
872
- let generatedProps = [];
873
- const NextJsQueriesCommand = {
874
- command: "nextjs:fragments",
875
- describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
876
- builder,
877
- handler: async (args, opts) => {
878
- generatedProps = [];
879
- // Prepare
880
- const { loadedContentTypes, createdTypeFolders } = opts || {};
881
- const { components: basePath, _config: { debug }, force } = parseArgs(args);
882
- const client = createCmsClient(args);
883
- const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
884
- // Start process
885
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments for ${contentTypes.map(x => x.key).join(', ')}\n`));
886
- const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
887
- const updatedTypes = contentTypes.map(contentType => {
888
- const typePath = getTypeFolder(typeFolders, contentType.key);
889
- return createGraphFragments(contentType, typePath, basePath, force, debug, allContentTypes, client.runtimeCmsVersion == OptiCmsVersion.CMS12);
890
- }).filter(x => x).flat();
891
- // Report outcome
892
- if (updatedTypes.length > 0)
893
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
894
- else
895
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
896
- if (!opts)
897
- process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
898
- generatedProps = [];
899
- }
900
- };
901
- function createGraphFragments(contentType, typePath, basePath, force, debug, contentTypes, forCms12 = false) {
902
- const returnValue = [];
903
- const baseType = contentType.baseType ?? 'default';
904
- const baseQueryFile = path.join(typePath, `${contentType.key}.${baseType}.graphql`);
905
- if (fs.existsSync(baseQueryFile)) {
906
- if (force) {
907
- if (debug)
908
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
909
- }
910
- else {
911
- if (debug)
912
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
913
- return undefined;
914
- }
915
- }
916
- else if (debug) {
917
- process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
918
- }
919
- const { fragment, propertyTypes } = createInitialFragment(contentType, false, undefined, forCms12);
920
- fs.writeFileSync(baseQueryFile, fragment);
921
- returnValue.push(contentType.key);
922
- let dependencies = Array.isArray(propertyTypes) ? [...propertyTypes] : [];
923
- while (Array.isArray(dependencies) && dependencies.length > 0) {
924
- let newDependencies = [];
925
- dependencies.forEach(dep => {
926
- const propContentType = contentTypes.filter(x => x.key == dep[0])[0];
927
- if (!propContentType) {
928
- console.warn(`🟠 The content type ${dep[0]} has been referenced, but is not found in the Optimizely CMS instance`);
929
- return;
930
- }
931
- const fullTypeName = forCms12 ? contentType.key + propContentType.key : propContentType.key;
932
- const propertyFragmentFile = path.join(basePath, propContentType.baseType, propContentType.key, `${fullTypeName}.property.graphql`);
933
- const propertyFragmentDir = path.dirname(propertyFragmentFile);
934
- if (!fs.existsSync(propertyFragmentDir))
935
- fs.mkdirSync(propertyFragmentDir, { recursive: true });
936
- if (!fs.existsSync(propertyFragmentFile) || force) {
937
- if (debug)
938
- process.stdout.write(chalk.gray(`${figures.arrowRight} Writing ${propContentType.displayName} (${propContentType.key}) property fragment\n`));
939
- const propContentTypeInfo = createInitialFragment(propContentType, true, contentType, forCms12);
940
- fs.writeFileSync(propertyFragmentFile, propContentTypeInfo.fragment);
941
- returnValue.push(propContentType.key);
942
- if (Array.isArray(propContentTypeInfo.propertyTypes))
943
- newDependencies.push(...propContentTypeInfo.propertyTypes);
944
- }
945
- });
946
- dependencies = newDependencies;
947
- }
948
- return returnValue.length > 0 ? returnValue : undefined;
949
- }
950
- function createInitialFragment(contentType, forProperty = false, forBaseType, forCms12 = false) {
951
- const propertyTypes = [];
952
- const fragmentFields = [];
953
- const typeProps = contentType.properties ?? {};
954
- Object.getOwnPropertyNames(typeProps).forEach(propKey => {
955
- // Exclude system properties, which are not present in Optimizely Graph
956
- if (['experience', 'section'].includes(contentType.baseType) && ['AdditionalData', 'UnstructuredData', 'Layout'].includes(propKey))
957
- return;
958
- // Exclude CMS 12 System Properties
959
- if (forCms12 && ['Categories'].includes(propKey))
960
- return;
961
- const propType = typeProps[propKey].type;
962
- const isConflict = generatedProps.some(x => x.propName == propKey && x.propType != propType);
963
- const propName = isConflict ? `${contentType.key}${propKey}: ${propKey}` : propKey;
964
- // Write the property
965
- switch (propType) {
966
- case IntegrationApi.PropertyDataType.ARRAY:
967
- {
968
- const typeData = typeProps[propKey];
969
- switch (typeData.items.type) {
970
- case IntegrationApi.PropertyDataType.INTEGER:
971
- if (typeData.format == 'categorization')
972
- fragmentFields.push(`${propName} { Id, Name, Description }`);
973
- else
974
- fragmentFields.push(propName);
975
- break;
976
- case IntegrationApi.PropertyDataType.STRING:
977
- fragmentFields.push(propName);
978
- break;
979
- case IntegrationApi.PropertyDataType.CONTENT:
980
- if (contentType.baseType == 'page' || contentType.baseType == 'experience')
981
- fragmentFields.push(`${propName} { ...${forCms12 ? 'PageIContentListItem' : 'BlockData'} }`);
982
- else
983
- fragmentFields.push(`${propName} { ...IContentListItem }`);
984
- break;
985
- case IntegrationApi.PropertyDataType.COMPONENT:
986
- const componentType = typeData.items.contentType;
987
- switch (componentType) {
988
- case 'link':
989
- fragmentFields.push(`${propName} { ...LinkItemData }`);
990
- break;
991
- default:
992
- {
993
- const componentFragmentName = forCms12 ? contentType.key + componentType : componentType + 'Property';
994
- fragmentFields.push(`${propName} { ...${componentFragmentName}Data }`);
995
- propertyTypes.push([componentType, true]);
996
- break;
997
- }
998
- }
999
- break;
1000
- case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
1001
- fragmentFields.push(`${propName} { ...ReferenceData }`);
1002
- break;
1003
- default:
1004
- fragmentFields.push(`${propName} { __typename }`);
1005
- break;
1006
- }
1007
- break;
1008
- }
1009
- case IntegrationApi.PropertyDataType.STRING: {
1010
- const propDetails = typeProps[propKey];
1011
- switch (propDetails.format ?? "") {
1012
- case 'html':
1013
- fragmentFields.push(forCms12 ? `${propName} { Structure, Html }` : `${propName} { json, html }`);
1014
- break;
1015
- case 'shortString':
1016
- case 'selectOne':
1017
- case '':
1018
- fragmentFields.push(propName);
1019
- break;
1020
- default:
1021
- if (!isConflict)
1022
- console.warn(chalk.redBright(`❗ Unsupported string format "${propDetails.format}" for ${contentType.key}.${propKey}; add it manually to the fragment if you need to access this field`));
1023
- break;
1024
- }
1025
- break;
1026
- }
1027
- case IntegrationApi.PropertyDataType.URL:
1028
- fragmentFields.push(forCms12 ? propName : `${propName} { ...LinkData }`);
1029
- break;
1030
- case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
1031
- fragmentFields.push(`${propName} { ...ReferenceData }`);
1032
- break;
1033
- case IntegrationApi.PropertyDataType.COMPONENT: {
1034
- const componentType = typeProps[propKey].contentType;
1035
- if (forCms12 && componentType == "link") {
1036
- fragmentFields.push(`${propName} { ...LinkItemData }`);
1037
- }
1038
- else {
1039
- const componentFragmentName = forCms12 ? contentType.key + componentType : componentType + 'Property';
1040
- fragmentFields.push(`${propName} { ...${componentFragmentName}Data }`);
1041
- propertyTypes.push([componentType, true]);
1042
- }
1043
- break;
1044
- }
1045
- case IntegrationApi.PropertyDataType.BINARY:
1046
- fragmentFields.push(`${propName} { ...BinaryData }`);
1047
- break;
1048
- default:
1049
- fragmentFields.push(propName);
1050
- break;
1051
- }
1052
- generatedProps.push({
1053
- propType: typeProps[propKey].type,
1054
- propName: propKey
1055
- });
1056
- });
1057
- if (contentType.baseType == "experience")
1058
- fragmentFields.push('...ExperienceData');
1059
- if (fragmentFields.length == 0) {
1060
- if (forCms12)
1061
- fragmentFields.push('empty: _metadata: ContentLink { key: GuidValue }');
1062
- else
1063
- fragmentFields.push('empty: _metadata { key }');
1064
- }
1065
- const fragmentTarget = forProperty ? (forCms12 ? (forBaseType?.key ?? '') + contentType.key : contentType.key + 'Property') : contentType.key;
1066
- const tpl = `fragment ${fragmentTarget}Data on ${fragmentTarget} {
1067
- ${fragmentFields.join("\n ")}
1068
- }`;
1069
- return {
1070
- fragment: tpl,
1071
- propertyTypes: propertyTypes.length == 0 ? null : propertyTypes
1072
- };
1169
+ return list.filter(x => x.type == type).at(0);
1073
1170
  }
1074
1171
 
1075
1172
  function ucFirst(current) {
@@ -1105,8 +1202,8 @@ const NextJsComponentsCommand = {
1105
1202
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1106
1203
  }
1107
1204
  };
1108
- function createComponent(contentType, typePath, force, debug = false) {
1109
- const componentFile = path.join(typePath, 'index.tsx');
1205
+ function createComponent(contentType, typePathInfo, force, debug = false) {
1206
+ const { componentFile, path: typePath } = typePathInfo;
1110
1207
  if (fs.existsSync(componentFile)) {
1111
1208
  if (!force) {
1112
1209
  if (debug)
@@ -1121,8 +1218,9 @@ function createComponent(contentType, typePath, force, debug = false) {
1121
1218
  // Get type information & short-hands
1122
1219
  const displayTemplate = getDisplayTemplateInfo$1(contentType, typePath);
1123
1220
  const baseDisplayTemplate = getBaseTypeTemplateInfo(contentType, typePath);
1124
- const varName = `${contentType.key}${ucFirst(contentType.baseType ?? 'part')}`;
1125
- const tplFn = Templates[contentType.baseType] ?? Templates['default'];
1221
+ const normalizedBaseType = normalizeBaseType(contentType.baseType ?? 'part');
1222
+ const varName = `${contentType.key}${ucFirst(normalizedBaseType)}`;
1223
+ const tplFn = Templates[normalizedBaseType] ?? Templates['default'];
1126
1224
  if (!tplFn) {
1127
1225
  if (debug)
1128
1226
  process.stdout.write(chalk.redBright(`${figures.cross} Skipping ${contentType.displayName} (${contentType.key}) component - no template for ${contentType.baseType} found\n`));
@@ -1146,6 +1244,11 @@ function getBaseTypeTemplateInfo(contentType, typePath) {
1146
1244
  }
1147
1245
  return undefined;
1148
1246
  }
1247
+ function normalizeBaseType(baseType) {
1248
+ if (baseType.startsWith('_'))
1249
+ return baseType.substring(1);
1250
+ return baseType;
1251
+ }
1149
1252
  function getDisplayTemplateInfo$1(contentType, typePath) {
1150
1253
  const displayTemplatesFile = path.join(typePath, 'displayTemplates.ts');
1151
1254
  if (fs.existsSync(displayTemplatesFile)) {
@@ -1159,24 +1262,50 @@ function getDisplayTemplateInfo$1(contentType, typePath) {
1159
1262
  }
1160
1263
  const Templates = {
1161
1264
  // Default Template for all components without specifics
1162
- default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { type CmsComponent } from "@remkoj/optimizely-cms-react";
1265
+ default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
1163
1266
  import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
1164
1267
  import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
1165
1268
  import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
1166
1269
 
1167
1270
  /**
1168
1271
  * ${contentType.displayName}
1272
+ * ---
1169
1273
  * ${contentType.description}
1170
1274
  */
1171
- export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, children }) => {
1275
+ export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps }) => {
1172
1276
  const componentName = '${contentType.displayName}'
1173
1277
  const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1174
- return <div className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4">
1278
+ return <CmsEditable className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4" {...editProps}>
1175
1279
  <div className="font-bold italic">{ componentName }</div>
1176
1280
  <div>{ componentInfo }</div>
1177
1281
  { 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> }
1178
- { children && <div className="mt-4 mx-4 flex flex-col">{ children }</div>}
1179
- </div>
1282
+ </CmsEditable>
1283
+ }
1284
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1285
+ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1286
+
1287
+ export default ${varName}`,
1288
+ // Default Template for all section types
1289
+ section: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
1290
+ import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
1291
+ import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
1292
+ import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
1293
+
1294
+ /**
1295
+ * ${contentType.displayName}
1296
+ * ---
1297
+ * ${contentType.description}
1298
+ */
1299
+ export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps, children }) => {
1300
+ const componentName = '${contentType.displayName}'
1301
+ const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1302
+ return <CmsEditable className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4" {...editProps}>
1303
+ <div className="font-bold italic">{ componentName }</div>
1304
+ <div>{ componentInfo }</div>
1305
+ { 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> }
1306
+ ${(displayTemplate || baseDisplayTemplate) ? '<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(layoutProps, undefined, 4) }</pre>' : '{/* This component doesn\'t have layout options */}'}
1307
+ <div>{ children }</div>
1308
+ </CmsEditable>
1180
1309
  }
1181
1310
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1182
1311
  ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
@@ -1184,26 +1313,32 @@ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}
1184
1313
  export default ${varName}`,
1185
1314
  // Template for all page component types
1186
1315
  page: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
1187
- import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
1316
+ import { get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'${displayTemplate ? `
1188
1317
  import { ${displayTemplate} } from "./displayTemplates";` : ''}
1189
1318
  import { getSdk } from "@/gql"
1190
1319
 
1191
1320
  /**
1192
1321
  * ${contentType.displayName}
1322
+ * ---
1193
1323
  * ${contentType.description}
1324
+ *
1325
+ * This component uses the content query that is auto-generated with the Optimizely CMS Preset for GraphQL Codegen, if you need
1326
+ * to override this query create a GraphQL file (for example: \`get${contentType.key}Data.query.graphql\`) in the same folder as
1327
+ * this file. This file must include a GraphQL query with the name \`get${contentType.key}Data\`.
1328
+ *
1329
+ * [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
1194
1330
  */
1195
- export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, children }) => {
1331
+ export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''} }) => {
1196
1332
  const componentName = '${contentType.displayName}'
1197
1333
  const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1198
1334
  return <div className="mx-auto px-2 container">
1199
1335
  <div className="font-bold italic">{ componentName }</div>
1200
1336
  <div>{ componentInfo }</div>
1201
1337
  { 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> }
1202
- { children && <div className="flex flex-col mt-4 mx-4">{ children }</div>}
1203
1338
  </div>
1204
1339
  }
1205
1340
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1206
- ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1341
+ ${varName}.getDataQuery = () => get${contentType.key}DataDocument
1207
1342
  ${varName}.getMetaData = async (contentLink, locale, client) => {
1208
1343
  const sdk = getSdk(client);
1209
1344
  // Add your metadata logic here
@@ -1213,23 +1348,36 @@ ${varName}.getMetaData = async (contentLink, locale, client) => {
1213
1348
  export default ${varName}`,
1214
1349
  // Template for all experience component types
1215
1350
  experience: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
1216
- import { ${contentType.key}DataFragmentDoc, type ExperienceDataFragment, type ${contentType.key}DataFragment } from "@/gql/graphql";
1351
+ import { ExperienceDataFragmentDoc, get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'
1352
+ import { getFragmentData } from '@/gql/fragment-masking'
1217
1353
  import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
1218
1354
  import { ${displayTemplate} } from "./displayTemplates";` : ''}
1219
1355
  import { getSdk } from "@/gql/client"
1220
1356
 
1221
1357
  /**
1222
1358
  * ${contentType.displayName}
1359
+ * ---
1223
1360
  * ${contentType.description}
1361
+ *
1362
+ * This component uses the content query that is auto-generated with the Optimizely CMS Preset for GraphQL Codegen, if you need
1363
+ * to override this query create the file \`${contentType.key}.query.graphql\` in the same folder as this file. This file then
1364
+ * must include a GraphQL query with the name \`get${contentType.key} Data\`.
1365
+ *
1366
+ * [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
1224
1367
  */
1225
- export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, ctx }) => {
1226
- const composition = (data as ExperienceDataFragment)?.composition
1227
- return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData" ctx={ctx}>
1228
- { composition && isNode(composition) && <OptimizelyComposition node={composition} ctx={ctx} /> }
1229
- </CmsEditable>
1368
+ export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, ctx }) => {
1369
+ if (ctx) ctx.editableContentIsExperience = true
1370
+ const composition = getFragmentData(ExperienceDataFragmentDoc, data).composition
1371
+ const componentName = '${contentType.displayName}'
1372
+ const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1373
+ return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData" ctx={ctx}>
1374
+ <div className="font-bold italic">{ componentName }</div>
1375
+ <div>{ componentInfo }</div>
1376
+ { composition && isNode(composition) && <OptimizelyComposition node={composition} ctx={ctx} /> }
1377
+ </CmsEditable>
1230
1378
  }
1231
1379
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1232
- ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1380
+ ${varName}.getDataQuery = () => get${contentType.key}DataDocument
1233
1381
  ${varName}.getMetaData = async (contentLink, locale, client) => {
1234
1382
  const sdk = getSdk(client);
1235
1383
  // Add your metadata logic here
@@ -1247,7 +1395,7 @@ const NextJsVisualBuilderCommand = {
1247
1395
  // Prepare
1248
1396
  const { components: basePath, _config: { debug }, force } = parseArgs(args);
1249
1397
  // Create the fall-back node and style defintions
1250
- await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node'], definitions: true });
1398
+ await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'], definitions: true });
1251
1399
  createGenericNode(basePath, force, debug);
1252
1400
  // Get all styles
1253
1401
  const { styles } = await getStyles(createCmsClient(args), { ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'] });
@@ -1258,7 +1406,8 @@ const NextJsVisualBuilderCommand = {
1258
1406
  });
1259
1407
  // Process base styles
1260
1408
  styles.filter(x => typeof (x.baseType) == 'string' && x.baseType.length > 0).map(styleDefinition => {
1261
- const templatePath = path.join(basePath, styleDefinition.baseType, 'styles', styleDefinition.key);
1409
+ const baseType = (styleDefinition.baseType ?? '').startsWith('_') ? (styleDefinition.baseType ?? '').substring(1) : styleDefinition.baseType;
1410
+ const templatePath = path.join(basePath, baseType, 'styles', styleDefinition.key);
1262
1411
  createSpecificNode(styleDefinition, templatePath, force, debug);
1263
1412
  });
1264
1413
  }
@@ -1278,13 +1427,30 @@ function createSpecificNode(template, templatePath, force = false, debug = false
1278
1427
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${template.displayName} (${template.key}) component\n`));
1279
1428
  if (!fs.existsSync(templatePath))
1280
1429
  fs.mkdirSync(templatePath, { recursive: true });
1430
+ const baseType = template.nodeType ?? template.baseType ?? 'unknown';
1281
1431
  const displayTemplateName = getDisplayTemplateInfo(template, templatePath);
1282
- const component = `import { extractSettings, type CmsLayoutComponent } from "@remkoj/optimizely-cms-react/rsc";${displayTemplateName ? `
1283
- import { ${displayTemplateName} } from "../displayTemplates";` : ''}
1432
+ const imports = ['import { extractSettings, type CmsLayoutComponent } from "@remkoj/optimizely-cms-react/rsc";'];
1433
+ const componentProps = [`className="vb:${baseType} vb:${baseType}:${template.key}"`];
1434
+ const componentArgs = ['layoutProps', 'children'];
1435
+ let componentType = 'div';
1436
+ if (displayTemplateName)
1437
+ imports.push(`import { ${displayTemplateName} } from "../displayTemplates";`);
1438
+ if (baseType.toLowerCase() == 'section') {
1439
+ imports.push('import { CmsEditable } from "@remkoj/optimizely-cms-react/rsc";');
1440
+ componentType = 'CmsEditable';
1441
+ componentProps.push('{ ...editProps }');
1442
+ componentArgs.push('editProps');
1443
+ }
1444
+ let style = '';
1445
+ if (baseType == "row")
1446
+ style = ` style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "stretch" }}`;
1447
+ if (baseType == "column")
1448
+ style = ` style={{ display: "flex", flexDirection: "column", flexGrow: 1 }}`;
1449
+ const component = `${imports.join("\n")}
1284
1450
 
1285
- export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ contentLink, layoutProps, children }) => {
1451
+ export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ ${componentArgs.join(', ')} }) => {
1286
1452
  const layout = extractSettings(layoutProps);
1287
- return (<div className="vb:${template.nodeType} vb:${template.nodeType}:${template.key}">{ children }</div>);
1453
+ return (<${componentType} ${componentProps.join(' ')}${style}>{ children }</${componentType}>);
1288
1454
  }
1289
1455
 
1290
1456
  export default ${template.key};`;
@@ -1304,13 +1470,19 @@ function createGenericNode(basePath, force, debug) {
1304
1470
  else if (debug)
1305
1471
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating generic node component\n`));
1306
1472
  const nodeContent = `import { CmsEditable, type CmsLayoutComponent } from '@remkoj/optimizely-cms-react/rsc'
1473
+ import { type CSSProperties } from 'react';
1307
1474
 
1308
- export const VisualBuilderNode : CmsLayoutComponent = ({ contentLink, layoutProps, children, ctx }) =>
1475
+ export const VisualBuilderNode : CmsLayoutComponent = ({ editProps, layoutProps, children }) =>
1309
1476
  {
1310
- let className = \`vb:\${layoutProps?.layoutType}\`
1311
- if (layoutProps && layoutProps.layoutType == "section")
1312
- return <CmsEditable as="div" className={ className } cmsId={ contentLink.key } ctx={ctx}>{ children }</CmsEditable>
1313
- return <div className={ className }>{ children }</div>
1477
+ let className = \`vb:\${layoutProps?.layoutType}\`;
1478
+ let style : CSSProperties | undefined = undefined;
1479
+ if (layoutProps?.layoutType == "row")
1480
+ style = {display: "flex", flexDirection: "row"}
1481
+ if (layoutProps?.layoutType == "column")
1482
+ style = {display: "flex", flexDirection: "column"}
1483
+ if (layoutProps && layoutProps.layoutType == "section")
1484
+ return <CmsEditable as="div" className={ className } {...editProps}>{ children }</CmsEditable>
1485
+ return <div className={ className } style={ style }>{ children }</div>
1314
1486
  }
1315
1487
 
1316
1488
  export default VisualBuilderNode`;
@@ -1558,7 +1730,7 @@ const NextJsCreateCommand = {
1558
1730
  await Promise.all([
1559
1731
  TypesPullCommand.handler(args),
1560
1732
  StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: [], definitions: true }),
1561
- NextJsQueriesCommand.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
1733
+ //createFragments.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
1562
1734
  ]);
1563
1735
  process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared types, styles and fragments") + "\n");
1564
1736
  // Then create the components
@@ -1573,182 +1745,199 @@ const NextJsCreateCommand = {
1573
1745
  }
1574
1746
  };
1575
1747
 
1576
- const deepmerge$1 = createDeepMerge();
1577
- async function loadSchema(client, schemaName) {
1578
- const schemas = Array.isArray(schemaName) ? schemaName : [schemaName];
1579
- process.stdout.write(`${figures.arrowRight} Downloading current JSON Schema\n`);
1580
- const spec = await client.getOpenApiSpec();
1581
- const specSchemas = spec.components?.schemas ?? {};
1582
- process.stdout.write(` ${figures.tick} Complete\n`);
1583
- const result = [];
1584
- process.stdout.write(`\n${figures.arrowRight} Constructing schema for ${schemas.join(', ')}\n`);
1585
- for await (const schema of schemas)
1586
- if (specSchemas[schema]) {
1587
- const definitions = {};
1588
- const processedSchema = processSchema(specSchemas[schema], definitions, spec);
1589
- const jsonSchema = {
1590
- //"$schema": "https://json-schema.org/draft-07/schema",
1591
- "$id": `https://api.cms.optimizely.com/schema/${schema}`,
1592
- type: "object",
1593
- title: schema,
1594
- ...processedSchema,
1595
- definitions
1596
- };
1597
- result.push({
1598
- title: schema,
1599
- schema: postProcessDefintions(jsonSchema)
1600
- });
1601
- process.stdout.write(` ${figures.tick} Constructed schema of ${schema}\n`);
1602
- }
1603
- return result;
1604
- }
1605
- function postProcessDefintions(jsonSchema) {
1606
- if (!jsonSchema.definitions)
1607
- return jsonSchema;
1608
- for (const definitionName in jsonSchema.definitions) {
1609
- if ((definitionName.endsWith("Property") || definitionName.endsWith("ListItem")) && jsonSchema.definitions[definitionName].properties) {
1610
- const type = jsonSchema.definitions[definitionName].properties?.type;
1611
- if (isRefSchema(type)) {
1612
- const typeSchema = resolveRefSchema(type, jsonSchema);
1613
- if (typeSchema && typeSchema.type === 'string' && Array.isArray(typeSchema.enum)) {
1614
- let typeValue = definitionName.substring(0, definitionName.length - 8);
1615
- typeValue = typeValue[0].toLowerCase() + typeValue.substring(1);
1616
- typeValue = typeValue === 'list' ? 'array' : typeValue;
1617
- typeValue = typeValue === 'jsonString' ? 'json' : typeValue;
1618
- if (typeSchema.enum.includes(typeValue)) {
1619
- const newTypeDef = deepmerge$1({}, typeSchema);
1620
- newTypeDef.enum = newTypeDef.enum.filter((x) => x === typeValue);
1621
- jsonSchema.definitions[definitionName].properties.type = newTypeDef;
1622
- }
1623
- }
1748
+ const NextJsFragmentsCommand = {
1749
+ command: "nextjs:fragments",
1750
+ describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
1751
+ builder,
1752
+ handler: async (args, opts) => {
1753
+ // Prepare
1754
+ const { loadedContentTypes, createdTypeFolders } = opts || {};
1755
+ const { components: basePath, _config: { debug }, force } = parseArgs(args);
1756
+ const client = createCmsClient(args);
1757
+ const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
1758
+ // Start process
1759
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments\n`));
1760
+ const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
1761
+ const tracker = new Map();
1762
+ const dependencies = [];
1763
+ const updatedTypes = contentTypes.map(contentType => {
1764
+ const { written, propertyTypes } = createComponentFragments(contentType, getTypeFolder(typeFolders, contentType.key), force, debug, tracker);
1765
+ dependencies.push(...propertyTypes);
1766
+ return written ? contentType.key : undefined;
1767
+ }).filter(x => x);
1768
+ // Report outcome
1769
+ if (updatedTypes.length > 0)
1770
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
1771
+ else
1772
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
1773
+ // Start property generation process
1774
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL property fragments\n`));
1775
+ const generatedProps = createPropertyFragments(dependencies, allContentTypes, (contentType) => {
1776
+ if (!contentType.key)
1777
+ return undefined;
1778
+ let tf = getTypeFolder(typeFolders, contentType.key);
1779
+ if (!tf) {
1780
+ tf = getContentTypePaths(contentType, basePath, true, debug);
1781
+ typeFolders.push(tf);
1624
1782
  }
1625
- }
1783
+ return tf;
1784
+ }, force, debug);
1785
+ // Report outcome
1786
+ if (generatedProps.length > 0)
1787
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
1788
+ else
1789
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL property fragments created/updated\n`));
1790
+ if (!opts)
1791
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1626
1792
  }
1627
- return jsonSchema;
1628
- }
1629
- /**
1630
- * Schema normalization to tranform the schema from a .Net generated OpenAPI Schema
1631
- * to a AJV compatible JSON Schema
1632
- *
1633
- * @see https://ajv.js.org/
1634
- * @param schema
1635
- * @param defs
1636
- * @param spec
1637
- * @returns
1638
- */
1639
- function processSchema(schema, defs, spec, mergeAllOf = true) {
1640
- if (Array.isArray(schema))
1641
- return schema.map(s => processSchema(s, defs, spec, mergeAllOf));
1642
- if (typeof schema !== 'object' || schema === null)
1643
- return schema;
1644
- const props = Object.getOwnPropertyNames(schema);
1645
- if (props.length === 1 && props[0] === '$ref') {
1646
- const ref = schema['$ref'];
1647
- if (isLocalRef(ref)) {
1648
- const refName = getLocalRefName(ref);
1649
- if (!defs[refName]) {
1650
- const refItem = resolveLocalRef(ref, spec);
1651
- defs[refName] = processSchema(refItem, defs, spec, mergeAllOf);
1652
- }
1653
- const newRef = `#/definitions/${refName}`;
1654
- return { "$ref": newRef };
1793
+ };
1794
+ function createComponentFragments(contentType, typePath, force, debug, propertyTracker = new Map()) {
1795
+ const baseQueryFile = typePath.fragmentFile;
1796
+ let writeFragment = true;
1797
+ if (fs.existsSync(baseQueryFile)) {
1798
+ if (force) {
1799
+ if (debug)
1800
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
1655
1801
  }
1656
1802
  else {
1657
- return schema;
1803
+ if (debug)
1804
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
1805
+ writeFragment = false;
1658
1806
  }
1659
1807
  }
1660
- else if (mergeAllOf && props.includes('allOf') && Array.isArray(schema['allOf'])) {
1661
- // process all of
1662
- const newObject = props.reduce((generated, propName) => {
1663
- if (propName != 'allOf')
1664
- generated[propName] = schema[propName];
1665
- return generated;
1666
- }, {});
1667
- const allOfSchemas = schema['allOf'].map((subschema) => {
1668
- const resolvedSchema = isRefSchema(subschema) ? resolveRefSchema(subschema, spec) : subschema;
1669
- const resolved = processSchema(resolvedSchema, defs, spec, mergeAllOf);
1670
- return resolved;
1671
- });
1672
- const merged = allOfSchemas.reduce((previous, current) => deepmerge$1(previous, current), newObject);
1673
- if (newObject['description'])
1674
- merged['description'] = newObject['description'];
1675
- if (newObject['title'])
1676
- merged['title'] = newObject['title'];
1677
- return merged;
1808
+ else if (debug) {
1809
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
1678
1810
  }
1679
- else {
1680
- const newSchema = {};
1681
- for (const key of props) {
1682
- if (key === 'pattern' && typeof schema[key] === 'string') {
1683
- newSchema[key] = safeCreateUnicodeRegExp(schema[key]);
1684
- /*} else if (key === 'readOnly' && schema[key] === true) {
1685
- return undefined*/
1686
- }
1687
- else if (['additionalProperties', 'required', 'properties', 'discriminator'].includes(key) && typeof schema[key] !== 'object') ;
1688
- else if (key === 'discriminator' && !Array.isArray(schema['oneOf'])) ;
1689
- else if (key === 'discriminator' && !schema['type']) {
1690
- newSchema.type = 'object';
1811
+ let written = false;
1812
+ if (writeFragment) {
1813
+ const fragment = GraphQLGen.buildFragment(contentType, undefined, false, propertyTracker);
1814
+ fs.writeFileSync(baseQueryFile, fragment);
1815
+ written = true;
1816
+ }
1817
+ return { written, propertyTypes: GraphQLGen.getReferencedPropertyComponents(contentType) };
1818
+ }
1819
+ function createPropertyFragments(propertyTypesList, allContentTypes, selectTypeFolder, force = false, debug = false) {
1820
+ const returnValue = [];
1821
+ for (const propertyContentTypeKey of propertyTypesList.filter((v, i, a) => a.indexOf(v, i + 1) <= i)) {
1822
+ // Load the ContentType definition
1823
+ const contentType = allContentTypes.filter(x => x.key == propertyContentTypeKey).at(0);
1824
+ if (!contentType) {
1825
+ console.warn(`🟠 The content type ${propertyContentTypeKey} has been referenced, but is not found in the Optimizely CMS instance`);
1826
+ continue;
1827
+ }
1828
+ // Load the paths for this ContentType
1829
+ const depFolder = selectTypeFolder(contentType);
1830
+ if (!depFolder) {
1831
+ console.warn(`🟠 The content type ${propertyContentTypeKey} cannot be stored in the project`);
1832
+ continue;
1833
+ }
1834
+ const propertyFragmentFile = depFolder.propertyFragmentFile;
1835
+ let mustWrite = true;
1836
+ if (fs.existsSync(propertyFragmentFile)) {
1837
+ if (force) {
1838
+ if (debug)
1839
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) property fragment\n`));
1691
1840
  }
1692
1841
  else {
1693
- const keyVal = processSchema(schema[key], defs, spec, mergeAllOf);
1694
- if (keyVal !== undefined)
1695
- newSchema[key] = keyVal;
1842
+ if (debug)
1843
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) property fragment - file already exists\n`));
1844
+ mustWrite = false;
1696
1845
  }
1697
1846
  }
1698
- return newSchema;
1847
+ else if (debug) {
1848
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) property fragment\n`));
1849
+ }
1850
+ if (mustWrite) {
1851
+ const fragment = GraphQLGen.buildFragment(contentType, undefined, true);
1852
+ fs.writeFileSync(propertyFragmentFile, fragment);
1853
+ returnValue.push(contentType.key);
1854
+ }
1855
+ // Recurse down for properties that we're not yet rendering
1856
+ const referencedPropertyTypes = GraphQLGen.getReferencedPropertyComponents(contentType).filter(x => !propertyTypesList.includes(x));
1857
+ if (referencedPropertyTypes.length > 0) {
1858
+ if (debug)
1859
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Component property ${propertyContentTypeKey} uses components a property, recursing down\n`));
1860
+ const additionalProperties = createPropertyFragments(referencedPropertyTypes, allContentTypes, selectTypeFolder, force, debug);
1861
+ returnValue.push(...additionalProperties);
1862
+ }
1699
1863
  }
1864
+ return returnValue;
1700
1865
  }
1701
- function isRefSchema(schema) {
1702
- if (typeof schema != 'object' || schema == null)
1703
- return false;
1704
- return Object.getOwnPropertyNames(schema).length === 1 && typeof schema['$ref'] === 'string';
1705
- }
1706
- function resolveRefSchema(schema, spec) {
1707
- if (!isRefSchema(schema))
1708
- return undefined;
1709
- return resolveLocalRef(schema['$ref'], spec);
1710
- }
1711
- function isLocalRef(ref) {
1712
- return typeof ref === 'string' && ref.startsWith('#');
1713
- }
1714
- function getLocalRefName(ref) {
1715
- if (!isLocalRef(ref))
1716
- return undefined;
1717
- const refPath = ref.substring(2).split('/');
1718
- return refPath.at(refPath.length - 1);
1719
- }
1720
- function resolveLocalRef(ref, spec) {
1721
- const refPath = ref.substring(2).split('/');
1722
- return refPath.reduce((integrator, current) => {
1723
- return integrator ? integrator[current] : undefined;
1724
- }, spec);
1725
- }
1726
- const replaceEscapedChars = [
1727
- [new RegExp('\\\\ ', 'g'), ' '],
1728
- [new RegExp('\\\\!', 'g'), '!'],
1729
- [new RegExp('\\\\"', 'g'), '"'],
1730
- [new RegExp('\\\\#', 'g'), '#'],
1731
- [new RegExp('\\\\%', 'g'), '%'],
1732
- [new RegExp('\\\\&', 'g'), '&'],
1733
- [new RegExp("\\\\'", 'g'), "'"],
1734
- [new RegExp('\\\\,', 'g'), ','],
1735
- [new RegExp('\\\\-', 'g'), '-'],
1736
- [new RegExp('\\\\:', 'g'), ':'],
1737
- [new RegExp('\\\\;', 'g'), ';'],
1738
- [new RegExp('\\\\<', 'g'), '<'],
1739
- [new RegExp('\\\\=', 'g'), '='],
1740
- [new RegExp('\\\\>', 'g'), '>'],
1741
- [new RegExp('\\\\@', 'g'), '@'],
1742
- [new RegExp('\\\\_', 'g'), '_'],
1743
- [new RegExp('\\\\`', 'g'), '`'],
1744
- [new RegExp('\\\\~', 'g'), '~'],
1745
- [new RegExp('\\\\[zZ]', 'g'), '$'],
1746
- ];
1747
- function safeCreateUnicodeRegExp(pattern) {
1748
- for (const unEscape of replaceEscapedChars) {
1749
- pattern = pattern.replace(unEscape[0], unEscape[1]);
1866
+
1867
+ const NextJsQueriesCommand = {
1868
+ command: "nextjs:queries",
1869
+ describe: "Create the GrapQL Queries to use two queries to load content",
1870
+ builder: yargs => {
1871
+ const updatedArgs = contentTypesBuilder(yargs, { ...ContentTypesArgsDefaults, baseTypes: ['page', 'experience'] });
1872
+ updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
1873
+ return updatedArgs;
1874
+ },
1875
+ handler: async (args, opts) => {
1876
+ // Prepare
1877
+ const { loadedContentTypes, createdTypeFolders } = opts || {};
1878
+ const { components: basePath, _config: { debug }, force } = parseArgs(args);
1879
+ const client = createCmsClient(args);
1880
+ const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
1881
+ // Start process
1882
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL queries\n`));
1883
+ const dependencies = [];
1884
+ const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
1885
+ const updatedTypes = contentTypes.map(contentType => {
1886
+ const typePath = getTypeFolder(typeFolders, contentType.key);
1887
+ const { written, propertyTypes } = createGraphQueries(contentType, typePath, force, debug);
1888
+ dependencies.push(...propertyTypes);
1889
+ return written ? contentType.key : undefined;
1890
+ }).filter(x => x).flat();
1891
+ // Report outcome
1892
+ if (updatedTypes.length > 0)
1893
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL queries for ${updatedTypes.join(', ')}\n`));
1894
+ else
1895
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL queries created/updated\n`));
1896
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL property fragments\n`));
1897
+ const generatedProps = createPropertyFragments(dependencies, allContentTypes, (contentType) => {
1898
+ if (!contentType.key)
1899
+ return undefined;
1900
+ let tf = getTypeFolder(typeFolders, contentType.key);
1901
+ if (!tf) {
1902
+ tf = getContentTypePaths(contentType, basePath, true, debug);
1903
+ typeFolders.push(tf);
1904
+ }
1905
+ return tf;
1906
+ }, force, debug);
1907
+ // Report outcome
1908
+ if (generatedProps.length > 0)
1909
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
1910
+ else
1911
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL property fragments created/updated\n`));
1912
+ if (!opts)
1913
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1750
1914
  }
1751
- return pattern;
1915
+ };
1916
+ function createGraphQueries(contentType, typePath, force, debug) {
1917
+ const baseQueryFile = typePath.queryFile;
1918
+ let mustWrite = true;
1919
+ if (fs.existsSync(baseQueryFile)) {
1920
+ if (force) {
1921
+ if (debug)
1922
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
1923
+ }
1924
+ else {
1925
+ if (debug)
1926
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
1927
+ mustWrite = false;
1928
+ }
1929
+ }
1930
+ else if (debug) {
1931
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
1932
+ }
1933
+ if (mustWrite) {
1934
+ const fragment = GraphQLGen.buildGetQuery(contentType, undefined, new Map());
1935
+ fs.writeFileSync(baseQueryFile, fragment);
1936
+ }
1937
+ return {
1938
+ written: mustWrite,
1939
+ propertyTypes: GraphQLGen.getReferencedPropertyComponents(contentType)
1940
+ };
1752
1941
  }
1753
1942
 
1754
1943
  const SchemaDownloadCommand = {
@@ -1769,12 +1958,12 @@ const SchemaDownloadCommand = {
1769
1958
  const fullSchemaDir = path.normalize(path.join(projectPath, schemaDir));
1770
1959
  const relativeSchemaDir = path.relative(projectPath, fullSchemaDir);
1771
1960
  process.stdout.write(`\n${figures.arrowRight} Validating schema folder ${relativeSchemaDir}\n`);
1772
- const schemaDirInfo = await fs$1.stat(fullSchemaDir).catch((e) => { if (e.code == 'ENOENT')
1961
+ const schemaDirInfo = await fsAsync.stat(fullSchemaDir).catch((e) => { if (e.code == 'ENOENT')
1773
1962
  return undefined;
1774
1963
  else
1775
1964
  throw e; });
1776
1965
  if (!schemaDirInfo) {
1777
- await fs$1.mkdir(fullSchemaDir, { recursive: true });
1966
+ await fsAsync.mkdir(fullSchemaDir, { recursive: true });
1778
1967
  }
1779
1968
  else if (!schemaDirInfo.isDirectory()) {
1780
1969
  process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] The schema directory exists, but is not a directory (${relativeSchemaDir})\n`)));
@@ -1787,7 +1976,7 @@ const SchemaDownloadCommand = {
1787
1976
  const schemaFile = path.normalize(path.join(fullSchemaDir, `${schema.title.toLowerCase()}.schema.json`));
1788
1977
  const relativePath = path.relative(projectPath, schemaFile);
1789
1978
  try {
1790
- await fs$1.writeFile(schemaFile, JSON.stringify(schema.schema, undefined, 2), {
1979
+ await fsAsync.writeFile(schemaFile, JSON.stringify(schema.schema, undefined, 2), {
1791
1980
  encoding: "utf-8",
1792
1981
  flag: force ? 'w' : 'wx'
1793
1982
  });
@@ -1941,13 +2130,6 @@ function groupErrorsByKeyword(errors) {
1941
2130
  toMerge.enum = [toMerge.enum.reduce((prev, current) => prev ? deepmerge(prev, current) : current, undefined)];
1942
2131
  return toMerge;
1943
2132
  }
1944
- function getValidator(schemaObject) {
1945
- const ajv = new Ajv({
1946
- discriminator: true
1947
- });
1948
- addFormats(ajv);
1949
- return ajv.compile(schemaObject);
1950
- }
1951
2133
 
1952
2134
  const SchemaVsCodeCommand = {
1953
2135
  command: "schema:vscode",
@@ -2034,7 +2216,7 @@ function writeFileAsync(path, data) {
2034
2216
  });
2035
2217
  }
2036
2218
 
2037
- var version = "6.0.0-pre8";
2219
+ var version = "6.0.0-pre9";
2038
2220
  var name = "opti-cms";
2039
2221
  var APP = {
2040
2222
  version: version,
@@ -2059,17 +2241,22 @@ const CmsVersionCommand = {
2059
2241
  throw error;
2060
2242
  }
2061
2243
  });
2244
+ const hasPreview2Info = versionInfo.results.preview2Data?.baseUrl ? true : false;
2062
2245
  const info = new Table({
2063
2246
  head: [
2064
2247
  chalk.yellow(chalk.bold("Component")),
2065
2248
  chalk.yellow(chalk.bold("Version"))
2066
2249
  ],
2067
- colWidths: [20, 60],
2250
+ colWidths: [30, 60],
2068
2251
  colAligns: ["left", "left"]
2069
2252
  });
2070
- info.push(["Instance", client.cmsUrl.host]);
2253
+ info.push(["Base URL", versionInfo.baseUrl ?? client.cmsUrl.href]);
2254
+ if (hasPreview2Info)
2255
+ info.push(["Base URL (Preview 2)", versionInfo.results.preview2Data?.baseUrl ?? 'n/a']);
2071
2256
  info.push(["Client API", client.apiVersion]);
2072
2257
  info.push(["Service API", versionInfo.apiVersion]);
2258
+ if (hasPreview2Info)
2259
+ info.push(["Service API (Preview 2)", versionInfo.results.preview2Data?.apiVersion ?? 'n/a']);
2073
2260
  info.push(["CMS Build", versionInfo.cmsVersion]);
2074
2261
  info.push(["Service Build", versionInfo.serviceVersion]);
2075
2262
  info.push(["SDK", APP.version]);
@@ -2192,7 +2379,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
2192
2379
  process.stderr.write(chalk.redBright(` ${figures.cross}The ContentItem ${key} is a reserved item, skipping deletion\n`));
2193
2380
  return removedCount;
2194
2381
  }
2195
- const deleteResult = await client.contentDelete({ path: { key }, query: { permanent: true } }).then(r => r.key).catch((e) => {
2382
+ const deleteResult = await client.preview2ContentDelete({ path: { key }, query: { permanent: true } }).then(r => r.key).catch((e) => {
2196
2383
  if (e.status == 404)
2197
2384
  return key;
2198
2385
  throw e;
@@ -2218,8 +2405,7 @@ async function resetSystemTypes(client) {
2218
2405
  body: {
2219
2406
  key: systemType,
2220
2407
  properties: {}
2221
- },
2222
- query: { ignoreDataLossWarnings: true }
2408
+ }
2223
2409
  }).catch((e) => e.status == 404 ? undefined : null);
2224
2410
  if (newType)
2225
2411
  resetTypes++;
@@ -2320,7 +2506,7 @@ async function getAllTypes(client, batchSize = 100) {
2320
2506
  return actualItems;
2321
2507
  }
2322
2508
  async function getAllAssets(client, parentKey, batchSize = 100) {
2323
- const items = await client.contentListAssets({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
2509
+ const items = await client.preview2ContentListAssets({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
2324
2510
  if (e.status == 404) {
2325
2511
  return {
2326
2512
  items: [],
@@ -2335,7 +2521,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
2335
2521
  const actualItems = items.items ?? [];
2336
2522
  const pageCount = Math.ceil(totalItemCount / pageSize);
2337
2523
  for (let pageNr = 1; pageNr < pageCount; pageNr++) {
2338
- const pageData = await client.contentListAssets({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
2524
+ const pageData = await client.preview2ContentListAssets({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
2339
2525
  if (e.status == 404) {
2340
2526
  return {
2341
2527
  items: [],
@@ -2350,7 +2536,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
2350
2536
  return actualItems;
2351
2537
  }
2352
2538
  async function getAllItems(client, parentKey, batchSize = 100) {
2353
- const items = await client.contentListItems({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
2539
+ const items = await client.preview2ContentListItems({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
2354
2540
  if (e.status == 404) {
2355
2541
  return {
2356
2542
  items: [],
@@ -2365,7 +2551,7 @@ async function getAllItems(client, parentKey, batchSize = 100) {
2365
2551
  const actualItems = items.items ?? [];
2366
2552
  const pageCount = Math.ceil(totalItemCount / pageSize);
2367
2553
  for (let pageNr = 1; pageNr < pageCount; pageNr++) {
2368
- const pageData = await client.contentListItems({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
2554
+ const pageData = await client.preview2ContentListItems({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
2369
2555
  if (e.status == 404) {
2370
2556
  return {
2371
2557
  items: [],
@@ -2387,6 +2573,7 @@ const commands = [
2387
2573
  NextJsComponentsCommand,
2388
2574
  NextJsCreateCommand,
2389
2575
  NextJsFactoryCommand,
2576
+ NextJsFragmentsCommand,
2390
2577
  NextJsQueriesCommand,
2391
2578
  NextJsVisualBuilderCommand,
2392
2579
  SchemaDownloadCommand,