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

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,26 @@ 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;
671
- if (outContentType.lastModifiedBy)
838
+ if (outContentType.lastModifiedBy || outContentType.lastModifiedBy == "")
672
839
  delete outContentType.lastModifiedBy;
673
840
  if (outContentType.lastModified)
674
841
  delete outContentType.lastModified;
675
842
  if (outContentType.created)
676
843
  delete outContentType.created;
844
+ for (const propName of Object.getOwnPropertyNames(outContentType.properties ?? {})) {
845
+ if (!["content", "contentReference"].includes(outContentType.properties[propName].type)) {
846
+ if (isEmptyArray(outContentType.properties[propName].allowedTypes))
847
+ delete outContentType.properties[propName].allowedTypes;
848
+ if (isEmptyArray(outContentType.properties[propName].restrictedTypes))
849
+ delete outContentType.properties[propName].restrictedTypes;
850
+ }
851
+ if (outContentType.properties[propName].type == 'array' && !["content", "contentReference"].includes(outContentType.properties[propName].items?.type)) {
852
+ if (isEmptyArray(outContentType.properties[propName].items.allowedTypes))
853
+ delete outContentType.properties[propName].items.allowedTypes;
854
+ if (isEmptyArray(outContentType.properties[propName].items.restrictedTypes))
855
+ delete outContentType.properties[propName].items.restrictedTypes;
856
+ }
857
+ }
677
858
  if (debug)
678
859
  process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key})\n`));
679
860
  fs.writeFileSync(typeFile, JSON.stringify(outContentType, undefined, 2));
@@ -682,6 +863,197 @@ const TypesPullCommand = {
682
863
  process.stdout.write(chalk.green(chalk.bold(`${figures.tick} Created/updated type definitions for ${updatedTypes.join(', ')}\n`)));
683
864
  }
684
865
  };
866
+ function isEmptyArray(toTest) {
867
+ return toTest === null || !Array.isArray(toTest) || toTest.length === 0;
868
+ }
869
+
870
+ const deepmerge$1 = createDeepMerge();
871
+ async function loadSchema(client, schemaName) {
872
+ const schemas = Array.isArray(schemaName) ? schemaName : [schemaName];
873
+ process.stdout.write(`${figures.arrowRight} Downloading current JSON Schema\n`);
874
+ const spec = await client.getOpenApiSpec();
875
+ const specSchemas = spec.components?.schemas ?? {};
876
+ process.stdout.write(` ${figures.tick} Complete\n`);
877
+ const result = [];
878
+ process.stdout.write(`\n${figures.arrowRight} Constructing schema for ${schemas.join(', ')}\n`);
879
+ for await (const schema of schemas)
880
+ if (specSchemas[schema]) {
881
+ const definitions = {};
882
+ const processedSchema = processSchema(specSchemas[schema], definitions, spec);
883
+ const jsonSchema = {
884
+ //"$schema": "https://json-schema.org/draft-07/schema",
885
+ "$id": new URL(`schema/${schema}`, client.getSchemaItemBase()).href,
886
+ type: "object",
887
+ title: schema,
888
+ ...processedSchema,
889
+ definitions
890
+ };
891
+ result.push({
892
+ title: schema,
893
+ schema: postProcessDefintions(jsonSchema)
894
+ });
895
+ process.stdout.write(` ${figures.tick} Constructed schema of ${schema}\n`);
896
+ }
897
+ return result;
898
+ }
899
+ function getValidator(schemaObject) {
900
+ const ajv = new Ajv({
901
+ discriminator: true
902
+ });
903
+ addFormats(ajv);
904
+ return ajv.compile(schemaObject);
905
+ }
906
+ function postProcessDefintions(jsonSchema) {
907
+ if (!jsonSchema.definitions)
908
+ return jsonSchema;
909
+ for (const definitionName in jsonSchema.definitions) {
910
+ if ((definitionName.endsWith("Property") || definitionName.endsWith("ListItem")) && jsonSchema.definitions[definitionName].properties) {
911
+ const type = jsonSchema.definitions[definitionName].properties?.type;
912
+ if (isRefSchema(type)) {
913
+ const typeSchema = resolveRefSchema(type, jsonSchema);
914
+ if (typeSchema && typeSchema.type === 'string' && Array.isArray(typeSchema.enum)) {
915
+ let typeValue = definitionName.substring(0, definitionName.length - 8);
916
+ typeValue = typeValue[0].toLowerCase() + typeValue.substring(1);
917
+ typeValue = typeValue === 'list' ? 'array' : typeValue;
918
+ typeValue = typeValue === 'jsonString' ? 'json' : typeValue;
919
+ if (typeSchema.enum.includes(typeValue)) {
920
+ const newTypeDef = deepmerge$1({}, typeSchema);
921
+ newTypeDef.enum = newTypeDef.enum.filter((x) => x === typeValue);
922
+ jsonSchema.definitions[definitionName].properties.type = newTypeDef;
923
+ }
924
+ }
925
+ }
926
+ }
927
+ }
928
+ return jsonSchema;
929
+ }
930
+ /**
931
+ * Schema normalization to tranform the schema from a .Net generated OpenAPI Schema
932
+ * to a AJV compatible JSON Schema
933
+ *
934
+ * @see https://ajv.js.org/
935
+ * @param schema
936
+ * @param defs
937
+ * @param spec
938
+ * @returns
939
+ */
940
+ function processSchema(schema, defs, spec, mergeAllOf = true) {
941
+ if (Array.isArray(schema))
942
+ return schema.map(s => processSchema(s, defs, spec, mergeAllOf));
943
+ if (typeof schema !== 'object' || schema === null)
944
+ return schema;
945
+ const props = Object.getOwnPropertyNames(schema);
946
+ if (props.length === 1 && props[0] === '$ref') {
947
+ const ref = schema['$ref'];
948
+ if (isLocalRef(ref)) {
949
+ const refName = getLocalRefName(ref);
950
+ if (!defs[refName]) {
951
+ const refItem = resolveLocalRef(ref, spec);
952
+ defs[refName] = processSchema(refItem, defs, spec, mergeAllOf);
953
+ }
954
+ const newRef = `#/definitions/${refName}`;
955
+ return { "$ref": newRef };
956
+ }
957
+ else {
958
+ return schema;
959
+ }
960
+ }
961
+ else if (mergeAllOf && props.includes('allOf') && Array.isArray(schema['allOf'])) {
962
+ // process all of
963
+ const newObject = props.reduce((generated, propName) => {
964
+ if (propName != 'allOf')
965
+ generated[propName] = schema[propName];
966
+ return generated;
967
+ }, {});
968
+ const allOfSchemas = schema['allOf'].map((subschema) => {
969
+ const resolvedSchema = isRefSchema(subschema) ? resolveRefSchema(subschema, spec) : subschema;
970
+ const resolved = processSchema(resolvedSchema, defs, spec, mergeAllOf);
971
+ return resolved;
972
+ });
973
+ const merged = allOfSchemas.reduce((previous, current) => deepmerge$1(previous, current), newObject);
974
+ if (newObject['description'])
975
+ merged['description'] = newObject['description'];
976
+ if (newObject['title'])
977
+ merged['title'] = newObject['title'];
978
+ return merged;
979
+ }
980
+ else {
981
+ const newSchema = {};
982
+ for (const key of props) {
983
+ if (key === 'pattern' && typeof schema[key] === 'string') {
984
+ newSchema[key] = safeCreateUnicodeRegExp(schema[key]);
985
+ /*} else if (key === 'readOnly' && schema[key] === true) {
986
+ return undefined*/
987
+ }
988
+ else if (['additionalProperties', 'required', 'properties', 'discriminator'].includes(key) && typeof schema[key] !== 'object') ;
989
+ else if (key === 'discriminator' && !Array.isArray(schema['oneOf'])) ;
990
+ else if (key === 'discriminator' && !schema['type']) {
991
+ newSchema.type = 'object';
992
+ }
993
+ else {
994
+ const keyVal = processSchema(schema[key], defs, spec, mergeAllOf);
995
+ if (keyVal !== undefined)
996
+ newSchema[key] = keyVal;
997
+ }
998
+ }
999
+ if (newSchema['oneOf'] && Array.isArray(newSchema['oneOf']) && newSchema['nullable']) {
1000
+ delete newSchema['nullable'];
1001
+ }
1002
+ return newSchema;
1003
+ }
1004
+ }
1005
+ function isRefSchema(schema) {
1006
+ if (typeof schema != 'object' || schema == null)
1007
+ return false;
1008
+ return Object.getOwnPropertyNames(schema).length === 1 && typeof schema['$ref'] === 'string';
1009
+ }
1010
+ function resolveRefSchema(schema, spec) {
1011
+ if (!isRefSchema(schema))
1012
+ return undefined;
1013
+ return resolveLocalRef(schema['$ref'], spec);
1014
+ }
1015
+ function isLocalRef(ref) {
1016
+ return typeof ref === 'string' && ref.startsWith('#');
1017
+ }
1018
+ function getLocalRefName(ref) {
1019
+ if (!isLocalRef(ref))
1020
+ return undefined;
1021
+ const refPath = ref.substring(2).split('/');
1022
+ return refPath.at(refPath.length - 1);
1023
+ }
1024
+ function resolveLocalRef(ref, spec) {
1025
+ const refPath = ref.substring(2).split('/');
1026
+ return refPath.reduce((integrator, current) => {
1027
+ return integrator ? integrator[current] : undefined;
1028
+ }, spec);
1029
+ }
1030
+ const replaceEscapedChars = [
1031
+ [new RegExp('\\\\ ', 'g'), ' '],
1032
+ [new RegExp('\\\\!', 'g'), '!'],
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('\\\\[zZ]', 'g'), '$'],
1050
+ ];
1051
+ function safeCreateUnicodeRegExp(pattern) {
1052
+ for (const unEscape of replaceEscapedChars) {
1053
+ pattern = pattern.replace(unEscape[0], unEscape[1]);
1054
+ }
1055
+ return pattern;
1056
+ }
685
1057
 
686
1058
  const TypesPushCommand = {
687
1059
  command: "types:push",
@@ -712,9 +1084,18 @@ const TypesPushCommand = {
712
1084
  (!baseTypes || baseTypes.length == 0 || baseTypes.includes(data.definition.baseType)) &&
713
1085
  (!types || types.length == 0 || types.includes(data.definition.key));
714
1086
  });
1087
+ // Create validator
1088
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Loading OpenAPI Specification for validation\n`));
1089
+ const typeSchema = (await loadSchema(client, 'ContentType')).at(0)?.schema;
1090
+ const typeValidator = await getValidator(typeSchema);
715
1091
  // Output selected types
716
- const results = await Promise.all(typeDefinitions.map(type => {
1092
+ const results = await Promise.all(typeDefinitions.map(async (type) => {
1093
+ if (!type.definition.key) {
1094
+ process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Content Type has no key in ${type.file}\n`));
1095
+ return;
1096
+ }
717
1097
  process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Pushing ${type.definition.displayName} from ${type.file}\n`));
1098
+ // Filter unwanted fields from the ContentType definition
718
1099
  const outType = { ...type.definition };
719
1100
  if (outType.source)
720
1101
  delete outType.source;
@@ -724,13 +1105,31 @@ const TypesPushCommand = {
724
1105
  delete outType.lastModified;
725
1106
  if (outType.lastModifiedBy || outType.lastModifiedBy == "")
726
1107
  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 }; });
1108
+ //if (outType.features) delete outType.features
1109
+ //if (outType.usage) delete outType.usage
1110
+ const validationResult = typeValidator(outType);
1111
+ if (validationResult || force) {
1112
+ if (!validationResult)
1113
+ process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Ignoring validation errors in ${type.file}\n`));
1114
+ const currentContentType = (await client.contentTypesGet({ path: { key: type.definition.key } }).catch(() => null));
1115
+ function buildError(e) {
1116
+ return { key: type.definition.key, type: type.definition, file: type.file, error: e };
1117
+ }
1118
+ function buildData(ct) {
1119
+ return { key: type.definition.key, type: ct, file: type.file };
1120
+ }
1121
+ return (currentContentType ?
1122
+ client.contentTypesPatch({ path: { key: type.definition.key }, body: outType }) :
1123
+ client.contentTypesCreate({ body: outType })).then(buildData).catch(buildError);
1124
+ }
1125
+ else {
1126
+ return {
1127
+ key: type.definition.key,
1128
+ type: type.definition,
1129
+ file: type.file,
1130
+ error: typeValidator.errors.map(x => x.message).join(', ')
1131
+ };
1132
+ }
734
1133
  }));
735
1134
  const overview = new Table({
736
1135
  head: [
@@ -740,255 +1139,32 @@ const TypesPushCommand = {
740
1139
  chalk.yellow(chalk.bold("Message"))
741
1140
  ],
742
1141
  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
1142
+ colAligns: ["left", "left", "center", "left"]
974
1143
  });
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 }');
1144
+ results.forEach(item => {
1145
+ overview.push([
1146
+ item.type.displayName,
1147
+ item.key,
1148
+ item.error ? chalk.redBright(chalk.bold(figures.cross)) : chalk.green(chalk.bold(figures.tick)),
1149
+ item.error ?? ''
1150
+ ]);
1151
+ });
1152
+ process.stdout.write(overview.toString() + "\n");
1153
+ // Mark us as done
1154
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
983
1155
  }
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
- };
1156
+ };
1157
+
1158
+ const builder = yargs => {
1159
+ const updatedArgs = contentTypesBuilder(yargs);
1160
+ updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
1161
+ return updatedArgs;
1162
+ };
1163
+ function createTypeFolders(contentTypes, basePath, debug = false) {
1164
+ return contentTypes.map(contentType => getContentTypePaths(contentType, basePath, true, debug));
1165
+ }
1166
+ function getTypeFolder(list, type) {
1167
+ return list.filter(x => x.type == type).at(0);
992
1168
  }
993
1169
 
994
1170
  function ucFirst(current) {
@@ -1024,8 +1200,8 @@ const NextJsComponentsCommand = {
1024
1200
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1025
1201
  }
1026
1202
  };
1027
- function createComponent(contentType, typePath, force, debug = false) {
1028
- const componentFile = path.join(typePath, 'index.tsx');
1203
+ function createComponent(contentType, typePathInfo, force, debug = false) {
1204
+ const { componentFile, path: typePath } = typePathInfo;
1029
1205
  if (fs.existsSync(componentFile)) {
1030
1206
  if (!force) {
1031
1207
  if (debug)
@@ -1040,8 +1216,9 @@ function createComponent(contentType, typePath, force, debug = false) {
1040
1216
  // Get type information & short-hands
1041
1217
  const displayTemplate = getDisplayTemplateInfo$1(contentType, typePath);
1042
1218
  const baseDisplayTemplate = getBaseTypeTemplateInfo(contentType, typePath);
1043
- const varName = `${contentType.key}${ucFirst(contentType.baseType ?? 'part')}`;
1044
- const tplFn = Templates[contentType.baseType] ?? Templates['default'];
1219
+ const normalizedBaseType = normalizeBaseType(contentType.baseType ?? 'part');
1220
+ const varName = `${contentType.key}${ucFirst(normalizedBaseType)}`;
1221
+ const tplFn = Templates[normalizedBaseType] ?? Templates['default'];
1045
1222
  if (!tplFn) {
1046
1223
  if (debug)
1047
1224
  process.stdout.write(chalk.redBright(`${figures.cross} Skipping ${contentType.displayName} (${contentType.key}) component - no template for ${contentType.baseType} found\n`));
@@ -1065,6 +1242,11 @@ function getBaseTypeTemplateInfo(contentType, typePath) {
1065
1242
  }
1066
1243
  return undefined;
1067
1244
  }
1245
+ function normalizeBaseType(baseType) {
1246
+ if (baseType.startsWith('_'))
1247
+ return baseType.substring(1);
1248
+ return baseType;
1249
+ }
1068
1250
  function getDisplayTemplateInfo$1(contentType, typePath) {
1069
1251
  const displayTemplatesFile = path.join(typePath, 'displayTemplates.ts');
1070
1252
  if (fs.existsSync(displayTemplatesFile)) {
@@ -1078,24 +1260,50 @@ function getDisplayTemplateInfo$1(contentType, typePath) {
1078
1260
  }
1079
1261
  const Templates = {
1080
1262
  // Default Template for all components without specifics
1081
- default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { type CmsComponent } from "@remkoj/optimizely-cms-react";
1263
+ default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
1082
1264
  import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
1083
1265
  import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
1084
1266
  import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
1085
1267
 
1086
1268
  /**
1087
1269
  * ${contentType.displayName}
1270
+ * ---
1088
1271
  * ${contentType.description}
1089
1272
  */
1090
- export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, children }) => {
1273
+ export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps }) => {
1091
1274
  const componentName = '${contentType.displayName}'
1092
1275
  const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1093
- return <div className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4">
1276
+ return <CmsEditable className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4" {...editProps}>
1094
1277
  <div className="font-bold italic">{ componentName }</div>
1095
1278
  <div>{ componentInfo }</div>
1096
1279
  { 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>
1280
+ </CmsEditable>
1281
+ }
1282
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1283
+ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1284
+
1285
+ export default ${varName}`,
1286
+ // Default Template for all section types
1287
+ section: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
1288
+ import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
1289
+ import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
1290
+ import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
1291
+
1292
+ /**
1293
+ * ${contentType.displayName}
1294
+ * ---
1295
+ * ${contentType.description}
1296
+ */
1297
+ export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps, children }) => {
1298
+ const componentName = '${contentType.displayName}'
1299
+ const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1300
+ return <CmsEditable className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4" {...editProps}>
1301
+ <div className="font-bold italic">{ componentName }</div>
1302
+ <div>{ componentInfo }</div>
1303
+ { 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> }
1304
+ ${(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 */}'}
1305
+ <div>{ children }</div>
1306
+ </CmsEditable>
1099
1307
  }
1100
1308
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1101
1309
  ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
@@ -1103,26 +1311,32 @@ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}
1103
1311
  export default ${varName}`,
1104
1312
  // Template for all page component types
1105
1313
  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 ? `
1314
+ import { get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'${displayTemplate ? `
1107
1315
  import { ${displayTemplate} } from "./displayTemplates";` : ''}
1108
1316
  import { getSdk } from "@/gql"
1109
1317
 
1110
1318
  /**
1111
1319
  * ${contentType.displayName}
1320
+ * ---
1112
1321
  * ${contentType.description}
1322
+ *
1323
+ * This component uses the content query that is auto-generated with the Optimizely CMS Preset for GraphQL Codegen, if you need
1324
+ * to override this query create a GraphQL file (for example: \`get${contentType.key}Data.query.graphql\`) in the same folder as
1325
+ * this file. This file must include a GraphQL query with the name \`get${contentType.key}Data\`.
1326
+ *
1327
+ * [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
1113
1328
  */
1114
- export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, children }) => {
1329
+ export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''} }) => {
1115
1330
  const componentName = '${contentType.displayName}'
1116
1331
  const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1117
1332
  return <div className="mx-auto px-2 container">
1118
1333
  <div className="font-bold italic">{ componentName }</div>
1119
1334
  <div>{ componentInfo }</div>
1120
1335
  { 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
1336
  </div>
1123
1337
  }
1124
1338
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1125
- ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1339
+ ${varName}.getDataQuery = () => get${contentType.key}DataDocument
1126
1340
  ${varName}.getMetaData = async (contentLink, locale, client) => {
1127
1341
  const sdk = getSdk(client);
1128
1342
  // Add your metadata logic here
@@ -1132,24 +1346,36 @@ ${varName}.getMetaData = async (contentLink, locale, client) => {
1132
1346
  export default ${varName}`,
1133
1347
  // Template for all experience component types
1134
1348
  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";
1349
+ import { ExperienceDataFragmentDoc, get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'
1350
+ import { getFragmentData } from '@/gql/fragment-masking'
1137
1351
  import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
1138
1352
  import { ${displayTemplate} } from "./displayTemplates";` : ''}
1139
- import { getSdk } from "@/gql"
1353
+ import { getSdk } from "@/gql/client"
1140
1354
 
1141
1355
  /**
1142
1356
  * ${contentType.displayName}
1357
+ * ---
1143
1358
  * ${contentType.description}
1359
+ *
1360
+ * This component uses the content query that is auto-generated with the Optimizely CMS Preset for GraphQL Codegen, if you need
1361
+ * to override this query create the file \`${contentType.key}.query.graphql\` in the same folder as this file. This file then
1362
+ * must include a GraphQL query with the name \`get${contentType.key} Data\`.
1363
+ *
1364
+ * [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
1144
1365
  */
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>
1366
+ export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, ctx }) => {
1367
+ if (ctx) ctx.editableContentIsExperience = true
1368
+ const composition = getFragmentData(ExperienceDataFragmentDoc, data).composition
1369
+ const componentName = '${contentType.displayName}'
1370
+ const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1371
+ return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData" ctx={ctx}>
1372
+ <div className="font-bold italic">{ componentName }</div>
1373
+ <div>{ componentInfo }</div>
1374
+ { composition && isNode(composition) && <OptimizelyComposition node={composition} ctx={ctx} /> }
1375
+ </CmsEditable>
1150
1376
  }
1151
1377
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1152
- ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1378
+ ${varName}.getDataQuery = () => get${contentType.key}DataDocument
1153
1379
  ${varName}.getMetaData = async (contentLink, locale, client) => {
1154
1380
  const sdk = getSdk(client);
1155
1381
  // Add your metadata logic here
@@ -1167,7 +1393,7 @@ const NextJsVisualBuilderCommand = {
1167
1393
  // Prepare
1168
1394
  const { components: basePath, _config: { debug }, force } = parseArgs(args);
1169
1395
  // Create the fall-back node and style defintions
1170
- await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node'], definitions: true });
1396
+ await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'], definitions: true });
1171
1397
  createGenericNode(basePath, force, debug);
1172
1398
  // Get all styles
1173
1399
  const { styles } = await getStyles(createCmsClient(args), { ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'] });
@@ -1178,7 +1404,8 @@ const NextJsVisualBuilderCommand = {
1178
1404
  });
1179
1405
  // Process base styles
1180
1406
  styles.filter(x => typeof (x.baseType) == 'string' && x.baseType.length > 0).map(styleDefinition => {
1181
- const templatePath = path.join(basePath, styleDefinition.baseType, 'styles', styleDefinition.key);
1407
+ const baseType = (styleDefinition.baseType ?? '').startsWith('_') ? (styleDefinition.baseType ?? '').substring(1) : styleDefinition.baseType;
1408
+ const templatePath = path.join(basePath, baseType, 'styles', styleDefinition.key);
1182
1409
  createSpecificNode(styleDefinition, templatePath, force, debug);
1183
1410
  });
1184
1411
  }
@@ -1198,13 +1425,30 @@ function createSpecificNode(template, templatePath, force = false, debug = false
1198
1425
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${template.displayName} (${template.key}) component\n`));
1199
1426
  if (!fs.existsSync(templatePath))
1200
1427
  fs.mkdirSync(templatePath, { recursive: true });
1428
+ const baseType = template.nodeType ?? template.baseType ?? 'unknown';
1201
1429
  const displayTemplateName = getDisplayTemplateInfo(template, templatePath);
1202
- const component = `import { extractSettings, type CmsLayoutComponent } from "@remkoj/optimizely-cms-react/rsc";${displayTemplateName ? `
1203
- import { ${displayTemplateName} } from "../displayTemplates";` : ''}
1430
+ const imports = ['import { extractSettings, type CmsLayoutComponent } from "@remkoj/optimizely-cms-react/rsc";'];
1431
+ const componentProps = [`className="vb:${baseType} vb:${baseType}:${template.key}"`];
1432
+ const componentArgs = ['layoutProps', 'children'];
1433
+ let componentType = 'div';
1434
+ if (displayTemplateName)
1435
+ imports.push(`import { ${displayTemplateName} } from "../displayTemplates";`);
1436
+ if (baseType.toLowerCase() == 'section') {
1437
+ imports.push('import { CmsEditable } from "@remkoj/optimizely-cms-react/rsc";');
1438
+ componentType = 'CmsEditable';
1439
+ componentProps.push('{ ...editProps }');
1440
+ componentArgs.push('editProps');
1441
+ }
1442
+ let style = '';
1443
+ if (baseType == "row")
1444
+ style = ` style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "stretch" }}`;
1445
+ if (baseType == "column")
1446
+ style = ` style={{ display: "flex", flexDirection: "column", flexGrow: 1 }}`;
1447
+ const component = `${imports.join("\n")}
1204
1448
 
1205
- export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ contentLink, layoutProps, children }) => {
1449
+ export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ ${componentArgs.join(', ')} }) => {
1206
1450
  const layout = extractSettings(layoutProps);
1207
- return (<div className="vb:${template.nodeType} vb:${template.nodeType}:${template.key}">{ children }</div>);
1451
+ return (<${componentType} ${componentProps.join(' ')}${style}>{ children }</${componentType}>);
1208
1452
  }
1209
1453
 
1210
1454
  export default ${template.key};`;
@@ -1224,13 +1468,19 @@ function createGenericNode(basePath, force, debug) {
1224
1468
  else if (debug)
1225
1469
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating generic node component\n`));
1226
1470
  const nodeContent = `import { CmsEditable, type CmsLayoutComponent } from '@remkoj/optimizely-cms-react/rsc'
1471
+ import { type CSSProperties } from 'react';
1227
1472
 
1228
- export const VisualBuilderNode : CmsLayoutComponent = ({ contentLink, layoutProps, children, ctx }) =>
1473
+ export const VisualBuilderNode : CmsLayoutComponent = ({ editProps, layoutProps, children }) =>
1229
1474
  {
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>
1475
+ let className = \`vb:\${layoutProps?.layoutType}\`;
1476
+ let style : CSSProperties | undefined = undefined;
1477
+ if (layoutProps?.layoutType == "row")
1478
+ style = {display: "flex", flexDirection: "row"}
1479
+ if (layoutProps?.layoutType == "column")
1480
+ style = {display: "flex", flexDirection: "column"}
1481
+ if (layoutProps && layoutProps.layoutType == "section")
1482
+ return <CmsEditable as="div" className={ className } {...editProps}>{ children }</CmsEditable>
1483
+ return <div className={ className } style={ style }>{ children }</div>
1234
1484
  }
1235
1485
 
1236
1486
  export default VisualBuilderNode`;
@@ -1478,7 +1728,7 @@ const NextJsCreateCommand = {
1478
1728
  await Promise.all([
1479
1729
  TypesPullCommand.handler(args),
1480
1730
  StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: [], definitions: true }),
1481
- NextJsQueriesCommand.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
1731
+ //createFragments.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
1482
1732
  ]);
1483
1733
  process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared types, styles and fragments") + "\n");
1484
1734
  // Then create the components
@@ -1493,6 +1743,484 @@ const NextJsCreateCommand = {
1493
1743
  }
1494
1744
  };
1495
1745
 
1746
+ const NextJsFragmentsCommand = {
1747
+ command: "nextjs:fragments",
1748
+ describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
1749
+ builder,
1750
+ handler: async (args, opts) => {
1751
+ // Prepare
1752
+ const { loadedContentTypes, createdTypeFolders } = opts || {};
1753
+ const { components: basePath, _config: { debug }, force } = parseArgs(args);
1754
+ const client = createCmsClient(args);
1755
+ const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
1756
+ // Start process
1757
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments\n`));
1758
+ const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
1759
+ const tracker = new Map();
1760
+ const dependencies = [];
1761
+ const updatedTypes = contentTypes.map(contentType => {
1762
+ const { written, propertyTypes } = createComponentFragments(contentType, getTypeFolder(typeFolders, contentType.key), force, debug, tracker);
1763
+ dependencies.push(...propertyTypes);
1764
+ return written ? contentType.key : undefined;
1765
+ }).filter(x => x);
1766
+ // Report outcome
1767
+ if (updatedTypes.length > 0)
1768
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
1769
+ else
1770
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
1771
+ // Start property generation process
1772
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL property fragments\n`));
1773
+ const generatedProps = createPropertyFragments(dependencies, allContentTypes, (contentType) => {
1774
+ if (!contentType.key)
1775
+ return undefined;
1776
+ let tf = getTypeFolder(typeFolders, contentType.key);
1777
+ if (!tf) {
1778
+ tf = getContentTypePaths(contentType, basePath, true, debug);
1779
+ typeFolders.push(tf);
1780
+ }
1781
+ return tf;
1782
+ }, force, debug);
1783
+ // Report outcome
1784
+ if (generatedProps.length > 0)
1785
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
1786
+ else
1787
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL property fragments created/updated\n`));
1788
+ if (!opts)
1789
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1790
+ }
1791
+ };
1792
+ function createComponentFragments(contentType, typePath, force, debug, propertyTracker = new Map()) {
1793
+ const baseQueryFile = typePath.fragmentFile;
1794
+ let writeFragment = true;
1795
+ if (fs.existsSync(baseQueryFile)) {
1796
+ if (force) {
1797
+ if (debug)
1798
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
1799
+ }
1800
+ else {
1801
+ if (debug)
1802
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
1803
+ writeFragment = false;
1804
+ }
1805
+ }
1806
+ else if (debug) {
1807
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
1808
+ }
1809
+ let written = false;
1810
+ if (writeFragment) {
1811
+ const fragment = GraphQLGen.buildFragment(contentType, undefined, false, propertyTracker);
1812
+ fs.writeFileSync(baseQueryFile, fragment);
1813
+ written = true;
1814
+ }
1815
+ return { written, propertyTypes: GraphQLGen.getReferencedPropertyComponents(contentType) };
1816
+ }
1817
+ function createPropertyFragments(propertyTypesList, allContentTypes, selectTypeFolder, force = false, debug = false) {
1818
+ const returnValue = [];
1819
+ for (const propertyContentTypeKey of propertyTypesList.filter((v, i, a) => a.indexOf(v, i + 1) <= i)) {
1820
+ // Load the ContentType definition
1821
+ const contentType = allContentTypes.filter(x => x.key == propertyContentTypeKey).at(0);
1822
+ if (!contentType) {
1823
+ console.warn(`🟠 The content type ${propertyContentTypeKey} has been referenced, but is not found in the Optimizely CMS instance`);
1824
+ continue;
1825
+ }
1826
+ // Load the paths for this ContentType
1827
+ const depFolder = selectTypeFolder(contentType);
1828
+ if (!depFolder) {
1829
+ console.warn(`🟠 The content type ${propertyContentTypeKey} cannot be stored in the project`);
1830
+ continue;
1831
+ }
1832
+ const propertyFragmentFile = depFolder.propertyFragmentFile;
1833
+ let mustWrite = true;
1834
+ if (fs.existsSync(propertyFragmentFile)) {
1835
+ if (force) {
1836
+ if (debug)
1837
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) property fragment\n`));
1838
+ }
1839
+ else {
1840
+ if (debug)
1841
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) property fragment - file already exists\n`));
1842
+ mustWrite = false;
1843
+ }
1844
+ }
1845
+ else if (debug) {
1846
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) property fragment\n`));
1847
+ }
1848
+ if (mustWrite) {
1849
+ const fragment = GraphQLGen.buildFragment(contentType, undefined, true);
1850
+ fs.writeFileSync(propertyFragmentFile, fragment);
1851
+ returnValue.push(contentType.key);
1852
+ }
1853
+ // Recurse down for properties that we're not yet rendering
1854
+ const referencedPropertyTypes = GraphQLGen.getReferencedPropertyComponents(contentType).filter(x => !propertyTypesList.includes(x));
1855
+ if (referencedPropertyTypes.length > 0) {
1856
+ if (debug)
1857
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Component property ${propertyContentTypeKey} uses components a property, recursing down\n`));
1858
+ const additionalProperties = createPropertyFragments(referencedPropertyTypes, allContentTypes, selectTypeFolder, force, debug);
1859
+ returnValue.push(...additionalProperties);
1860
+ }
1861
+ }
1862
+ return returnValue;
1863
+ }
1864
+
1865
+ const NextJsQueriesCommand = {
1866
+ command: "nextjs:queries",
1867
+ describe: "Create the GrapQL Queries to use two queries to load content",
1868
+ builder: yargs => {
1869
+ const updatedArgs = contentTypesBuilder(yargs, { ...ContentTypesArgsDefaults, baseTypes: ['page', 'experience'] });
1870
+ updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
1871
+ return updatedArgs;
1872
+ },
1873
+ handler: async (args, opts) => {
1874
+ // Prepare
1875
+ const { loadedContentTypes, createdTypeFolders } = opts || {};
1876
+ const { components: basePath, _config: { debug }, force } = parseArgs(args);
1877
+ const client = createCmsClient(args);
1878
+ const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
1879
+ // Start process
1880
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL queries\n`));
1881
+ const dependencies = [];
1882
+ const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
1883
+ const updatedTypes = contentTypes.map(contentType => {
1884
+ const typePath = getTypeFolder(typeFolders, contentType.key);
1885
+ const { written, propertyTypes } = createGraphQueries(contentType, typePath, force, debug);
1886
+ dependencies.push(...propertyTypes);
1887
+ return written ? contentType.key : undefined;
1888
+ }).filter(x => x).flat();
1889
+ // Report outcome
1890
+ if (updatedTypes.length > 0)
1891
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL queries for ${updatedTypes.join(', ')}\n`));
1892
+ else
1893
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL queries created/updated\n`));
1894
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL property fragments\n`));
1895
+ const generatedProps = createPropertyFragments(dependencies, allContentTypes, (contentType) => {
1896
+ if (!contentType.key)
1897
+ return undefined;
1898
+ let tf = getTypeFolder(typeFolders, contentType.key);
1899
+ if (!tf) {
1900
+ tf = getContentTypePaths(contentType, basePath, true, debug);
1901
+ typeFolders.push(tf);
1902
+ }
1903
+ return tf;
1904
+ }, force, debug);
1905
+ // Report outcome
1906
+ if (generatedProps.length > 0)
1907
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
1908
+ else
1909
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL property fragments created/updated\n`));
1910
+ if (!opts)
1911
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1912
+ }
1913
+ };
1914
+ function createGraphQueries(contentType, typePath, force, debug) {
1915
+ const baseQueryFile = typePath.queryFile;
1916
+ let mustWrite = true;
1917
+ if (fs.existsSync(baseQueryFile)) {
1918
+ if (force) {
1919
+ if (debug)
1920
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
1921
+ }
1922
+ else {
1923
+ if (debug)
1924
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
1925
+ mustWrite = false;
1926
+ }
1927
+ }
1928
+ else if (debug) {
1929
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
1930
+ }
1931
+ if (mustWrite) {
1932
+ const fragment = GraphQLGen.buildGetQuery(contentType, undefined, new Map());
1933
+ fs.writeFileSync(baseQueryFile, fragment);
1934
+ }
1935
+ return {
1936
+ written: mustWrite,
1937
+ propertyTypes: GraphQLGen.getReferencedPropertyComponents(contentType)
1938
+ };
1939
+ }
1940
+
1941
+ const SchemaDownloadCommand = {
1942
+ command: "schema:download",
1943
+ describe: "Create JSON schema files for the specified types, which you can use to configure JSON validation in your IDE of choice.",
1944
+ builder: (yargs) => {
1945
+ const newYargs = yargs;
1946
+ newYargs.option('schemaDir', { alias: 'd', description: "The schema path relative to your project root directory", string: true, type: 'string', demandOption: false, default: './.schema' });
1947
+ newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] });
1948
+ newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
1949
+ return newYargs;
1950
+ },
1951
+ async handler(args, opts) {
1952
+ const { _config: cmsConfig, path: projectPath, force, schemaDir, schemas } = parseArgs(args);
1953
+ const client = createCmsClient(cmsConfig);
1954
+ // Loading Schema
1955
+ const schemaDefs = await loadSchema(client, schemas);
1956
+ const fullSchemaDir = path.normalize(path.join(projectPath, schemaDir));
1957
+ const relativeSchemaDir = path.relative(projectPath, fullSchemaDir);
1958
+ process.stdout.write(`\n${figures.arrowRight} Validating schema folder ${relativeSchemaDir}\n`);
1959
+ const schemaDirInfo = await fsAsync.stat(fullSchemaDir).catch((e) => { if (e.code == 'ENOENT')
1960
+ return undefined;
1961
+ else
1962
+ throw e; });
1963
+ if (!schemaDirInfo) {
1964
+ await fsAsync.mkdir(fullSchemaDir, { recursive: true });
1965
+ }
1966
+ else if (!schemaDirInfo.isDirectory()) {
1967
+ process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] The schema directory exists, but is not a directory (${relativeSchemaDir})\n`)));
1968
+ process.exit(1);
1969
+ }
1970
+ process.stdout.write(chalk.greenBright(` ${figures.tick} `));
1971
+ process.stdout.write(`Schema directory exists\n`);
1972
+ process.stdout.write(`\n${figures.arrowRight} Writing schema files\n`);
1973
+ for (const schema of schemaDefs) {
1974
+ const schemaFile = path.normalize(path.join(fullSchemaDir, `${schema.title.toLowerCase()}.schema.json`));
1975
+ const relativePath = path.relative(projectPath, schemaFile);
1976
+ try {
1977
+ await fsAsync.writeFile(schemaFile, JSON.stringify(schema.schema, undefined, 2), {
1978
+ encoding: "utf-8",
1979
+ flag: force ? 'w' : 'wx'
1980
+ });
1981
+ process.stdout.write(chalk.greenBright(` ${figures.tick} `));
1982
+ process.stdout.write(`Written the ${schema.title} schema to ${relativePath}\n`);
1983
+ }
1984
+ catch (err) {
1985
+ if (!force && err.code === 'EEXIST') {
1986
+ process.stdout.write(chalk.yellowBright(` ${figures.info} `));
1987
+ process.stdout.write(`The schema ${schema.title} already exists in ${relativePath}, run with -f to overwrite\n`);
1988
+ }
1989
+ else
1990
+ process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] ${err.toString()}\n`)));
1991
+ }
1992
+ }
1993
+ }
1994
+ };
1995
+
1996
+ const SchemaListCommand = {
1997
+ command: "schema:list",
1998
+ describe: "List all schema's that are available within the SaaS CMS instance",
1999
+ builder: (yargs) => {
2000
+ const newYargs = yargs;
2001
+ return newYargs;
2002
+ },
2003
+ async handler(args, opts) {
2004
+ const client = createCmsClient(args);
2005
+ process.stdout.write(`\n${figures.arrowRight} Downloading OpenAPI Specification\n`);
2006
+ const spec = await client.getOpenApiSpec();
2007
+ process.stdout.write(chalk.greenBright(` ${figures.tick} `));
2008
+ process.stdout.write(`The ${spec.info.title} (Version: ${spec.info.version}) specification has been downloaded.\n`);
2009
+ const specSchemas = (spec?.components?.schemas ?? {});
2010
+ const schemas = new Table({
2011
+ head: [
2012
+ chalk.yellow(chalk.bold("Key")),
2013
+ chalk.yellow(chalk.bold("Description"))
2014
+ ],
2015
+ colWidths: [35, 60],
2016
+ colAligns: ["left", "left"],
2017
+ wordWrap: true,
2018
+ wrapOnWordBoundary: true
2019
+ });
2020
+ for (const key in specSchemas)
2021
+ if (key) {
2022
+ schemas.push([key, specSchemas[key].description?.replaceAll(/\s+/g, ' ') ?? `-`]);
2023
+ }
2024
+ process.stdout.write("\n" + schemas.toString() + "\n");
2025
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
2026
+ }
2027
+ };
2028
+
2029
+ const deepmerge = createDeepMerge();
2030
+ const SchemaValidateCommand = {
2031
+ command: "schema:validate",
2032
+ describe: "Validate the opti-type.json & opti-style.json files",
2033
+ builder: (yargs) => {
2034
+ const newYargs = yargs;
2035
+ //newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] })
2036
+ //newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false })
2037
+ //newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true })
2038
+ return newYargs;
2039
+ },
2040
+ handler: async (args) => {
2041
+ const projectPath = args.path;
2042
+ const client = createCmsClient(args);
2043
+ const schemas = await loadSchema(client, ['DisplayTemplate', 'ContentType']);
2044
+ process.stdout.write(``);
2045
+ const styleSchema = schemas.find(x => x.title == 'DisplayTemplate')?.schema;
2046
+ const typeSchema = schemas.find(x => x.title == 'ContentType')?.schema;
2047
+ const [styleValidator, typeValidator] = await Promise.all([getValidator(styleSchema), getValidator(typeSchema)]);
2048
+ process.stdout.write(`\n${figures.arrowRight} Validating display templates (*.opti-style.json) and content types (*.opti-type.json).\n`);
2049
+ const patterns = ["./**/*.opti-type.json", "./**/*.opti-style.json"];
2050
+ const globMatches = globIterate(patterns, { cwd: projectPath, absolute: true });
2051
+ let allOk = true;
2052
+ for await (const fileMatch of globMatches) {
2053
+ try {
2054
+ const content = JSON.parse(fs.readFileSync(fileMatch).toString());
2055
+ let errors = undefined;
2056
+ if (fileMatch.endsWith('.opti-type.json') && !typeValidator(content)) {
2057
+ errors = typeValidator.errors;
2058
+ }
2059
+ else if (fileMatch.endsWith('.opti-style.json') && !styleValidator(content)) {
2060
+ errors = styleValidator.errors;
2061
+ }
2062
+ if (errors) {
2063
+ allOk = false;
2064
+ process.stdout.write(chalk.redBright(chalk.bold(`\n${figures.cross} ${path.relative(projectPath, fileMatch)}\n`)));
2065
+ const groupedErrors = groupErrors(errors);
2066
+ for (const instancepath in groupedErrors) {
2067
+ for (const key in groupedErrors[instancepath]) {
2068
+ const errorList = groupedErrors[instancepath][key].filter((v, i, a) => a.findIndex(pv => deepEqual(v, pv)) === i);
2069
+ for (const error of errorList) {
2070
+ process.stdout.write(` Node ${instancepath} ${error.message}\n`);
2071
+ switch (error.keyword) {
2072
+ case 'enum':
2073
+ process.stdout.write(` Allowed values: ${error.params.allowedValues.map((x) => x.toString()).join(', ')}\n`);
2074
+ break;
2075
+ case 'oneOf':
2076
+ const matching = Array.isArray(error.params.passingSchemas) ? error.params.passingSchemas : [];
2077
+ if (matching.length === 0)
2078
+ process.stdout.write(` Currently none matching\n`);
2079
+ else
2080
+ process.stdout.write(` Currently matching ${matching.map((x) => x.toString()).join(", ")}\n`);
2081
+ break;
2082
+ }
2083
+ }
2084
+ }
2085
+ }
2086
+ }
2087
+ }
2088
+ catch (e) {
2089
+ allOk = false;
2090
+ process.stdout.write(chalk.redBright(chalk.bold(`${figures.cross} ${path.relative(projectPath, fileMatch)}\n`)));
2091
+ process.stdout.write(` ${e.message}\n`);
2092
+ }
2093
+ }
2094
+ process.stdout.write(`\n`);
2095
+ if (allOk)
2096
+ process.stdout.write(chalk.greenBright(`${figures.tick} All types & styles match the schema\n`));
2097
+ else {
2098
+ process.stdout.write(chalk.redBright(`${figures.cross} Some types & styles contain errors\n`));
2099
+ process.exit(1);
2100
+ }
2101
+ }
2102
+ };
2103
+ function groupErrors(errors) {
2104
+ const errorrsByInstance = groupErrorsByPath(errors);
2105
+ const instancePaths = Object.getOwnPropertyNames(errorrsByInstance);
2106
+ return instancePaths.reduce((previous, current) => {
2107
+ previous[current] = groupErrorsByKeyword(errorrsByInstance[current]);
2108
+ return previous;
2109
+ }, {});
2110
+ }
2111
+ function groupErrorsByPath(errors) {
2112
+ return errors.reduce((previous, current) => {
2113
+ const instancePath = current.instancePath == "" ? "[ROOT]" : current.instancePath;
2114
+ const partial = {};
2115
+ partial[instancePath] = [current];
2116
+ return deepmerge(previous, partial);
2117
+ }, {});
2118
+ }
2119
+ function groupErrorsByKeyword(errors) {
2120
+ const toMerge = errors.reduce((previous, current) => {
2121
+ const keyword = current.keyword;
2122
+ const partial = {};
2123
+ partial[keyword] = [current];
2124
+ return deepmerge(previous, partial);
2125
+ }, {});
2126
+ // Enum errors must be merged
2127
+ if (toMerge.enum)
2128
+ toMerge.enum = [toMerge.enum.reduce((prev, current) => prev ? deepmerge(prev, current) : current, undefined)];
2129
+ return toMerge;
2130
+ }
2131
+
2132
+ const SchemaVsCodeCommand = {
2133
+ command: "schema:vscode",
2134
+ describe: "Configure schema validation for opti-type.json & opti-style.json files by VSCode in the current project folder",
2135
+ builder: (yargs) => {
2136
+ const newYargs = yargs;
2137
+ //newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] })
2138
+ //newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false })
2139
+ //newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true })
2140
+ return newYargs;
2141
+ },
2142
+ handler: async (args) => {
2143
+ const projectPath = args.path;
2144
+ const client = createCmsClient(args);
2145
+ const schemas = await loadSchema(client, ['DisplayTemplate', 'ContentType']);
2146
+ process.stdout.write(`\n${figures.arrowRight} Writing schema files\n`);
2147
+ for (const schema of schemas) {
2148
+ const schemaFile = path.join(projectPath, '.vscode', `${schema.title.toLowerCase()}.schema.json`);
2149
+ void await writeFileAsync(schemaFile, JSON.stringify(schema.schema, undefined, 2));
2150
+ process.stdout.write(` ${figures.tick} Written the ${schema.title} schema to ${path.relative(projectPath, schemaFile)}\n`);
2151
+ }
2152
+ process.stdout.write(`\n${figures.arrowRight} Updating folder settings\n`);
2153
+ const settingsFile = path.join(projectPath, '.vscode', 'settings.json');
2154
+ const settings = (await readJsonFileAsync(settingsFile).catch(() => undefined)) ?? {};
2155
+ settings['json.validate.enable'] = true;
2156
+ settings['json.schemaDownload.enable'] = true;
2157
+ settings['json.schemas'] = settings['json.schemas'] || [];
2158
+ const hasContentType = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.opti-type.json')) >= 0) >= 0;
2159
+ const hasStyleDefinition = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.opti-style.json')) >= 0) >= 0;
2160
+ const hasSchemaType = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.schema.json')) >= 0) >= 0;
2161
+ if (!hasContentType)
2162
+ settings['json.schemas'].push({
2163
+ "fileMatch": [
2164
+ "**/*.opti-type.json"
2165
+ ],
2166
+ "url": "./.vscode/contenttype.schema.json"
2167
+ });
2168
+ if (!hasStyleDefinition)
2169
+ settings['json.schemas'].push({
2170
+ "fileMatch": [
2171
+ "**/*.opti-style.json"
2172
+ ],
2173
+ "url": "./.vscode/displaytemplate.schema.json"
2174
+ });
2175
+ if (!hasSchemaType)
2176
+ settings['json.schemas'].push({
2177
+ "fileMatch": [
2178
+ "**/*.schema.json"
2179
+ ],
2180
+ "url": "https://json-schema.org/draft-07/schema"
2181
+ });
2182
+ if (settings['json.schemas'].length == 0)
2183
+ delete settings['json.schemas'];
2184
+ await writeFileAsync(settingsFile, JSON.stringify(settings, undefined, 2));
2185
+ }
2186
+ };
2187
+ function readJsonFileAsync(path) {
2188
+ return new Promise((resolve, reject) => fs.readFile(path, (err, data) => {
2189
+ if (err) {
2190
+ if (err?.code === 'ENOENT')
2191
+ resolve(undefined);
2192
+ else
2193
+ reject(err);
2194
+ }
2195
+ else {
2196
+ try {
2197
+ const d = data.toString();
2198
+ resolve(JSON.parse(d));
2199
+ }
2200
+ catch (e) {
2201
+ reject(e);
2202
+ }
2203
+ }
2204
+ }));
2205
+ }
2206
+ function writeFileAsync(path, data) {
2207
+ return new Promise((resolve, reject) => {
2208
+ fs.writeFile(path, data, (err) => {
2209
+ if (err)
2210
+ reject(err);
2211
+ else
2212
+ resolve();
2213
+ });
2214
+ });
2215
+ }
2216
+
2217
+ var version = "6.0.0-pre11";
2218
+ var name = "opti-cms";
2219
+ var APP = {
2220
+ version: version,
2221
+ name: name
2222
+ };
2223
+
1496
2224
  const CmsVersionCommand = {
1497
2225
  command: "cms:version",
1498
2226
  aliases: "$0",
@@ -1511,19 +2239,25 @@ const CmsVersionCommand = {
1511
2239
  throw error;
1512
2240
  }
1513
2241
  });
2242
+ const hasPreview2Info = versionInfo.results.preview2Data?.baseUrl ? true : false;
1514
2243
  const info = new Table({
1515
2244
  head: [
1516
2245
  chalk.yellow(chalk.bold("Component")),
1517
2246
  chalk.yellow(chalk.bold("Version"))
1518
2247
  ],
1519
- colWidths: [20, 60],
2248
+ colWidths: [30, 60],
1520
2249
  colAligns: ["left", "left"]
1521
2250
  });
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]);
2251
+ info.push(["Base URL", versionInfo.baseUrl ?? client.cmsUrl.href]);
2252
+ if (hasPreview2Info)
2253
+ info.push(["Base URL (Preview 2)", versionInfo.results.preview2Data?.baseUrl ?? 'n/a']);
2254
+ info.push(["Client API", client.apiVersion]);
2255
+ info.push(["Service API", versionInfo.apiVersion]);
2256
+ if (hasPreview2Info)
2257
+ info.push(["Service API (Preview 2)", versionInfo.results.preview2Data?.apiVersion ?? 'n/a']);
2258
+ info.push(["CMS Build", versionInfo.cmsVersion]);
2259
+ info.push(["Service Build", versionInfo.serviceVersion]);
2260
+ info.push(["SDK", APP.version]);
1527
2261
  process.stdout.write(info.toString() + "\n");
1528
2262
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Optimizely CMS Status: ${versionInfo.status}\n`));
1529
2263
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
@@ -1643,7 +2377,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
1643
2377
  process.stderr.write(chalk.redBright(` ${figures.cross}The ContentItem ${key} is a reserved item, skipping deletion\n`));
1644
2378
  return removedCount;
1645
2379
  }
1646
- const deleteResult = await client.contentDelete({ path: { key }, query: { permanent: true } }).then(r => r.key).catch((e) => {
2380
+ const deleteResult = await client.preview2ContentDelete({ path: { key }, query: { permanent: true } }).then(r => r.key).catch((e) => {
1647
2381
  if (e.status == 404)
1648
2382
  return key;
1649
2383
  throw e;
@@ -1669,8 +2403,7 @@ async function resetSystemTypes(client) {
1669
2403
  body: {
1670
2404
  key: systemType,
1671
2405
  properties: {}
1672
- },
1673
- query: { ignoreDataLossWarnings: true }
2406
+ }
1674
2407
  }).catch((e) => e.status == 404 ? undefined : null);
1675
2408
  if (newType)
1676
2409
  resetTypes++;
@@ -1771,7 +2504,7 @@ async function getAllTypes(client, batchSize = 100) {
1771
2504
  return actualItems;
1772
2505
  }
1773
2506
  async function getAllAssets(client, parentKey, batchSize = 100) {
1774
- const items = await client.contentListAssets({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
2507
+ const items = await client.preview2ContentListAssets({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
1775
2508
  if (e.status == 404) {
1776
2509
  return {
1777
2510
  items: [],
@@ -1786,7 +2519,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
1786
2519
  const actualItems = items.items ?? [];
1787
2520
  const pageCount = Math.ceil(totalItemCount / pageSize);
1788
2521
  for (let pageNr = 1; pageNr < pageCount; pageNr++) {
1789
- const pageData = await client.contentListAssets({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
2522
+ const pageData = await client.preview2ContentListAssets({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
1790
2523
  if (e.status == 404) {
1791
2524
  return {
1792
2525
  items: [],
@@ -1801,7 +2534,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
1801
2534
  return actualItems;
1802
2535
  }
1803
2536
  async function getAllItems(client, parentKey, batchSize = 100) {
1804
- const items = await client.contentListItems({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
2537
+ const items = await client.preview2ContentListItems({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
1805
2538
  if (e.status == 404) {
1806
2539
  return {
1807
2540
  items: [],
@@ -1816,7 +2549,7 @@ async function getAllItems(client, parentKey, batchSize = 100) {
1816
2549
  const actualItems = items.items ?? [];
1817
2550
  const pageCount = Math.ceil(totalItemCount / pageSize);
1818
2551
  for (let pageNr = 1; pageNr < pageCount; pageNr++) {
1819
- const pageData = await client.contentListItems({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
2552
+ const pageData = await client.preview2ContentListItems({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
1820
2553
  if (e.status == 404) {
1821
2554
  return {
1822
2555
  items: [],
@@ -1833,28 +2566,26 @@ async function getAllItems(client, parentKey, batchSize = 100) {
1833
2566
 
1834
2567
  // Style processing
1835
2568
  const commands = [
2569
+ CmsResetCommand,
2570
+ CmsVersionCommand,
2571
+ NextJsComponentsCommand,
2572
+ NextJsCreateCommand,
2573
+ NextJsFactoryCommand,
2574
+ NextJsFragmentsCommand,
2575
+ NextJsQueriesCommand,
2576
+ NextJsVisualBuilderCommand,
2577
+ SchemaDownloadCommand,
2578
+ SchemaListCommand,
2579
+ SchemaValidateCommand,
2580
+ SchemaVsCodeCommand,
1836
2581
  StylesCreateCommand,
1837
- StylesPushCommand,
1838
2582
  StylesListCommand,
1839
2583
  StylesPullCommand,
2584
+ StylesPushCommand,
1840
2585
  TypesPullCommand,
1841
- TypesPushCommand,
1842
- NextJsCreateCommand,
1843
- NextJsQueriesCommand,
1844
- NextJsComponentsCommand,
1845
- NextJsVisualBuilderCommand,
1846
- NextJsFactoryCommand,
1847
- CmsResetCommand,
1848
- CmsVersionCommand
2586
+ TypesPushCommand
1849
2587
  ];
1850
2588
 
1851
- var version = "6.0.0-pre1";
1852
- var name = "opti-cms";
1853
- var APP = {
1854
- version: version,
1855
- name: name
1856
- };
1857
-
1858
2589
  async function main() {
1859
2590
  const envFiles = prepare();
1860
2591
  const app = createOptiCmsApp(APP.name, APP.version, undefined, envFiles);
@@ -1864,7 +2595,7 @@ async function main() {
1864
2595
  }
1865
2596
  catch {
1866
2597
  //We're ignoring error here, as yargs will already generate the "nice output" for it
1867
- //console.log ('Caught error')
2598
+ //console.log('Caught error')
1868
2599
  }
1869
2600
  }
1870
2601
  main();