@remkoj/optimizely-cms-cli 6.0.0-pre1 → 6.0.0-pre10

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,14 +1,20 @@
1
- import { globSync, glob } from 'glob';
1
+ import { globSync, glob, globIterate } from 'glob';
2
2
  import { config } from 'dotenv';
3
3
  import { expand } from 'dotenv-expand';
4
- import { 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';
13
+ import createDeepMerge from '@fastify/deepmerge';
14
+ import { Ajv } from 'ajv';
15
+ import addFormats from 'ajv-formats';
16
+ import GraphQLGen from '@remkoj/optimizely-graph-functions/contenttype-loader';
17
+ import deepEqual from 'fast-deep-equal';
12
18
 
13
19
  /**
14
20
  * Prepare the application context, by parsing the .env files in the main
@@ -17,19 +23,25 @@ import { input, select, confirm } from '@inquirer/prompts';
17
23
  * @returns A string array with the files processed
18
24
  */
19
25
  function prepare() {
20
- const envFiles = globSync(".env*").sort((a, b) => b.length - a.length).filter(n => n == ".env" || n == ".env.local" || (process.env.NODE_ENV && n == `.env.${process.env.NODE_ENV}.local`));
21
- expand(config({ path: envFiles }));
26
+ const globResult = globSync(".env*");
27
+ const envFiles = globResult.sort((a, b) => b.length - a.length).filter(n => n == ".env" || n == ".env.local" || (process.env.NODE_ENV && n == `.env.${process.env.NODE_ENV}.local`));
28
+ expand(config({ path: envFiles, debug: false, quiet: true }));
22
29
  return envFiles;
23
30
  }
24
31
 
25
32
  function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
33
+ if (envFiles) {
34
+ process.stdout.write(chalk.bold(`✅ Loaded environment files:`) + "\n");
35
+ envFiles.forEach(envFile => {
36
+ process.stdout.write(` 👉 ${envFile}\n`);
37
+ });
38
+ process.stdout.write("\n");
39
+ }
26
40
  let config;
27
41
  try {
28
- config = getCmsIntegrationApiConfigFromEnvironment();
42
+ config = readPartialEnvConfig();
29
43
  }
30
44
  catch (e) {
31
- if (envFiles)
32
- console.error(chalk.gray(`Included environment files: ${envFiles.join(', ')}`));
33
45
  console.error(chalk.redBright(`${chalk.bold("[ERROR]:")} Error processing environment variables: ${e.message}`));
34
46
  process.exit(1);
35
47
  }
@@ -39,7 +51,7 @@ function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
39
51
  .usage('$0 <cmd> [args]')
40
52
  .option("path", { alias: "p", description: "Application root folder", string: true, type: "string", demandOption: false, default: process.cwd() })
41
53
  .option("components", { alias: "c", description: "Path to components folder", string: true, type: "string", demandOption: false, default: "./src/components/cms" })
42
- .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 })
43
55
  .option("client_id", { alias: "ci", description: "API Client ID", string: true, type: "string", demandOption: isDemanded(config.clientId), default: config.clientId })
44
56
  .option('client_secret', { alias: "cs", description: "API Client Secrent", string: true, type: "string", demandOption: isDemanded(config.clientSecret), default: config.clientSecret })
45
57
  .option('user_id', { alias: "u", description: "Impersonate user id", string: true, type: "string", demandOption: false, default: config.actAs })
@@ -51,12 +63,10 @@ function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
51
63
  .epilogue(`Copyright Remko Jantzen - 2023-${(new Date(Date.now())).getFullYear()}`)
52
64
  .help()
53
65
  .fail((msg, error, args) => {
54
- if (envFiles)
55
- console.error(chalk.gray(`Included environment files: ${envFiles.join(', ')}\n`));
56
66
  if (msg)
57
- console.error(msg + "\n");
67
+ console.error(chalk.redBright(msg) + "\n");
58
68
  if (error)
59
- console.error(`[${chalk.bold(error.name ?? 'Error')}]: ${error.message ?? ''}\n`);
69
+ console.error(chalk.redBright(`[${chalk.bold(error.name ?? 'Error')}]: ${error.message ?? ''}`) + '\n');
60
70
  args.showHelp("error");
61
71
  });
62
72
  }
@@ -105,16 +115,23 @@ function parseArgs({ client_id, client_secret, cms_url, user_id, verbose, path:
105
115
  }
106
116
 
107
117
  function createCmsClient(args) {
108
- const { _config: cfg } = parseArgs(args);
109
- const baseConfig = getCmsIntegrationApiConfigFromEnvironment();
118
+ const cfg = getCmsIntegrationApiOptions(args);
119
+ const baseConfig = readPartialEnvConfig();
110
120
  const client = createClient({
111
121
  ...baseConfig,
112
122
  ...cfg
113
123
  });
114
124
  if (cfg.debug)
115
- 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`));
116
126
  return client;
117
127
  }
128
+ function getCmsIntegrationApiOptions(args) {
129
+ if (typeof args !== 'object' || args === null)
130
+ return undefined;
131
+ if (args.base && (typeof args.base === 'object') && (typeof args.base.href === 'string'))
132
+ return args;
133
+ return parseArgs(args)._config;
134
+ }
118
135
 
119
136
  const StylesPushCommand = {
120
137
  command: "styles:push",
@@ -149,7 +166,12 @@ const StylesPushCommand = {
149
166
  return undefined; // Only include defined styles, if any
150
167
  if (cfg.debug)
151
168
  process.stdout.write(chalk.gray(`${figures.arrowRight} Pushing: ${styleKey}\n`));
152
- 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 }));
153
175
  return newTemplate;
154
176
  }))).filter(isNotNullOrUndefined);
155
177
  const styles = new Table({
@@ -189,80 +211,159 @@ function isNotNullOrUndefined(i) {
189
211
  return i ? true : false;
190
212
  }
191
213
 
192
- const contentTypesBuilder = yargs => {
193
- yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
194
- yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
195
- yargs.option("baseTypes", { alias: 'b', description: "Select only these base types", string: true, type: 'array', demandOption: false, default: [] });
196
- yargs.option("types", { alias: 't', description: "Select only these types", string: true, type: 'array', demandOption: false, default: [] });
197
- 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 });
198
279
  return yargs;
199
280
  };
200
- function getEnumOptions(enumObject) {
201
- return Object.keys(enumObject).filter((item) => {
202
- return isNaN(Number(item));
203
- });
204
- }
205
- async function getContentTypes(client, args, pageSize = 100, allowSystem = false) {
281
+ async function getContentTypes(client, args, pageSize = 25, allowSystem = false, customFilter) {
206
282
  const { _config: cfg, excludeBaseTypes, excludeTypes, baseTypes, types, all } = parseArgs(args);
207
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
208
- if (cfg.debug)
209
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
210
- let resultsPage = await client.contentTypesList({ query: { pageIndex: 0, pageSize } });
211
- const results = resultsPage.items ?? [];
212
- let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
213
- while (pagesRemaining > 0 && results.length < resultsPage.totalItemCount) {
214
- if (cfg.debug)
215
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${resultsPage.pageIndex + 2} of ${Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize)} (${resultsPage.pageSize} items per page)\n`));
216
- resultsPage = await client.contentTypesList({ query: { pageIndex: resultsPage.pageIndex + 1, pageSize: resultsPage.pageSize } });
217
- results.push(...resultsPage.items);
218
- pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
219
- }
220
- if (cfg.debug) {
221
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
222
- process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Content-Types based upon arguments\n`));
223
- }
224
- const validBaseTypes = getEnumOptions(IntegrationApi.ContentBaseType).map(x => x.toLowerCase());
225
- const allContentTypes = all ?
226
- // If we're returning all content types, including non-supported base types, make sure the base type is always set
227
- results.map(contentType => {
228
- return {
229
- ...contentType,
230
- baseType: contentType.baseType ?? 'default'
231
- };
232
- }) :
233
- // Otherwise filter out any non supported content base type
234
- results.filter(contentType => {
235
- const baseType = (contentType.baseType ?? 'default').toLowerCase();
236
- const isValid = validBaseTypes.includes(baseType);
237
- if (!isValid && cfg.debug)
238
- process.stdout.write(chalk.gray(`${figures.arrowRight} Removing ${contentType.key} as it has an unsupported base type: ${baseType}\n`));
239
- return isValid;
240
- });
241
- const contentTypes = allContentTypes.filter(data => {
242
- // Remove items based upon filters
243
- const keepType = (!excludeBaseTypes.includes(data.baseType)) &&
244
- (!excludeTypes.includes(data.key)) &&
245
- (baseTypes.length == 0 || baseTypes.includes(data.baseType)) &&
246
- (types.length == 0 || types.includes(data.key));
247
- if (!keepType) {
283
+ const allContentTypes = [];
284
+ const filteredContentTypes = [];
285
+ for await (const contentType of getAllContentTypes(client, cfg.debug, pageSize)) {
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:')) {
248
288
  if (cfg.debug)
249
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${data.key} due to applied filters\n`));
250
- return false;
289
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it is a reference to external data in Optimizely Graph\n`));
290
+ continue;
251
291
  }
252
- // Skip system types if desired
253
- if (data.source == 'system' && !allowSystem) {
292
+ // Build the unfiltered array
293
+ allContentTypes.push(contentType);
294
+ // Skip based upon base type filters
295
+ if (!shouldInclude(typeToSlug(contentType.baseType), baseTypes.map(x => typeToSlug(x)), excludeBaseTypes.map(x => typeToSlug(x)))) {
254
296
  if (cfg.debug)
255
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${data.key} due to it being a system type\n`));
256
- return false;
297
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it has a restricted base type: ${contentType.baseType}\n`));
298
+ continue;
257
299
  }
258
- return true;
259
- });
300
+ // Skip based upon type filters
301
+ if (!shouldInclude(contentType.key, types, excludeTypes)) {
302
+ if (cfg.debug)
303
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it has a restricted type: ${contentType.key}\n`));
304
+ continue;
305
+ }
306
+ // Skip based upon system filter
307
+ if (contentType.source == 'system' && !allowSystem) {
308
+ if (cfg.debug)
309
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${contentType.key} due to it being a system type\n`));
310
+ continue;
311
+ }
312
+ // Skip based upon custom filter
313
+ if (customFilter && !await customFilter(contentType)) {
314
+ if (cfg.debug)
315
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${contentType.key} due to the custom filter\n`));
316
+ continue;
317
+ }
318
+ // Add to list
319
+ filteredContentTypes.push(contentType);
320
+ }
260
321
  if (cfg.debug)
261
- process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${contentTypes.length} items\n`));
262
- return {
263
- all: allContentTypes,
264
- contentTypes: contentTypes
265
- };
322
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${allContentTypes.length} to ${filteredContentTypes.length} items\n`));
323
+ return { all: allContentTypes, contentTypes: filteredContentTypes };
324
+ }
325
+ function shouldInclude(value, allow, disallow) {
326
+ // Item is allowed when either allow is not set, an empty array or has the value
327
+ const isAllowed = !Array.isArray(allow) || allow.length === 0 || allow.includes(value);
328
+ // Item is disallowed when and the array is set and includes the value
329
+ const isDisallowed = Array.isArray(disallow) && disallow.includes(value);
330
+ return isAllowed && !isDisallowed;
331
+ }
332
+ /**
333
+ * Retrieve all content types as an Async Generator, allowing processing of entries whilest they are being loaded from the CMS instance.
334
+ *
335
+ * @param client
336
+ * @param args
337
+ * @param pageSize
338
+ */
339
+ async function* getAllContentTypes(client, debug = false, pageSize = 25) {
340
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
341
+ let requestPageSize = pageSize;
342
+ let requestPageIndex = 0;
343
+ let totalItemCount = 0;
344
+ let totalPages = 0;
345
+ do {
346
+ const resultsPage = await client.contentTypesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch((_) => {
347
+ return {
348
+ items: [],
349
+ totalItemCount: 0,
350
+ pageIndex: requestPageIndex,
351
+ pageSize: requestPageSize
352
+ };
353
+ });
354
+ // Calculate fields for next page
355
+ totalItemCount = resultsPage.totalItemCount ?? 0;
356
+ requestPageSize = resultsPage.pageSize;
357
+ requestPageIndex = resultsPage.pageIndex + 1;
358
+ totalPages = Math.ceil(totalItemCount / requestPageSize);
359
+ // Debug output
360
+ if (debug)
361
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched contentTypes page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page)\n`));
362
+ // Yield items
363
+ for (const contentType of (resultsPage.items ?? [])) {
364
+ yield contentType;
365
+ }
366
+ } while (requestPageIndex < totalPages);
266
367
  }
267
368
 
268
369
  const stylesBuilder = yargs => {
@@ -274,73 +375,90 @@ const stylesBuilder = yargs => {
274
375
  newArgs.option("templateTypes", { alias: 'tt', description: "Select only these template types", choices: ['node', 'base', 'component'], type: 'array', demandOption: false, default: [] });
275
376
  return newArgs;
276
377
  };
277
- async function getStyles(client, args, pageSize = 100) {
378
+ async function getStyles(client, args, pageSize = 25) {
278
379
  if (client.runtimeCmsVersion == OptiCmsVersion.CMS12)
279
380
  return { all: [], styles: [] };
280
381
  const { _config: cfg, excludeBaseTypes, excludeTypes, excludeNodeTypes, excludeTemplates, baseTypes, types, nodes, templates, templateTypes } = parseArgs(args);
281
382
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Style-Definitions from Optimizely CMS\n`));
282
- if (cfg.debug)
283
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
284
- let resultsPage = await client.displayTemplatesList({ query: { pageIndex: 0, pageSize } });
285
- const results = resultsPage.items ?? [];
286
- let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
287
- while (pagesRemaining > 0 && results.length < resultsPage.totalItemCount) {
288
- if (cfg.debug)
289
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${resultsPage.pageIndex + 2} of ${Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize)} (${resultsPage.pageSize} items per page)\n`));
290
- resultsPage = await client.displayTemplatesList({ query: { pageIndex: resultsPage.pageIndex + 1, pageSize: resultsPage.pageSize } });
291
- results.push(...resultsPage.items);
292
- pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
293
- }
294
- if (cfg.debug) {
295
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Style-Definitions from Optimizely CMS\n`));
296
- process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Style-Definitions based upon arguments\n`));
297
- }
298
- const styles = results.filter(data => {
299
- if (isExcluded(data.key, excludeTemplates, templates)) {
383
+ const allDisplayTemplates = [];
384
+ const filteredDisplayTemplates = [];
385
+ for await (const displayTemplate of getAllStyles(client, cfg.debug, pageSize)) {
386
+ allDisplayTemplates.push(displayTemplate);
387
+ const templateType = displayTemplate.baseType ? 'base' : displayTemplate.nodeType ? 'node' : displayTemplate.contentType ? 'component' : 'unknown';
388
+ if (isExcluded(displayTemplate.key, excludeTemplates, templates)) {
300
389
  if (cfg.debug)
301
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style defintion key filtering active\n`));
302
- return false;
390
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style defintion key filtering active\n`));
391
+ continue;
303
392
  }
304
- const templateType = data.baseType ? 'base' : data.nodeType ? 'node' : data.contentType ? 'component' : 'unknown';
305
393
  if (isExcluded(templateType, [], templateTypes)) {
306
394
  if (cfg.debug)
307
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style type filtering is active\n`));
308
- return false;
395
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style type filtering is active\n`));
396
+ continue;
309
397
  }
310
- if (data.baseType && isExcluded(data.baseType, excludeBaseTypes, baseTypes)) {
398
+ if (displayTemplate.baseType && isExcluded(displayTemplate.baseType, excludeBaseTypes, baseTypes)) {
311
399
  if (cfg.debug)
312
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is defined at base type level and base type filtering is active\n`));
313
- return false;
400
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is defined at base type level and base type filtering is active\n`));
401
+ continue;
314
402
  }
315
- if (data.contentType && isExcluded(data.contentType, excludeTypes, types)) {
403
+ if (displayTemplate.contentType && isExcluded(displayTemplate.contentType, excludeTypes, types)) {
316
404
  if (cfg.debug)
317
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is defined at component type level and component type filtering is active\n`));
318
- return false;
405
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is defined at component type level and component type filtering is active\n`));
406
+ continue;
319
407
  }
320
408
  if (templateType != 'component' && types.length > 0) {
321
409
  if (cfg.debug)
322
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is targeting the ${templateType} level and component type selection is active\n`));
323
- return false;
410
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is targeting the ${templateType} level and component type selection is active\n`));
411
+ continue;
324
412
  }
325
- if (data.nodeType && isExcluded(data.nodeType, excludeNodeTypes, nodes)) {
413
+ if (displayTemplate.nodeType && isExcluded(displayTemplate.nodeType, excludeNodeTypes, nodes)) {
326
414
  if (cfg.debug)
327
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is defined at node type level and node type filtering is active\n`));
328
- return false;
415
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is defined at node type level and node type filtering is active\n`));
416
+ continue;
329
417
  }
330
418
  if (templateType != 'node' && nodes.length > 0) {
331
419
  if (cfg.debug)
332
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is targeting the ${templateType} level and node type selection is active\n`));
333
- return false;
420
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is targeting the ${templateType} level and node type selection is active\n`));
421
+ continue;
334
422
  }
335
- return true;
336
- });
423
+ filteredDisplayTemplates.push(displayTemplate);
424
+ }
337
425
  if (cfg.debug)
338
- process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${styles.length} items\n`));
426
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Applied style filters, reduced from ${allDisplayTemplates.length} to ${filteredDisplayTemplates.length} items\n`));
339
427
  return {
340
- all: results,
341
- styles
428
+ all: allDisplayTemplates,
429
+ styles: filteredDisplayTemplates
342
430
  };
343
431
  }
432
+ async function* getAllStyles(client, debug = false, pageSize = 5) {
433
+ if (client.runtimeCmsVersion == OptiCmsVersion.CMS12)
434
+ return;
435
+ let requestPageSize = pageSize;
436
+ let requestPageIndex = 0;
437
+ let totalItemCount = 0;
438
+ let totalPages = 0;
439
+ do {
440
+ const resultsPage = await client.displayTemplatesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch((_) => {
441
+ return {
442
+ items: [],
443
+ totalItemCount: 0,
444
+ pageIndex: requestPageIndex,
445
+ pageSize: requestPageSize
446
+ };
447
+ });
448
+ // Calculate fields for next page
449
+ totalItemCount = resultsPage.totalItemCount ?? 0;
450
+ requestPageSize = resultsPage.pageSize;
451
+ requestPageIndex = resultsPage.pageIndex + 1;
452
+ totalPages = Math.ceil(totalItemCount / requestPageSize);
453
+ // Debug output
454
+ if (debug)
455
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched displayTemplates page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page)\n`));
456
+ // Yield items
457
+ for (const displayTemplate of (resultsPage.items ?? [])) {
458
+ yield displayTemplate;
459
+ }
460
+ } while (requestPageIndex < totalPages);
461
+ }
344
462
  function isExcluded(value, exclusions, inclusions) {
345
463
  if (value == undefined || value == null)
346
464
  return false;
@@ -356,18 +474,9 @@ async function getStyleFilePath(definition, opts) {
356
474
  return `${opts?.contentBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
357
475
  if (!opts?.client)
358
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");
359
- const pageSize = 50;
360
- let resultsPage = await opts.client.contentTypesList({ query: { pageIndex: 0, pageSize } });
361
- const contentTypes = resultsPage.items ?? [];
362
- let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
363
- while (pagesRemaining > 0 && contentTypes.length < resultsPage.totalItemCount) {
364
- resultsPage = await opts.client.contentTypesList({ query: { pageIndex: resultsPage.pageIndex + 1, pageSize: resultsPage.pageSize } });
365
- contentTypes.push(...resultsPage.items);
366
- pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
367
- }
368
- const fetchedBaseType = contentTypes.filter(x => x.key == definition.contentType).map(x => x.baseType).at(0);
369
- if (fetchedBaseType)
370
- 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`;
371
480
  }
372
481
  throw new Error(`Unable to resolve the target for the DisplayTemplate: ${definition.key}`);
373
482
  }
@@ -411,7 +520,7 @@ const StylesPullCommand = {
411
520
  builder: (yargs) => {
412
521
  const newYargs = stylesBuilder(yargs);
413
522
  newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
414
- 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 });
415
524
  return newYargs;
416
525
  },
417
526
  handler: async (args) => {
@@ -426,29 +535,9 @@ const StylesPullCommand = {
426
535
  process.stdout.write(chalk.gray(`${figures.arrowRight} Start creating .opti-style.json files\n`));
427
536
  const typeFiles = {};
428
537
  const updatedTemplates = (await Promise.all(filteredResults.map(async (displayTemplate) => {
429
- let itemPath = undefined;
430
- let targetType;
431
- let typesPath;
432
- if (displayTemplate.nodeType) {
433
- itemPath = path.join(basePath, 'nodes', displayTemplate.nodeType, displayTemplate.key);
434
- typesPath = path.join(basePath, 'nodes', displayTemplate.nodeType);
435
- targetType = 'node/' + displayTemplate.nodeType;
436
- }
437
- else if (displayTemplate.baseType) {
438
- itemPath = path.join(basePath, displayTemplate.baseType, 'styles', displayTemplate.key);
439
- typesPath = path.join(basePath, displayTemplate.baseType, 'styles');
440
- targetType = 'base/' + displayTemplate.baseType;
441
- }
442
- else if (displayTemplate.contentType) {
443
- const contentType = await client.contentTypesGet({ path: { key: displayTemplate.contentType ?? '-' } });
444
- itemPath = path.join(basePath, contentType.baseType, contentType.key);
445
- typesPath = path.join(basePath, contentType.baseType, contentType.key);
446
- targetType = 'content/' + displayTemplate.contentType;
447
- }
448
- if (!fs.existsSync(itemPath))
449
- fs.mkdirSync(itemPath, { recursive: true });
450
- // Write Style JSON
451
- 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
452
541
  const outputTemplate = { ...displayTemplate };
453
542
  if (outputTemplate.createdBy)
454
543
  delete outputTemplate.createdBy;
@@ -458,6 +547,7 @@ const StylesPullCommand = {
458
547
  delete outputTemplate.created;
459
548
  if (outputTemplate.lastModified)
460
549
  delete outputTemplate.lastModified;
550
+ // Write file to disk
461
551
  if (fs.existsSync(filePath)) {
462
552
  if (!force) {
463
553
  if (cfg.debug)
@@ -475,80 +565,20 @@ const StylesPullCommand = {
475
565
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating style file for ${displayTemplate.key} in ${filePath}\n`));
476
566
  fs.writeFileSync(filePath, JSON.stringify(outputTemplate, undefined, 2));
477
567
  }
568
+ // Ensure we're tracking all files
478
569
  if (!typeFiles[targetType]) {
479
570
  typeFiles[targetType] = {
480
- filePath: path.join(typesPath, 'displayTemplates.ts'),
571
+ filePath: helperPath,
481
572
  templates: []
482
573
  };
483
574
  }
484
- typeFiles[targetType].templates.push({ file: filePath, data: displayTemplate });
575
+ typeFiles[targetType].templates.push({ key: displayTemplate.key, file: filePath, data: displayTemplate });
485
576
  return displayTemplate.key;
486
577
  }))).filter(x => x);
487
578
  //#endregion
488
579
  //#region Create needed definition files
489
- if (definitions) {
490
- process.stdout.write(chalk.gray(`${figures.arrowRight} Start creating displayTemplates.ts files\n`));
491
- for (const targetId of Object.getOwnPropertyNames(typeFiles)) {
492
- const { filePath: typeFilePath, templates } = typeFiles[targetId];
493
- if (fs.existsSync(typeFilePath)) {
494
- if (!force) {
495
- if (cfg.debug)
496
- process.stdout.write(chalk.gray(`${figures.cross} Skipped writing definition file for ${targetId} - it already exists\n`));
497
- continue;
498
- }
499
- if (cfg.debug)
500
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting definition file for ${targetId} - ${typeFilePath}\n`));
501
- }
502
- else if (cfg.debug)
503
- process.stdout.write(chalk.gray(`${figures.arrowRight} Creating definition file for ${targetId} - ${typeFilePath}\n`));
504
- // Write Style definition
505
- const imports = [
506
- 'import type { LayoutProps } from "@remkoj/optimizely-cms-react"',
507
- 'import type { ReactNode } from "react"'
508
- ];
509
- const typeContents = [];
510
- const props = [];
511
- let typeId = targetId.split('/', 2)[1];
512
- templates.forEach(({ file: displayTemplateFile, data: displayTemplate }) => {
513
- const importPath = path.relative(path.dirname(typeFilePath), displayTemplateFile).replaceAll('\\', '/');
514
- imports.push(`import type ${displayTemplate.key}Styles from "./${importPath}"`);
515
- typeContents.push(`export type ${displayTemplate.key}Props = LayoutProps<typeof ${displayTemplate.key}Styles>`);
516
- typeContents.push(`export type ${displayTemplate.key}ComponentProps<DT extends Record<string, any> = Record<string, any>> = {
517
- data: DT
518
- layoutProps: ${displayTemplate.key}Props | undefined
519
- } & JSX.IntrinsicElements['div']`);
520
- typeContents.push(`export type ${displayTemplate.key}Component<DT extends Record<string, any> = Record<string, any>> = (props: ${displayTemplate.key}ComponentProps<DT>) => ReactNode`);
521
- typeContents.push('');
522
- props.push(`${displayTemplate.key}Props`);
523
- if (!typeId)
524
- typeId = displayTemplate.nodeType ?? displayTemplate.baseType ?? displayTemplate.contentType;
525
- });
526
- if (typeId) {
527
- typeId = ucFirst$1(typeId);
528
- typeContents.push('');
529
- typeContents.push(`export type ${typeId}LayoutProps = ${props.join(' | ')}
530
- export type ${typeId}ComponentProps<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = {
531
- data: DT
532
- layoutProps: LP | undefined
533
- } & JSX.IntrinsicElements['div']
534
-
535
- export type ${typeId}Component<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = (props: ${typeId}ComponentProps<DT,LP>) => ReactNode
536
-
537
- export function isDefaultProps(props?: ${typeId}LayoutProps | null) : props is ${templates.filter(t => t.data.isDefault).at(0)?.data?.key}Props
538
- {
539
- return props?.template == "${templates.filter(t => t.data.isDefault).at(0)?.data?.key}"
540
- }`);
541
- templates.forEach(t => {
542
- typeContents.push(`
543
- export function is${t.data.key}Props(props?: ${typeId}LayoutProps | null) : props is ${t.data.key}Props
544
- {
545
- return props?.template == "${t.data.key}"
546
- }`);
547
- });
548
- }
549
- fs.writeFileSync(typeFilePath, imports.join("\n") + "\n\n" + typeContents.join("\n"));
550
- }
551
- }
580
+ if (definitions)
581
+ await createDisplayTemplateHelpers(typeFiles);
552
582
  //#endregion
553
583
  process.stdout.write(chalk.green(chalk.bold(figures.tick + ` Created/updated style definitions for ${updatedTemplates.join(', ')}`)) + "\n");
554
584
  }
@@ -558,6 +588,131 @@ function ucFirst$1(input) {
558
588
  return input;
559
589
  return input[0].toUpperCase() + input.substring(1);
560
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
+ }
561
716
 
562
717
  // Node JS and 3rd Party libraries
563
718
  const StylesCreateCommand = {
@@ -573,11 +728,11 @@ const StylesCreateCommand = {
573
728
  process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
574
729
  return;
575
730
  }
576
- const allowedBaseTypes = ['section', 'element'];
731
+ const allowedBaseTypes = ['section', 'component', 'experience'];
577
732
  // Prepare
578
733
  process.stdout.write(chalk.yellowBright(chalk.bold(`Reading current information from Optimizely CMS\n`)));
579
734
  const [{ contentTypes }, { styles }] = await Promise.all([
580
- getContentTypes(client, { ...args, excludeBaseTypes: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [], all: false }),
735
+ getContentTypes(client, { ...args, excludeBaseTypes: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [], all: false }, 25, false, (ct) => ct.compositionBehaviors?.includes('elementEnabled') || ct.compositionBehaviors?.includes('sectionEnabled') || ct.baseType == 'experience' || ct.baseType == 'section'),
581
736
  getStyles(client, { ...args, excludeBaseTypes: [], excludeNodeTypes: [], excludeTemplates: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [], nodes: [], components: '', templates: [], templateTypes: [], all: false })
582
737
  ]);
583
738
  const styleKeys = styles.map(x => x.key);
@@ -585,27 +740,34 @@ const StylesCreateCommand = {
585
740
  process.stdout.write(chalk.yellowBright(chalk.bold(`\nConfigure your style definition\n`)));
586
741
  const key = await input({ message: "Identifier:", validate: (val) => { return val.match(/^[a-zA-Z][A-Za-z0-9\-_]*$/) != null && !styleKeys.includes(val); } });
587
742
  const displayName = await input({ message: "Display name:" });
588
- const type = (await select({ message: "Style target:", choices: [
589
- { value: "baseType", name: "Base Type", description: "Target all Content-Types that inherit from the specified base type" },
590
- { value: "contentType", name: "Content-Type", description: "Target a specific Content-Type that supports styling" },
591
- { value: "nodeType", name: "Experience Node", description: "Target a specific node type from a Section" }
592
- ] }));
743
+ const type = (await select({
744
+ message: "Style target:", choices: [
745
+ { value: "baseType", name: "Base type", description: "Target all Content-Types that inherit from the specified base type" },
746
+ { value: "contentType", name: "Content type", description: "Target a specific Content-Type that supports styling" },
747
+ { value: "nodeType", name: "Node", description: "Target a specific node type from a Section" }
748
+ ]
749
+ }));
593
750
  let typeId = "";
594
751
  switch (type) {
595
752
  case 'contentType':
596
753
  typeId = await select({ message: "Content-Type:", choices: contentTypes.map(x => { return { value: x.key, description: x.description, name: x.displayName }; }) });
597
754
  break;
598
755
  case 'nodeType':
599
- typeId = await select({ message: "Node type:", choices: [
756
+ typeId = await select({
757
+ message: "Node type:", choices: [
600
758
  { value: "row", description: "Row", name: "Row" },
601
759
  { value: "column", description: "Column", name: "Column" }
602
- ] });
760
+ ]
761
+ });
603
762
  break;
604
763
  case 'baseType':
605
- typeId = await select({ message: "Base type:", choices: [
764
+ typeId = await select({
765
+ message: "Base type:", choices: [
766
+ { value: "experience", description: "Target all experience types", name: "Experience" },
606
767
  { value: "section", description: "Target all section types", name: "Section" },
607
- { value: "element", description: "Target all element types", name: "Element" }
608
- ] });
768
+ { value: "component", description: "Target all component types", name: "Component" }
769
+ ]
770
+ });
609
771
  break;
610
772
  }
611
773
  const isDefault = await confirm({ message: "Should this style be marked as default?" });
@@ -625,8 +787,18 @@ const StylesCreateCommand = {
625
787
  process.exit(0);
626
788
  }
627
789
  }
790
+ // @ToDo: Add properties
628
791
  fs.writeFileSync(path.join(basePath, styleFilePath), JSON.stringify(definition, undefined, 4));
629
792
  process.stdout.write(chalk.yellowBright(chalk.bold(`\nWritten style defintion template to: ${path.normalize(path.join(basePath, styleFilePath))}\n`)));
793
+ if (await confirm({ message: "Do you want to publish this style into Optimizely CMS?" })) {
794
+ const response = await client.displayTemplatesCreate({ body: definition }).catch(_ => undefined);
795
+ if (!response) {
796
+ process.stderr.write(chalk.redBright(chalk.bold(`\[ERROR] Unable to create style definition ${definition.displayName} in Optimizely CMS\n`)));
797
+ }
798
+ else {
799
+ process.stdout.write(chalk.yellowBright(chalk.bold(`\nCreated style definition ${definition.displayName} in Optimizely CMS\n`)));
800
+ }
801
+ }
630
802
  process.stdout.write(chalk.yellowBright(`\n1. Add your properties to the 'settings' list in the file`));
631
803
  process.stdout.write(chalk.yellowBright(`\n2. Run `));
632
804
  process.stdout.write(chalk.whiteBright(`yarn opti-cms styles:push -t ${definition.key}`));
@@ -652,8 +824,7 @@ const TypesPullCommand = {
652
824
  const client = createCmsClient(args);
653
825
  const { contentTypes } = await getContentTypes(client, args);
654
826
  const updatedTypes = contentTypes.map(contentType => {
655
- const typePath = path.join(basePath, contentType.baseType, contentType.key);
656
- const typeFile = path.join(typePath, `${contentType.key}.opti-type.json`);
827
+ const { typePath, typeFile } = getContentTypePaths(contentType, basePath);
657
828
  if (!fs.existsSync(typePath))
658
829
  fs.mkdirSync(typePath, { recursive: true });
659
830
  if (fs.existsSync(typeFile) && !force) {
@@ -664,16 +835,28 @@ const TypesPullCommand = {
664
835
  const outContentType = { ...contentType };
665
836
  if (outContentType.source || outContentType.source == "")
666
837
  delete outContentType.source;
667
- if (outContentType.features)
668
- delete outContentType.features;
669
- if (outContentType.usage)
670
- delete outContentType.usage;
838
+ //if (outContentType.features) delete outContentType.features
839
+ //if (outContentType.usage) delete outContentType.usage
671
840
  if (outContentType.lastModifiedBy)
672
841
  delete outContentType.lastModifiedBy;
673
842
  if (outContentType.lastModified)
674
843
  delete outContentType.lastModified;
675
844
  if (outContentType.created)
676
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
+ }
677
860
  if (debug)
678
861
  process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key})\n`));
679
862
  fs.writeFileSync(typeFile, JSON.stringify(outContentType, undefined, 2));
@@ -682,6 +865,197 @@ const TypesPullCommand = {
682
865
  process.stdout.write(chalk.green(chalk.bold(`${figures.tick} Created/updated type definitions for ${updatedTypes.join(', ')}\n`)));
683
866
  }
684
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
+ }
685
1059
 
686
1060
  const TypesPushCommand = {
687
1061
  command: "types:push",
@@ -712,9 +1086,18 @@ const TypesPushCommand = {
712
1086
  (!baseTypes || baseTypes.length == 0 || baseTypes.includes(data.definition.baseType)) &&
713
1087
  (!types || types.length == 0 || types.includes(data.definition.key));
714
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);
715
1093
  // Output selected types
716
- 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
+ }
717
1099
  process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Pushing ${type.definition.displayName} from ${type.file}\n`));
1100
+ // Filter unwanted fields from the ContentType definition
718
1101
  const outType = { ...type.definition };
719
1102
  if (outType.source)
720
1103
  delete outType.source;
@@ -724,13 +1107,31 @@ const TypesPushCommand = {
724
1107
  delete outType.lastModified;
725
1108
  if (outType.lastModifiedBy || outType.lastModifiedBy == "")
726
1109
  delete outType.lastModifiedBy;
727
- if (outType.features)
728
- delete outType.features;
729
- if (outType.usage)
730
- delete outType.usage;
731
- return client.contentTypesPut({ path: { key: outType.key }, body: outType, query: { ignoreDataLossWarnings: force } })
732
- .then(ct => { return { key: type.definition.key, type: ct, file: type.file }; })
733
- .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
+ }
734
1135
  }));
735
1136
  const overview = new Table({
736
1137
  head: [
@@ -740,255 +1141,32 @@ const TypesPushCommand = {
740
1141
  chalk.yellow(chalk.bold("Message"))
741
1142
  ],
742
1143
  colWidths: [31, 20, 9, 20],
743
- colAligns: ["left", "left", "center", "left"]
744
- });
745
- results.forEach(item => {
746
- overview.push([
747
- item.type.displayName,
748
- item.key,
749
- item.error ? chalk.redBright(chalk.bold(figures.cross)) : chalk.green(chalk.bold(figures.tick)),
750
- item.error ?? ''
751
- ]);
752
- });
753
- process.stdout.write(overview.toString() + "\n");
754
- // Mark us as done
755
- process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
756
- }
757
- };
758
-
759
- const builder = yargs => {
760
- const updatedArgs = contentTypesBuilder(yargs);
761
- updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
762
- return updatedArgs;
763
- };
764
- function createTypeFolders(contentTypes, basePath, debug = false) {
765
- const folders = contentTypes.map(contentType => {
766
- const baseType = contentType.baseType ?? 'default';
767
- // Create the type folder
768
- const typePath = path.join(basePath, baseType, contentType.key);
769
- if (!fs.existsSync(typePath)) {
770
- fs.mkdirSync(typePath, { recursive: true });
771
- if (debug)
772
- process.stdout.write(chalk.gray(`${figures.arrowRight} Created folder ${typePath} for Content-Type ${contentType.key}\n`));
773
- }
774
- // Check folders
775
- if (!fs.statSync(typePath).isDirectory())
776
- throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
777
- return {
778
- type: contentType.key,
779
- path: typePath,
780
- };
781
- });
782
- return folders;
783
- }
784
- function getTypeFolder(list, type) {
785
- return list.filter(x => x.type == type).at(0)?.path;
786
- }
787
-
788
- /**
789
- * Keep track of all generated properties
790
- */
791
- let generatedProps = [];
792
- const NextJsQueriesCommand = {
793
- command: "nextjs:fragments",
794
- describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
795
- builder,
796
- handler: async (args, opts) => {
797
- generatedProps = [];
798
- // Prepare
799
- const { loadedContentTypes, createdTypeFolders } = opts || {};
800
- const { components: basePath, _config: { debug }, force } = parseArgs(args);
801
- const client = createCmsClient(args);
802
- const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
803
- // Start process
804
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments for ${contentTypes.map(x => x.key).join(', ')}\n`));
805
- const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
806
- const updatedTypes = contentTypes.map(contentType => {
807
- const typePath = getTypeFolder(typeFolders, contentType.key);
808
- return createGraphFragments(contentType, typePath, basePath, force, debug, allContentTypes, client.runtimeCmsVersion == OptiCmsVersion.CMS12);
809
- }).filter(x => x).flat();
810
- // Report outcome
811
- if (updatedTypes.length > 0)
812
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
813
- else
814
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
815
- if (!opts)
816
- process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
817
- generatedProps = [];
818
- }
819
- };
820
- function createGraphFragments(contentType, typePath, basePath, force, debug, contentTypes, forCms12 = false) {
821
- const returnValue = [];
822
- const baseType = contentType.baseType ?? 'default';
823
- const baseQueryFile = path.join(typePath, `${contentType.key}.${baseType}.graphql`);
824
- if (fs.existsSync(baseQueryFile)) {
825
- if (force) {
826
- if (debug)
827
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
828
- }
829
- else {
830
- if (debug)
831
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
832
- return undefined;
833
- }
834
- }
835
- else if (debug) {
836
- process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
837
- }
838
- const { fragment, propertyTypes } = createInitialFragment(contentType, false, undefined, forCms12);
839
- fs.writeFileSync(baseQueryFile, fragment);
840
- returnValue.push(contentType.key);
841
- let dependencies = Array.isArray(propertyTypes) ? [...propertyTypes] : [];
842
- while (Array.isArray(dependencies) && dependencies.length > 0) {
843
- let newDependencies = [];
844
- dependencies.forEach(dep => {
845
- const propContentType = contentTypes.filter(x => x.key == dep[0])[0];
846
- if (!propContentType) {
847
- console.warn(`🟠 The content type ${dep[0]} has been referenced, but is not found in the Optimizely CMS instance`);
848
- return;
849
- }
850
- const fullTypeName = forCms12 ? contentType.key + propContentType.key : propContentType.key;
851
- const propertyFragmentFile = path.join(basePath, propContentType.baseType, propContentType.key, `${fullTypeName}.property.graphql`);
852
- const propertyFragmentDir = path.dirname(propertyFragmentFile);
853
- if (!fs.existsSync(propertyFragmentDir))
854
- fs.mkdirSync(propertyFragmentDir, { recursive: true });
855
- if (!fs.existsSync(propertyFragmentFile) || force) {
856
- if (debug)
857
- process.stdout.write(chalk.gray(`${figures.arrowRight} Writing ${propContentType.displayName} (${propContentType.key}) property fragment\n`));
858
- const propContentTypeInfo = createInitialFragment(propContentType, true, contentType, forCms12);
859
- fs.writeFileSync(propertyFragmentFile, propContentTypeInfo.fragment);
860
- returnValue.push(propContentType.key);
861
- if (Array.isArray(propContentTypeInfo.propertyTypes))
862
- newDependencies.push(...propContentTypeInfo.propertyTypes);
863
- }
864
- });
865
- dependencies = newDependencies;
866
- }
867
- return returnValue.length > 0 ? returnValue : undefined;
868
- }
869
- function createInitialFragment(contentType, forProperty = false, forBaseType, forCms12 = false) {
870
- const propertyTypes = [];
871
- const fragmentFields = [];
872
- const typeProps = contentType.properties ?? {};
873
- Object.getOwnPropertyNames(typeProps).forEach(propKey => {
874
- // Exclude system properties, which are not present in Optimizely Graph
875
- if (['experience', 'section'].includes(contentType.baseType) && ['AdditionalData', 'UnstructuredData', 'Layout'].includes(propKey))
876
- return;
877
- // Exclude CMS 12 System Properties
878
- if (forCms12 && ['Categories'].includes(propKey))
879
- return;
880
- const propType = typeProps[propKey].type;
881
- const isConflict = generatedProps.some(x => x.propName == propKey && x.propType != propType);
882
- const propName = isConflict ? `${contentType.key}${propKey}: ${propKey}` : propKey;
883
- // Write the property
884
- switch (propType) {
885
- case IntegrationApi.PropertyDataType.ARRAY:
886
- {
887
- const typeData = typeProps[propKey];
888
- switch (typeData.items.type) {
889
- case IntegrationApi.PropertyDataType.INTEGER:
890
- if (typeData.format == 'categorization')
891
- fragmentFields.push(`${propName} { Id, Name, Description }`);
892
- else
893
- fragmentFields.push(propName);
894
- break;
895
- case IntegrationApi.PropertyDataType.STRING:
896
- fragmentFields.push(propName);
897
- break;
898
- case IntegrationApi.PropertyDataType.CONTENT:
899
- if (contentType.baseType == 'page' || contentType.baseType == 'experience')
900
- fragmentFields.push(`${propName} { ...${forCms12 ? 'PageIContentListItem' : 'BlockData'} }`);
901
- else
902
- fragmentFields.push(`${propName} { ...IContentListItem }`);
903
- break;
904
- case IntegrationApi.PropertyDataType.COMPONENT:
905
- const componentType = typeData.items.contentType;
906
- switch (componentType) {
907
- case 'link':
908
- fragmentFields.push(`${propName} { ...LinkItemData }`);
909
- break;
910
- default:
911
- {
912
- const componentFragmentName = forCms12 ? contentType.key + componentType : componentType + 'Property';
913
- fragmentFields.push(`${propName} { ...${componentFragmentName}Data }`);
914
- propertyTypes.push([componentType, true]);
915
- break;
916
- }
917
- }
918
- break;
919
- case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
920
- fragmentFields.push(`${propName} { ...ReferenceData }`);
921
- break;
922
- default:
923
- fragmentFields.push(`${propName} { __typename }`);
924
- break;
925
- }
926
- break;
927
- }
928
- case IntegrationApi.PropertyDataType.STRING: {
929
- const propDetails = typeProps[propKey];
930
- switch (propDetails.format ?? "") {
931
- case 'html':
932
- fragmentFields.push(forCms12 ? `${propName} { Structure, Html }` : `${propName} { json, html }`);
933
- break;
934
- case 'shortString':
935
- case 'selectOne':
936
- case '':
937
- fragmentFields.push(propName);
938
- break;
939
- default:
940
- if (!isConflict)
941
- 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`));
942
- break;
943
- }
944
- break;
945
- }
946
- case IntegrationApi.PropertyDataType.URL:
947
- fragmentFields.push(forCms12 ? propName : `${propName} { ...LinkData }`);
948
- break;
949
- case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
950
- fragmentFields.push(`${propName} { ...ReferenceData }`);
951
- break;
952
- case IntegrationApi.PropertyDataType.COMPONENT: {
953
- const componentType = typeProps[propKey].contentType;
954
- if (forCms12 && componentType == "link") {
955
- fragmentFields.push(`${propName} { ...LinkItemData }`);
956
- }
957
- else {
958
- const componentFragmentName = forCms12 ? contentType.key + componentType : componentType + 'Property';
959
- fragmentFields.push(`${propName} { ...${componentFragmentName}Data }`);
960
- propertyTypes.push([componentType, true]);
961
- }
962
- break;
963
- }
964
- case IntegrationApi.PropertyDataType.BINARY:
965
- fragmentFields.push(`${propName} { ...BinaryData }`);
966
- break;
967
- default:
968
- fragmentFields.push(propName);
969
- break;
970
- }
971
- generatedProps.push({
972
- propType: typeProps[propKey].type,
973
- propName: propKey
1144
+ colAligns: ["left", "left", "center", "left"]
974
1145
  });
975
- });
976
- if (contentType.baseType == "experience")
977
- fragmentFields.push('...ExperienceData');
978
- if (fragmentFields.length == 0) {
979
- if (forCms12)
980
- fragmentFields.push('empty: _metadata: ContentLink { key: GuidValue }');
981
- else
982
- fragmentFields.push('empty: _metadata { key }');
1146
+ results.forEach(item => {
1147
+ overview.push([
1148
+ item.type.displayName,
1149
+ item.key,
1150
+ item.error ? chalk.redBright(chalk.bold(figures.cross)) : chalk.green(chalk.bold(figures.tick)),
1151
+ item.error ?? ''
1152
+ ]);
1153
+ });
1154
+ process.stdout.write(overview.toString() + "\n");
1155
+ // Mark us as done
1156
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
983
1157
  }
984
- const fragmentTarget = forProperty ? (forCms12 ? (forBaseType?.key ?? '') + contentType.key : contentType.key + 'Property') : contentType.key;
985
- const tpl = `fragment ${fragmentTarget}Data on ${fragmentTarget} {
986
- ${fragmentFields.join("\n ")}
987
- }`;
988
- return {
989
- fragment: tpl,
990
- propertyTypes: propertyTypes.length == 0 ? null : propertyTypes
991
- };
1158
+ };
1159
+
1160
+ const builder = yargs => {
1161
+ const updatedArgs = contentTypesBuilder(yargs);
1162
+ updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
1163
+ return updatedArgs;
1164
+ };
1165
+ function createTypeFolders(contentTypes, basePath, debug = false) {
1166
+ return contentTypes.map(contentType => getContentTypePaths(contentType, basePath, true, debug));
1167
+ }
1168
+ function getTypeFolder(list, type) {
1169
+ return list.filter(x => x.type == type).at(0);
992
1170
  }
993
1171
 
994
1172
  function ucFirst(current) {
@@ -1024,8 +1202,8 @@ const NextJsComponentsCommand = {
1024
1202
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1025
1203
  }
1026
1204
  };
1027
- function createComponent(contentType, typePath, force, debug = false) {
1028
- const componentFile = path.join(typePath, 'index.tsx');
1205
+ function createComponent(contentType, typePathInfo, force, debug = false) {
1206
+ const { componentFile, path: typePath } = typePathInfo;
1029
1207
  if (fs.existsSync(componentFile)) {
1030
1208
  if (!force) {
1031
1209
  if (debug)
@@ -1040,8 +1218,9 @@ function createComponent(contentType, typePath, force, debug = false) {
1040
1218
  // Get type information & short-hands
1041
1219
  const displayTemplate = getDisplayTemplateInfo$1(contentType, typePath);
1042
1220
  const baseDisplayTemplate = getBaseTypeTemplateInfo(contentType, typePath);
1043
- const varName = `${contentType.key}${ucFirst(contentType.baseType ?? 'part')}`;
1044
- 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'];
1045
1224
  if (!tplFn) {
1046
1225
  if (debug)
1047
1226
  process.stdout.write(chalk.redBright(`${figures.cross} Skipping ${contentType.displayName} (${contentType.key}) component - no template for ${contentType.baseType} found\n`));
@@ -1065,6 +1244,11 @@ function getBaseTypeTemplateInfo(contentType, typePath) {
1065
1244
  }
1066
1245
  return undefined;
1067
1246
  }
1247
+ function normalizeBaseType(baseType) {
1248
+ if (baseType.startsWith('_'))
1249
+ return baseType.substring(1);
1250
+ return baseType;
1251
+ }
1068
1252
  function getDisplayTemplateInfo$1(contentType, typePath) {
1069
1253
  const displayTemplatesFile = path.join(typePath, 'displayTemplates.ts');
1070
1254
  if (fs.existsSync(displayTemplatesFile)) {
@@ -1078,24 +1262,50 @@ function getDisplayTemplateInfo$1(contentType, typePath) {
1078
1262
  }
1079
1263
  const Templates = {
1080
1264
  // Default Template for all components without specifics
1081
- 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";
1082
1266
  import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
1083
1267
  import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
1084
1268
  import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
1085
1269
 
1086
1270
  /**
1087
1271
  * ${contentType.displayName}
1272
+ * ---
1088
1273
  * ${contentType.description}
1089
1274
  */
1090
- 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 }) => {
1091
1276
  const componentName = '${contentType.displayName}'
1092
1277
  const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1093
- 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}>
1094
1279
  <div className="font-bold italic">{ componentName }</div>
1095
1280
  <div>{ componentInfo }</div>
1096
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> }
1097
- { children && <div className="mt-4 mx-4 flex flex-col">{ children }</div>}
1098
- </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>
1099
1309
  }
1100
1310
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1101
1311
  ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
@@ -1103,26 +1313,32 @@ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}
1103
1313
  export default ${varName}`,
1104
1314
  // Template for all page component types
1105
1315
  page: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
1106
- 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 ? `
1107
1317
  import { ${displayTemplate} } from "./displayTemplates";` : ''}
1108
1318
  import { getSdk } from "@/gql"
1109
1319
 
1110
1320
  /**
1111
1321
  * ${contentType.displayName}
1322
+ * ---
1112
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)
1113
1330
  */
1114
- 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' : ''} }) => {
1115
1332
  const componentName = '${contentType.displayName}'
1116
1333
  const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1117
1334
  return <div className="mx-auto px-2 container">
1118
1335
  <div className="font-bold italic">{ componentName }</div>
1119
1336
  <div>{ componentInfo }</div>
1120
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> }
1121
- { children && <div className="flex flex-col mt-4 mx-4">{ children }</div>}
1122
1338
  </div>
1123
1339
  }
1124
1340
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1125
- ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1341
+ ${varName}.getDataQuery = () => get${contentType.key}DataDocument
1126
1342
  ${varName}.getMetaData = async (contentLink, locale, client) => {
1127
1343
  const sdk = getSdk(client);
1128
1344
  // Add your metadata logic here
@@ -1132,24 +1348,36 @@ ${varName}.getMetaData = async (contentLink, locale, client) => {
1132
1348
  export default ${varName}`,
1133
1349
  // Template for all experience component types
1134
1350
  experience: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
1135
- import { getFragmentData } from "@/gql/fragment-masking";
1136
- import { ExperienceDataFragmentDoc, CompositionDataFragmentDoc, ${contentType.key}DataFragmentDoc, 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'
1137
1353
  import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
1138
1354
  import { ${displayTemplate} } from "./displayTemplates";` : ''}
1139
- import { getSdk } from "@/gql"
1355
+ import { getSdk } from "@/gql/client"
1140
1356
 
1141
1357
  /**
1142
1358
  * ${contentType.displayName}
1359
+ * ---
1143
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)
1144
1367
  */
1145
- export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, ctx }) => {
1146
- const composition = getFragmentData(CompositionDataFragmentDoc, getFragmentData(ExperienceDataFragmentDoc, data)?.composition)
1147
- return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData" ctx={ctx}>
1148
- { composition && isNode(composition) && <OptimizelyComposition node={composition} /> }
1149
- </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>
1150
1378
  }
1151
1379
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1152
- ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1380
+ ${varName}.getDataQuery = () => get${contentType.key}DataDocument
1153
1381
  ${varName}.getMetaData = async (contentLink, locale, client) => {
1154
1382
  const sdk = getSdk(client);
1155
1383
  // Add your metadata logic here
@@ -1167,7 +1395,7 @@ const NextJsVisualBuilderCommand = {
1167
1395
  // Prepare
1168
1396
  const { components: basePath, _config: { debug }, force } = parseArgs(args);
1169
1397
  // Create the fall-back node and style defintions
1170
- 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 });
1171
1399
  createGenericNode(basePath, force, debug);
1172
1400
  // Get all styles
1173
1401
  const { styles } = await getStyles(createCmsClient(args), { ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'] });
@@ -1178,7 +1406,8 @@ const NextJsVisualBuilderCommand = {
1178
1406
  });
1179
1407
  // Process base styles
1180
1408
  styles.filter(x => typeof (x.baseType) == 'string' && x.baseType.length > 0).map(styleDefinition => {
1181
- 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);
1182
1411
  createSpecificNode(styleDefinition, templatePath, force, debug);
1183
1412
  });
1184
1413
  }
@@ -1198,13 +1427,30 @@ function createSpecificNode(template, templatePath, force = false, debug = false
1198
1427
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${template.displayName} (${template.key}) component\n`));
1199
1428
  if (!fs.existsSync(templatePath))
1200
1429
  fs.mkdirSync(templatePath, { recursive: true });
1430
+ const baseType = template.nodeType ?? template.baseType ?? 'unknown';
1201
1431
  const displayTemplateName = getDisplayTemplateInfo(template, templatePath);
1202
- const component = `import { extractSettings, type CmsLayoutComponent } from "@remkoj/optimizely-cms-react/rsc";${displayTemplateName ? `
1203
- 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")}
1204
1450
 
1205
- export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ contentLink, layoutProps, children }) => {
1451
+ export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ ${componentArgs.join(', ')} }) => {
1206
1452
  const layout = extractSettings(layoutProps);
1207
- return (<div className="vb:${template.nodeType} vb:${template.nodeType}:${template.key}">{ children }</div>);
1453
+ return (<${componentType} ${componentProps.join(' ')}${style}>{ children }</${componentType}>);
1208
1454
  }
1209
1455
 
1210
1456
  export default ${template.key};`;
@@ -1224,13 +1470,19 @@ function createGenericNode(basePath, force, debug) {
1224
1470
  else if (debug)
1225
1471
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating generic node component\n`));
1226
1472
  const nodeContent = `import { CmsEditable, type CmsLayoutComponent } from '@remkoj/optimizely-cms-react/rsc'
1473
+ import { type CSSProperties } from 'react';
1227
1474
 
1228
- export const VisualBuilderNode : CmsLayoutComponent = ({ contentLink, layoutProps, children, ctx }) =>
1475
+ export const VisualBuilderNode : CmsLayoutComponent = ({ editProps, layoutProps, children }) =>
1229
1476
  {
1230
- let className = \`vb:\${layoutProps?.layoutType}\`
1231
- if (layoutProps && layoutProps.layoutType == "section")
1232
- return <CmsEditable as="div" className={ className } cmsId={ contentLink.key } ctx={ctx}>{ children }</CmsEditable>
1233
- 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>
1234
1486
  }
1235
1487
 
1236
1488
  export default VisualBuilderNode`;
@@ -1478,7 +1730,7 @@ const NextJsCreateCommand = {
1478
1730
  await Promise.all([
1479
1731
  TypesPullCommand.handler(args),
1480
1732
  StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: [], definitions: true }),
1481
- NextJsQueriesCommand.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
1733
+ //createFragments.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
1482
1734
  ]);
1483
1735
  process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared types, styles and fragments") + "\n");
1484
1736
  // Then create the components
@@ -1493,6 +1745,484 @@ const NextJsCreateCommand = {
1493
1745
  }
1494
1746
  };
1495
1747
 
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);
1782
+ }
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");
1792
+ }
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`));
1801
+ }
1802
+ else {
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;
1806
+ }
1807
+ }
1808
+ else if (debug) {
1809
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
1810
+ }
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`));
1840
+ }
1841
+ else {
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;
1845
+ }
1846
+ }
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
+ }
1863
+ }
1864
+ return returnValue;
1865
+ }
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");
1914
+ }
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
+ };
1941
+ }
1942
+
1943
+ const SchemaDownloadCommand = {
1944
+ command: "schema:download",
1945
+ describe: "Create JSON schema files for the specified types, which you can use to configure JSON validation in your IDE of choice.",
1946
+ builder: (yargs) => {
1947
+ const newYargs = yargs;
1948
+ newYargs.option('schemaDir', { alias: 'd', description: "The schema path relative to your project root directory", string: true, type: 'string', demandOption: false, default: './.schema' });
1949
+ newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] });
1950
+ newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
1951
+ return newYargs;
1952
+ },
1953
+ async handler(args, opts) {
1954
+ const { _config: cmsConfig, path: projectPath, force, schemaDir, schemas } = parseArgs(args);
1955
+ const client = createCmsClient(cmsConfig);
1956
+ // Loading Schema
1957
+ const schemaDefs = await loadSchema(client, schemas);
1958
+ const fullSchemaDir = path.normalize(path.join(projectPath, schemaDir));
1959
+ const relativeSchemaDir = path.relative(projectPath, fullSchemaDir);
1960
+ process.stdout.write(`\n${figures.arrowRight} Validating schema folder ${relativeSchemaDir}\n`);
1961
+ const schemaDirInfo = await fsAsync.stat(fullSchemaDir).catch((e) => { if (e.code == 'ENOENT')
1962
+ return undefined;
1963
+ else
1964
+ throw e; });
1965
+ if (!schemaDirInfo) {
1966
+ await fsAsync.mkdir(fullSchemaDir, { recursive: true });
1967
+ }
1968
+ else if (!schemaDirInfo.isDirectory()) {
1969
+ process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] The schema directory exists, but is not a directory (${relativeSchemaDir})\n`)));
1970
+ process.exit(1);
1971
+ }
1972
+ process.stdout.write(chalk.greenBright(` ${figures.tick} `));
1973
+ process.stdout.write(`Schema directory exists\n`);
1974
+ process.stdout.write(`\n${figures.arrowRight} Writing schema files\n`);
1975
+ for (const schema of schemaDefs) {
1976
+ const schemaFile = path.normalize(path.join(fullSchemaDir, `${schema.title.toLowerCase()}.schema.json`));
1977
+ const relativePath = path.relative(projectPath, schemaFile);
1978
+ try {
1979
+ await fsAsync.writeFile(schemaFile, JSON.stringify(schema.schema, undefined, 2), {
1980
+ encoding: "utf-8",
1981
+ flag: force ? 'w' : 'wx'
1982
+ });
1983
+ process.stdout.write(chalk.greenBright(` ${figures.tick} `));
1984
+ process.stdout.write(`Written the ${schema.title} schema to ${relativePath}\n`);
1985
+ }
1986
+ catch (err) {
1987
+ if (!force && err.code === 'EEXIST') {
1988
+ process.stdout.write(chalk.yellowBright(` ${figures.info} `));
1989
+ process.stdout.write(`The schema ${schema.title} already exists in ${relativePath}, run with -f to overwrite\n`);
1990
+ }
1991
+ else
1992
+ process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] ${err.toString()}\n`)));
1993
+ }
1994
+ }
1995
+ }
1996
+ };
1997
+
1998
+ const SchemaListCommand = {
1999
+ command: "schema:list",
2000
+ describe: "List all schema's that are available within the SaaS CMS instance",
2001
+ builder: (yargs) => {
2002
+ const newYargs = yargs;
2003
+ return newYargs;
2004
+ },
2005
+ async handler(args, opts) {
2006
+ const client = createCmsClient(args);
2007
+ process.stdout.write(`\n${figures.arrowRight} Downloading OpenAPI Specification\n`);
2008
+ const spec = await client.getOpenApiSpec();
2009
+ process.stdout.write(chalk.greenBright(` ${figures.tick} `));
2010
+ process.stdout.write(`The ${spec.info.title} (Version: ${spec.info.version}) specification has been downloaded.\n`);
2011
+ const specSchemas = (spec?.components?.schemas ?? {});
2012
+ const schemas = new Table({
2013
+ head: [
2014
+ chalk.yellow(chalk.bold("Key")),
2015
+ chalk.yellow(chalk.bold("Description"))
2016
+ ],
2017
+ colWidths: [35, 60],
2018
+ colAligns: ["left", "left"],
2019
+ wordWrap: true,
2020
+ wrapOnWordBoundary: true
2021
+ });
2022
+ for (const key in specSchemas)
2023
+ if (key) {
2024
+ schemas.push([key, specSchemas[key].description?.replaceAll(/\s+/g, ' ') ?? `-`]);
2025
+ }
2026
+ process.stdout.write("\n" + schemas.toString() + "\n");
2027
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
2028
+ }
2029
+ };
2030
+
2031
+ const deepmerge = createDeepMerge();
2032
+ const SchemaValidateCommand = {
2033
+ command: "schema:validate",
2034
+ describe: "Validate the opti-type.json & opti-style.json files",
2035
+ builder: (yargs) => {
2036
+ const newYargs = yargs;
2037
+ //newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] })
2038
+ //newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false })
2039
+ //newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true })
2040
+ return newYargs;
2041
+ },
2042
+ handler: async (args) => {
2043
+ const projectPath = args.path;
2044
+ const client = createCmsClient(args);
2045
+ const schemas = await loadSchema(client, ['DisplayTemplate', 'ContentType']);
2046
+ process.stdout.write(``);
2047
+ const styleSchema = schemas.find(x => x.title == 'DisplayTemplate')?.schema;
2048
+ const typeSchema = schemas.find(x => x.title == 'ContentType')?.schema;
2049
+ const [styleValidator, typeValidator] = await Promise.all([getValidator(styleSchema), getValidator(typeSchema)]);
2050
+ process.stdout.write(`\n${figures.arrowRight} Validating display templates (*.opti-style.json) and content types (*.opti-type.json).\n`);
2051
+ const patterns = ["./**/*.opti-type.json", "./**/*.opti-style.json"];
2052
+ const globMatches = globIterate(patterns, { cwd: projectPath, absolute: true });
2053
+ let allOk = true;
2054
+ for await (const fileMatch of globMatches) {
2055
+ try {
2056
+ const content = JSON.parse(fs.readFileSync(fileMatch).toString());
2057
+ let errors = undefined;
2058
+ if (fileMatch.endsWith('.opti-type.json') && !typeValidator(content)) {
2059
+ errors = typeValidator.errors;
2060
+ }
2061
+ else if (fileMatch.endsWith('.opti-style.json') && !styleValidator(content)) {
2062
+ errors = styleValidator.errors;
2063
+ }
2064
+ if (errors) {
2065
+ allOk = false;
2066
+ process.stdout.write(chalk.redBright(chalk.bold(`\n${figures.cross} ${path.relative(projectPath, fileMatch)}\n`)));
2067
+ const groupedErrors = groupErrors(errors);
2068
+ for (const instancepath in groupedErrors) {
2069
+ for (const key in groupedErrors[instancepath]) {
2070
+ const errorList = groupedErrors[instancepath][key].filter((v, i, a) => a.findIndex(pv => deepEqual(v, pv)) === i);
2071
+ for (const error of errorList) {
2072
+ process.stdout.write(` Node ${instancepath} ${error.message}\n`);
2073
+ switch (error.keyword) {
2074
+ case 'enum':
2075
+ process.stdout.write(` Allowed values: ${error.params.allowedValues.map((x) => x.toString()).join(', ')}\n`);
2076
+ break;
2077
+ case 'oneOf':
2078
+ const matching = Array.isArray(error.params.passingSchemas) ? error.params.passingSchemas : [];
2079
+ if (matching.length === 0)
2080
+ process.stdout.write(` Currently none matching\n`);
2081
+ else
2082
+ process.stdout.write(` Currently matching ${matching.map((x) => x.toString()).join(", ")}\n`);
2083
+ break;
2084
+ }
2085
+ }
2086
+ }
2087
+ }
2088
+ }
2089
+ }
2090
+ catch (e) {
2091
+ allOk = false;
2092
+ process.stdout.write(chalk.redBright(chalk.bold(`${figures.cross} ${path.relative(projectPath, fileMatch)}\n`)));
2093
+ process.stdout.write(` ${e.message}\n`);
2094
+ }
2095
+ }
2096
+ process.stdout.write(`\n`);
2097
+ if (allOk)
2098
+ process.stdout.write(chalk.greenBright(`${figures.tick} All types & styles match the schema\n`));
2099
+ else {
2100
+ process.stdout.write(chalk.redBright(`${figures.cross} Some types & styles contain errors\n`));
2101
+ process.exit(1);
2102
+ }
2103
+ }
2104
+ };
2105
+ function groupErrors(errors) {
2106
+ const errorrsByInstance = groupErrorsByPath(errors);
2107
+ const instancePaths = Object.getOwnPropertyNames(errorrsByInstance);
2108
+ return instancePaths.reduce((previous, current) => {
2109
+ previous[current] = groupErrorsByKeyword(errorrsByInstance[current]);
2110
+ return previous;
2111
+ }, {});
2112
+ }
2113
+ function groupErrorsByPath(errors) {
2114
+ return errors.reduce((previous, current) => {
2115
+ const instancePath = current.instancePath == "" ? "[ROOT]" : current.instancePath;
2116
+ const partial = {};
2117
+ partial[instancePath] = [current];
2118
+ return deepmerge(previous, partial);
2119
+ }, {});
2120
+ }
2121
+ function groupErrorsByKeyword(errors) {
2122
+ const toMerge = errors.reduce((previous, current) => {
2123
+ const keyword = current.keyword;
2124
+ const partial = {};
2125
+ partial[keyword] = [current];
2126
+ return deepmerge(previous, partial);
2127
+ }, {});
2128
+ // Enum errors must be merged
2129
+ if (toMerge.enum)
2130
+ toMerge.enum = [toMerge.enum.reduce((prev, current) => prev ? deepmerge(prev, current) : current, undefined)];
2131
+ return toMerge;
2132
+ }
2133
+
2134
+ const SchemaVsCodeCommand = {
2135
+ command: "schema:vscode",
2136
+ describe: "Configure schema validation for opti-type.json & opti-style.json files by VSCode in the current project folder",
2137
+ builder: (yargs) => {
2138
+ const newYargs = yargs;
2139
+ //newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] })
2140
+ //newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false })
2141
+ //newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true })
2142
+ return newYargs;
2143
+ },
2144
+ handler: async (args) => {
2145
+ const projectPath = args.path;
2146
+ const client = createCmsClient(args);
2147
+ const schemas = await loadSchema(client, ['DisplayTemplate', 'ContentType']);
2148
+ process.stdout.write(`\n${figures.arrowRight} Writing schema files\n`);
2149
+ for (const schema of schemas) {
2150
+ const schemaFile = path.join(projectPath, '.vscode', `${schema.title.toLowerCase()}.schema.json`);
2151
+ void await writeFileAsync(schemaFile, JSON.stringify(schema.schema, undefined, 2));
2152
+ process.stdout.write(` ${figures.tick} Written the ${schema.title} schema to ${path.relative(projectPath, schemaFile)}\n`);
2153
+ }
2154
+ process.stdout.write(`\n${figures.arrowRight} Updating folder settings\n`);
2155
+ const settingsFile = path.join(projectPath, '.vscode', 'settings.json');
2156
+ const settings = (await readJsonFileAsync(settingsFile).catch(() => undefined)) ?? {};
2157
+ settings['json.validate.enable'] = true;
2158
+ settings['json.schemaDownload.enable'] = true;
2159
+ settings['json.schemas'] = settings['json.schemas'] || [];
2160
+ const hasContentType = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.opti-type.json')) >= 0) >= 0;
2161
+ const hasStyleDefinition = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.opti-style.json')) >= 0) >= 0;
2162
+ const hasSchemaType = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.schema.json')) >= 0) >= 0;
2163
+ if (!hasContentType)
2164
+ settings['json.schemas'].push({
2165
+ "fileMatch": [
2166
+ "**/*.opti-type.json"
2167
+ ],
2168
+ "url": "./.vscode/contenttype.schema.json"
2169
+ });
2170
+ if (!hasStyleDefinition)
2171
+ settings['json.schemas'].push({
2172
+ "fileMatch": [
2173
+ "**/*.opti-style.json"
2174
+ ],
2175
+ "url": "./.vscode/displaytemplate.schema.json"
2176
+ });
2177
+ if (!hasSchemaType)
2178
+ settings['json.schemas'].push({
2179
+ "fileMatch": [
2180
+ "**/*.schema.json"
2181
+ ],
2182
+ "url": "https://json-schema.org/draft-07/schema"
2183
+ });
2184
+ if (settings['json.schemas'].length == 0)
2185
+ delete settings['json.schemas'];
2186
+ await writeFileAsync(settingsFile, JSON.stringify(settings, undefined, 2));
2187
+ }
2188
+ };
2189
+ function readJsonFileAsync(path) {
2190
+ return new Promise((resolve, reject) => fs.readFile(path, (err, data) => {
2191
+ if (err) {
2192
+ if (err?.code === 'ENOENT')
2193
+ resolve(undefined);
2194
+ else
2195
+ reject(err);
2196
+ }
2197
+ else {
2198
+ try {
2199
+ const d = data.toString();
2200
+ resolve(JSON.parse(d));
2201
+ }
2202
+ catch (e) {
2203
+ reject(e);
2204
+ }
2205
+ }
2206
+ }));
2207
+ }
2208
+ function writeFileAsync(path, data) {
2209
+ return new Promise((resolve, reject) => {
2210
+ fs.writeFile(path, data, (err) => {
2211
+ if (err)
2212
+ reject(err);
2213
+ else
2214
+ resolve();
2215
+ });
2216
+ });
2217
+ }
2218
+
2219
+ var version = "6.0.0-pre10";
2220
+ var name = "opti-cms";
2221
+ var APP = {
2222
+ version: version,
2223
+ name: name
2224
+ };
2225
+
1496
2226
  const CmsVersionCommand = {
1497
2227
  command: "cms:version",
1498
2228
  aliases: "$0",
@@ -1511,19 +2241,25 @@ const CmsVersionCommand = {
1511
2241
  throw error;
1512
2242
  }
1513
2243
  });
2244
+ const hasPreview2Info = versionInfo.results.preview2Data?.baseUrl ? true : false;
1514
2245
  const info = new Table({
1515
2246
  head: [
1516
2247
  chalk.yellow(chalk.bold("Component")),
1517
2248
  chalk.yellow(chalk.bold("Version"))
1518
2249
  ],
1519
- colWidths: [20, 60],
2250
+ colWidths: [30, 60],
1520
2251
  colAligns: ["left", "left"]
1521
2252
  });
1522
- info.push(["Instance", client.cmsUrl.host]);
1523
- info.push(["Client", client.apiVersion]);
1524
- info.push(["API", versionInfo.apiVersion]);
1525
- info.push(["CMS", versionInfo.cmsVersion]);
1526
- info.push(["Service", versionInfo.serviceVersion]);
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']);
2256
+ info.push(["Client API", client.apiVersion]);
2257
+ info.push(["Service API", versionInfo.apiVersion]);
2258
+ if (hasPreview2Info)
2259
+ info.push(["Service API (Preview 2)", versionInfo.results.preview2Data?.apiVersion ?? 'n/a']);
2260
+ info.push(["CMS Build", versionInfo.cmsVersion]);
2261
+ info.push(["Service Build", versionInfo.serviceVersion]);
2262
+ info.push(["SDK", APP.version]);
1527
2263
  process.stdout.write(info.toString() + "\n");
1528
2264
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Optimizely CMS Status: ${versionInfo.status}\n`));
1529
2265
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
@@ -1643,7 +2379,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
1643
2379
  process.stderr.write(chalk.redBright(` ${figures.cross}The ContentItem ${key} is a reserved item, skipping deletion\n`));
1644
2380
  return removedCount;
1645
2381
  }
1646
- 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) => {
1647
2383
  if (e.status == 404)
1648
2384
  return key;
1649
2385
  throw e;
@@ -1669,8 +2405,7 @@ async function resetSystemTypes(client) {
1669
2405
  body: {
1670
2406
  key: systemType,
1671
2407
  properties: {}
1672
- },
1673
- query: { ignoreDataLossWarnings: true }
2408
+ }
1674
2409
  }).catch((e) => e.status == 404 ? undefined : null);
1675
2410
  if (newType)
1676
2411
  resetTypes++;
@@ -1771,7 +2506,7 @@ async function getAllTypes(client, batchSize = 100) {
1771
2506
  return actualItems;
1772
2507
  }
1773
2508
  async function getAllAssets(client, parentKey, batchSize = 100) {
1774
- 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) => {
1775
2510
  if (e.status == 404) {
1776
2511
  return {
1777
2512
  items: [],
@@ -1786,7 +2521,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
1786
2521
  const actualItems = items.items ?? [];
1787
2522
  const pageCount = Math.ceil(totalItemCount / pageSize);
1788
2523
  for (let pageNr = 1; pageNr < pageCount; pageNr++) {
1789
- 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) => {
1790
2525
  if (e.status == 404) {
1791
2526
  return {
1792
2527
  items: [],
@@ -1801,7 +2536,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
1801
2536
  return actualItems;
1802
2537
  }
1803
2538
  async function getAllItems(client, parentKey, batchSize = 100) {
1804
- 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) => {
1805
2540
  if (e.status == 404) {
1806
2541
  return {
1807
2542
  items: [],
@@ -1816,7 +2551,7 @@ async function getAllItems(client, parentKey, batchSize = 100) {
1816
2551
  const actualItems = items.items ?? [];
1817
2552
  const pageCount = Math.ceil(totalItemCount / pageSize);
1818
2553
  for (let pageNr = 1; pageNr < pageCount; pageNr++) {
1819
- 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) => {
1820
2555
  if (e.status == 404) {
1821
2556
  return {
1822
2557
  items: [],
@@ -1833,28 +2568,26 @@ async function getAllItems(client, parentKey, batchSize = 100) {
1833
2568
 
1834
2569
  // Style processing
1835
2570
  const commands = [
2571
+ CmsResetCommand,
2572
+ CmsVersionCommand,
2573
+ NextJsComponentsCommand,
2574
+ NextJsCreateCommand,
2575
+ NextJsFactoryCommand,
2576
+ NextJsFragmentsCommand,
2577
+ NextJsQueriesCommand,
2578
+ NextJsVisualBuilderCommand,
2579
+ SchemaDownloadCommand,
2580
+ SchemaListCommand,
2581
+ SchemaValidateCommand,
2582
+ SchemaVsCodeCommand,
1836
2583
  StylesCreateCommand,
1837
- StylesPushCommand,
1838
2584
  StylesListCommand,
1839
2585
  StylesPullCommand,
2586
+ StylesPushCommand,
1840
2587
  TypesPullCommand,
1841
- TypesPushCommand,
1842
- NextJsCreateCommand,
1843
- NextJsQueriesCommand,
1844
- NextJsComponentsCommand,
1845
- NextJsVisualBuilderCommand,
1846
- NextJsFactoryCommand,
1847
- CmsResetCommand,
1848
- CmsVersionCommand
2588
+ TypesPushCommand
1849
2589
  ];
1850
2590
 
1851
- var version = "6.0.0-pre1";
1852
- var name = "opti-cms";
1853
- var APP = {
1854
- version: version,
1855
- name: name
1856
- };
1857
-
1858
2591
  async function main() {
1859
2592
  const envFiles = prepare();
1860
2593
  const app = createOptiCmsApp(APP.name, APP.version, undefined, envFiles);
@@ -1864,7 +2597,7 @@ async function main() {
1864
2597
  }
1865
2598
  catch {
1866
2599
  //We're ignoring error here, as yargs will already generate the "nice output" for it
1867
- //console.log ('Caught error')
2600
+ //console.log('Caught error')
1868
2601
  }
1869
2602
  }
1870
2603
  main();