@remkoj/optimizely-cms-cli 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import fs from 'node:fs';
8
8
  import chalk from 'chalk';
9
9
  import figures from 'figures';
10
10
  import Table from 'cli-table3';
11
+ import { input, select, confirm } from '@inquirer/prompts';
11
12
 
12
13
  /**
13
14
  * Prepare the application context, by parsing the .env files in the main
@@ -22,10 +23,18 @@ function prepare() {
22
23
  }
23
24
 
24
25
  function createOptiCmsApp(scriptName, version, epilogue) {
25
- const config = getCmsIntegrationApiConfigFromEnvironment();
26
+ let config;
27
+ try {
28
+ config = getCmsIntegrationApiConfigFromEnvironment();
29
+ }
30
+ catch {
31
+ config = {
32
+ base: new URL('https://example.cms.optimizely.com')
33
+ };
34
+ }
26
35
  return yargs(process.argv)
27
36
  .scriptName(scriptName)
28
- .version(version )
37
+ .version(version)
29
38
  .usage('$0 <cmd> [args]')
30
39
  .option("path", { alias: "p", description: "Application root folder", string: true, type: "string", demandOption: false, default: process.cwd() })
31
40
  .option("components", { alias: "c", description: "Path to components folder", string: true, type: "string", demandOption: false, default: "./src/components/cms" })
@@ -55,11 +64,20 @@ function isDemanded(value) {
55
64
  * @param param0 The parameters from the Command Line application
56
65
  * @returns The arguments, with the Optimizely CMS Client parameters transformed into a configuration object
57
66
  */
58
- function parseArgs({ client_id, client_secret, cms_url, user_id, verbose, path: argsPath, components: argsComponents, ...args }) {
67
+ function parseArgs({ client_id, client_secret, cms_url, user_id, verbose, path: argsPath, components: argsComponents, ...args }, createBasePath = true) {
59
68
  const appPath = path.isAbsolute(argsPath) ? argsPath : path.normalize(path.join(process.cwd(), argsPath));
60
69
  const componentDir = path.normalize(path.join(argsPath, argsComponents));
61
70
  if (!componentDir.startsWith(argsPath))
62
71
  throw new Error(`The component directory ${componentDir} is outside the application directory (${appPath})`);
72
+ if (createBasePath && !fs.existsSync(componentDir)) {
73
+ if (verbose)
74
+ process.stdout.write(chalk.gray(`${figures.arrowRight} The components directory ${componentDir} does not exist yet, it will be created\n`));
75
+ fs.mkdirSync(componentDir, { recursive: true });
76
+ }
77
+ if (!fs.statSync(componentDir).isDirectory()) {
78
+ process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The components path ${componentDir} exists, but is not a folder\n`));
79
+ process.exit(1);
80
+ }
63
81
  return {
64
82
  _config: {
65
83
  base: cms_url,
@@ -143,26 +161,167 @@ function isNotNullOrUndefined(i) {
143
161
  return i ? true : false;
144
162
  }
145
163
 
164
+ function createCmsClient(args) {
165
+ const { _config: cfg } = parseArgs(args);
166
+ const client = createClient(cfg);
167
+ if (cfg.debug)
168
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Connecting to ${cfg.base.href} as ${cfg.actAs ?? cfg.clientId}\n`));
169
+ return client;
170
+ }
171
+
172
+ const contentTypesBuilder = yargs => {
173
+ yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
174
+ yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
175
+ yargs.option("baseTypes", { alias: 'b', description: "Select only these base types", string: true, type: 'array', demandOption: false, default: [] });
176
+ yargs.option("types", { alias: 't', description: "Select only these types", string: true, type: 'array', demandOption: false, default: [] });
177
+ return yargs;
178
+ };
179
+ async function getContentTypes(client, args, pageSize = 100, allowSystem = false) {
180
+ const { _config: cfg, excludeBaseTypes, excludeTypes, baseTypes, types } = parseArgs(args);
181
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
182
+ if (cfg.debug)
183
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
184
+ let resultsPage = await client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
185
+ const results = resultsPage.items ?? [];
186
+ let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
187
+ while (pagesRemaining > 0 && results.length < resultsPage.totalItemCount) {
188
+ if (cfg.debug)
189
+ 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`));
190
+ resultsPage = await client.contentTypes.contentTypesList(undefined, undefined, resultsPage.pageIndex + 1, resultsPage.pageSize);
191
+ results.push(...resultsPage.items);
192
+ pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
193
+ }
194
+ if (cfg.debug) {
195
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
196
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Content-Types based upon arguments\n`));
197
+ }
198
+ const contentTypes = results.filter(data => {
199
+ // Remove items based upon filters
200
+ const keepType = (!excludeBaseTypes.includes(data.baseType)) &&
201
+ (!excludeTypes.includes(data.key)) &&
202
+ (baseTypes.length == 0 || baseTypes.includes(data.baseType)) &&
203
+ (types.length == 0 || types.includes(data.key));
204
+ if (!keepType) {
205
+ if (cfg.debug)
206
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${data.key} due to applied filters\n`));
207
+ return false;
208
+ }
209
+ // Skip system types if desired
210
+ if (data.source == 'system' && !allowSystem) {
211
+ if (cfg.debug)
212
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${data.key} due to it being a system type\n`));
213
+ return false;
214
+ }
215
+ return true;
216
+ });
217
+ if (cfg.debug)
218
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${contentTypes.length} items\n`));
219
+ return {
220
+ all: results,
221
+ contentTypes
222
+ };
223
+ }
224
+
225
+ const stylesBuilder = yargs => {
226
+ const newArgs = contentTypesBuilder(yargs);
227
+ newArgs.option('excludeNodeTypes', { alias: 'ent', description: "Exclude these node types", string: true, type: 'array', demandOption: false, default: [] });
228
+ newArgs.option('excludeTemplates', { alias: 'et', description: "Exclude these templates", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
229
+ newArgs.option("nodes", { alias: 'n', description: "Select only these node types", string: true, type: 'array', demandOption: false, default: [] });
230
+ newArgs.option("templates", { alias: 'd', description: "Select only these templates", string: true, type: 'array', demandOption: false, default: [] });
231
+ newArgs.option("templateTypes", { alias: 'tt', description: "Select only these template types", choices: ['node', 'base', 'component'], type: 'array', demandOption: false, default: [] });
232
+ return newArgs;
233
+ };
234
+ async function getStyles(client, args, pageSize = 100) {
235
+ const { _config: cfg, excludeBaseTypes, excludeTypes, excludeNodeTypes, excludeTemplates, baseTypes, types, nodes, templates, templateTypes } = parseArgs(args);
236
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Style-Definitions from Optimizely CMS\n`));
237
+ if (cfg.debug)
238
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
239
+ let resultsPage = await client.displayTemplates.displayTemplatesList(0, pageSize);
240
+ const results = resultsPage.items ?? [];
241
+ let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
242
+ while (pagesRemaining > 0 && results.length < resultsPage.totalItemCount) {
243
+ if (cfg.debug)
244
+ 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`));
245
+ resultsPage = await client.displayTemplates.displayTemplatesList(resultsPage.pageIndex + 1, resultsPage.pageSize);
246
+ results.push(...resultsPage.items);
247
+ pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
248
+ }
249
+ if (cfg.debug) {
250
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Style-Definitions from Optimizely CMS\n`));
251
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Style-Definitions based upon arguments\n`));
252
+ }
253
+ const styles = results.filter(data => {
254
+ if (isExcluded(data.key, excludeTemplates, templates)) {
255
+ if (cfg.debug)
256
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style defintion key filtering active\n`));
257
+ return false;
258
+ }
259
+ const templateType = data.baseType ? 'base' : data.nodeType ? 'node' : data.contentType ? 'component' : 'unknown';
260
+ if (isExcluded(templateType, [], templateTypes)) {
261
+ if (cfg.debug)
262
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style type filtering is active\n`));
263
+ return false;
264
+ }
265
+ if (data.baseType && isExcluded(data.baseType, excludeBaseTypes, baseTypes)) {
266
+ if (cfg.debug)
267
+ 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`));
268
+ return false;
269
+ }
270
+ if (data.contentType && isExcluded(data.contentType, excludeTypes, types)) {
271
+ if (cfg.debug)
272
+ 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`));
273
+ return false;
274
+ }
275
+ if (data.nodeType && isExcluded(data.nodeType, excludeNodeTypes, nodes)) {
276
+ if (cfg.debug)
277
+ 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`));
278
+ return false;
279
+ }
280
+ return true;
281
+ });
282
+ if (cfg.debug)
283
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${styles.length} items\n`));
284
+ return {
285
+ all: results,
286
+ styles
287
+ };
288
+ }
289
+ function isExcluded(value, exclusions, inclusions) {
290
+ if (value == undefined || value == null)
291
+ return false;
292
+ return exclusions.includes(value) || (inclusions.length > 0 && !inclusions.includes(value));
293
+ }
294
+ async function getStyleFilePath(definition, opts) {
295
+ if (definition.nodeType)
296
+ return `nodes/${definition.nodeType}/${definition.key}/${definition.key}.opti-style.json`;
297
+ if (definition.baseType)
298
+ return `${definition.baseType}/styles/${definition.key}/${definition.key}.opti-style.json`;
299
+ if (definition.contentType) {
300
+ if (opts?.contentBaseType)
301
+ return `${opts?.contentBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
302
+ if (!opts?.client)
303
+ throw new Error("Neither the contentBaseType, nor the ApiClient has been provided for a definition for a specific ContentType - unable to generate the path");
304
+ const pageSize = 50;
305
+ let resultsPage = await opts.client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
306
+ const contentTypes = resultsPage.items ?? [];
307
+ let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
308
+ while (pagesRemaining > 0 && contentTypes.length < resultsPage.totalItemCount) {
309
+ resultsPage = await opts.client.contentTypes.contentTypesList(undefined, undefined, resultsPage.pageIndex + 1, resultsPage.pageSize);
310
+ contentTypes.push(...resultsPage.items);
311
+ pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
312
+ }
313
+ const fetchedBaseType = contentTypes.filter(x => x.key == definition.contentType).map(x => x.baseType).at(0);
314
+ if (fetchedBaseType)
315
+ return `${fetchedBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
316
+ }
317
+ throw new Error(`Unable to resolve the target for the DisplayTemplate: ${definition.key}`);
318
+ }
319
+
146
320
  const StylesListCommand = {
147
321
  command: "styles:list",
148
322
  describe: "List Visual Builder style definitions from the CMS",
149
323
  handler: async (args) => {
150
- const { _config: cfg, ...opts } = parseArgs(args);
151
- const client = createClient(cfg);
152
- const pageSize = 100;
153
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Reading DisplayStyles from Optimizely CMS\n`));
154
- if (cfg.debug)
155
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
156
- let templatesPage = await client.displayTemplates.displayTemplatesList(0, pageSize);
157
- const results = templatesPage.items ?? [];
158
- let pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
159
- while (pagesRemaining > 0 && results.length < templatesPage.totalItemCount) {
160
- if (cfg.debug)
161
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${templatesPage.pageIndex + 2} of ${Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize)} (${templatesPage.pageSize} items per page)\n`));
162
- templatesPage = await client.displayTemplates.displayTemplatesList(templatesPage.pageIndex + 1, templatesPage.pageSize);
163
- results.push(...templatesPage.items);
164
- pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
165
- }
324
+ const { all: results } = await getStyles(createCmsClient(args), { ...args, excludeBaseTypes: [], excludeNodeTypes: [], excludeTemplates: [], excludeTypes: [], baseTypes: [], nodes: [], templates: [], types: [], templateTypes: [] });
166
325
  const styles = new Table({
167
326
  head: [
168
327
  chalk.yellow(chalk.bold("Name")),
@@ -190,50 +349,15 @@ const StylesPullCommand = {
190
349
  command: "styles:pull",
191
350
  describe: "Create Visual Builder style definitions from the CMS",
192
351
  builder: (yargs) => {
193
- yargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
194
- yargs.option("excludeTemplates", { alias: 'e', description: "Exclude these templates", string: true, type: 'array', demandOption: false, default: [] });
195
- yargs.option("templates", { alias: 't', description: "Select only these templates", string: true, type: 'array', demandOption: false, default: [] });
196
- yargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: false });
197
- return yargs;
352
+ const newYargs = stylesBuilder(yargs);
353
+ newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
354
+ newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true });
355
+ return newYargs;
198
356
  },
199
357
  handler: async (args) => {
200
- const { _config: cfg, components: basePath, force, definitions, excludeTemplates, templates } = parseArgs(args);
201
- const client = createClient(cfg);
202
- const pageSize = 100;
203
- //#region Load all templates from Optimizely CMS
204
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Reading DisplayStyles from Optimizely CMS\n`));
205
- if (cfg.debug)
206
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
207
- let templatesPage = await client.displayTemplates.displayTemplatesList(0, pageSize);
208
- const results = templatesPage.items ?? [];
209
- let pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
210
- while (pagesRemaining > 0 && results.length < templatesPage.totalItemCount) {
211
- if (cfg.debug)
212
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${templatesPage.pageIndex + 2} of ${Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize)} (${templatesPage.pageSize} items per page)\n`));
213
- templatesPage = await client.displayTemplates.displayTemplatesList(templatesPage.pageIndex + 1, templatesPage.pageSize);
214
- results.push(...templatesPage.items);
215
- pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
216
- }
217
- if (cfg.debug)
218
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
219
- //#endregion
220
- //#region Ensure target base path exists
221
- if (!fs.existsSync(basePath))
222
- fs.mkdirSync(basePath, { recursive: true });
223
- if (!fs.statSync(basePath).isDirectory()) {
224
- process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The components path ${basePath} is not a folder\n`));
225
- process.exit(1);
226
- }
227
- //#endregion
228
- //#region Apply template filters
229
- const filteredResults = results.filter(result => {
230
- if (excludeTemplates.includes(result.key))
231
- return false;
232
- if (templates.length > 0 && !templates.includes(result.key))
233
- return false;
234
- return true;
235
- });
236
- //#endregion
358
+ const { _config: cfg, components: basePath, force, definitions } = parseArgs(args);
359
+ const client = createCmsClient(args);
360
+ const { styles: filteredResults } = await getStyles(client, args);
237
361
  //#region Create & Write opti-style.json files
238
362
  const typeFiles = {};
239
363
  const updatedTemplates = (await Promise.all(filteredResults.map(async (displayTemplate) => {
@@ -241,8 +365,8 @@ const StylesPullCommand = {
241
365
  let targetType;
242
366
  let typesPath;
243
367
  if (displayTemplate.nodeType) {
244
- itemPath = path.join(basePath, 'styles', displayTemplate.nodeType, displayTemplate.key);
245
- typesPath = path.join(basePath, 'styles', displayTemplate.nodeType);
368
+ itemPath = path.join(basePath, 'nodes', displayTemplate.nodeType, displayTemplate.key);
369
+ typesPath = path.join(basePath, 'nodes', displayTemplate.nodeType);
246
370
  targetType = 'node/' + displayTemplate.nodeType;
247
371
  }
248
372
  else if (displayTemplate.baseType) {
@@ -260,6 +384,17 @@ const StylesPullCommand = {
260
384
  fs.mkdirSync(itemPath, { recursive: true });
261
385
  // Write Style JSON
262
386
  const filePath = path.join(itemPath, `${displayTemplate.key}.opti-style.json`);
387
+ if (fs.existsSync(filePath)) {
388
+ if (!force) {
389
+ if (cfg.debug)
390
+ process.stdout.write(chalk.gray(`${figures.cross} Skipping style file for ${displayTemplate.key} - File already exists\n`));
391
+ return;
392
+ }
393
+ if (cfg.debug)
394
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting style file for ${displayTemplate.key}\n`));
395
+ }
396
+ else if (cfg.debug)
397
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating style file for ${displayTemplate.key}\n`));
263
398
  fs.writeFileSync(filePath, JSON.stringify(displayTemplate, undefined, 2));
264
399
  if (!typeFiles[targetType]) {
265
400
  typeFiles[targetType] = {
@@ -269,17 +404,23 @@ const StylesPullCommand = {
269
404
  }
270
405
  typeFiles[targetType].templates.push({ file: filePath, data: displayTemplate });
271
406
  return displayTemplate.key;
272
- })));
407
+ }))).filter(x => x);
273
408
  //#endregion
274
409
  //#region Create needed definition files
275
410
  if (definitions) {
276
411
  for (const targetId of Object.getOwnPropertyNames(typeFiles)) {
277
412
  const { filePath: typeFilePath, templates } = typeFiles[targetId];
278
- if (fs.existsSync(typeFilePath) && !force) {
413
+ if (fs.existsSync(typeFilePath)) {
414
+ if (!force) {
415
+ if (cfg.debug)
416
+ process.stdout.write(chalk.gray(`${figures.cross} Skipped writing definition file for ${targetId} - it already exists\n`));
417
+ continue;
418
+ }
279
419
  if (cfg.debug)
280
- process.stdout.write(chalk.gray(`${figures.cross} Skipped writing definition file ${typeFilePath} as it already exists\n`));
281
- continue;
420
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting definition file for ${targetId}\n`));
282
421
  }
422
+ else if (cfg.debug)
423
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating definition file for ${targetId}\n`));
283
424
  // Write Style definition
284
425
  const imports = ['import type { LayoutProps } from "@remkoj/optimizely-cms-react/components"', 'import type { ReactNode } from "react"'];
285
426
  const typeContents = [];
@@ -314,8 +455,7 @@ export type ${typeId}Component<DT extends Record<string, any> = Record<string, a
314
455
  }
315
456
  }
316
457
  //#endregion
317
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated style definitions for ${updatedTemplates.join(', ')}\n`));
318
- process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
458
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + ` Created/updated style definitions for ${updatedTemplates.join(', ')}`)) + "\n");
319
459
  }
320
460
  };
321
461
  function ucFirst$1(input) {
@@ -324,63 +464,110 @@ function ucFirst$1(input) {
324
464
  return input[0].toUpperCase() + input.substring(1);
325
465
  }
326
466
 
467
+ // Node JS and 3rd Party libraries
468
+ const StylesCreateCommand = {
469
+ command: "style:create",
470
+ describe: "Create a new style definition",
471
+ builder: yargs => {
472
+ return yargs;
473
+ },
474
+ handler: async (args) => {
475
+ const { components: basePath } = parseArgs(args);
476
+ const client = createCmsClient(args);
477
+ const allowedBaseTypes = ['section', 'element'];
478
+ // Prepare
479
+ process.stdout.write(chalk.yellowBright(chalk.bold(`Reading current information from Optimizely CMS\n`)));
480
+ const [{ contentTypes }, { styles }] = await Promise.all([
481
+ getContentTypes(client, { ...args, excludeBaseTypes: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [] }),
482
+ getStyles(client, { ...args, excludeBaseTypes: [], excludeNodeTypes: [], excludeTemplates: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [], nodes: [], components: '', templates: [], templateTypes: [] })
483
+ ]);
484
+ const styleKeys = styles.map(x => x.key);
485
+ // Define the configuration
486
+ process.stdout.write(chalk.yellowBright(chalk.bold(`\nConfigure your style definition\n`)));
487
+ const key = await input({ message: "Identifier:", validate: (val) => { return val.match(/^[a-zA-Z][A-Za-z0-9\-_]*$/) != null && !styleKeys.includes(val); } });
488
+ const displayName = await input({ message: "Display name:" });
489
+ const type = (await select({ message: "Style target:", choices: [
490
+ { value: "baseType", name: "Base Type", description: "Target all Content-Types that inherit from the specified base type" },
491
+ { value: "contentType", name: "Content-Type", description: "Target a specific Content-Type that supports styling" },
492
+ { value: "nodeType", name: "Experience Node", description: "Target a specific node type from a Section" }
493
+ ] }));
494
+ let typeId = "";
495
+ switch (type) {
496
+ case 'contentType':
497
+ typeId = await select({ message: "Content-Type:", choices: contentTypes.map(x => { return { value: x.key, description: x.description, name: x.displayName }; }) });
498
+ break;
499
+ case 'nodeType':
500
+ typeId = await select({ message: "Node type:", choices: [
501
+ { value: "row", description: "Row", name: "Row" },
502
+ { value: "column", description: "Column", name: "Column" }
503
+ ] });
504
+ break;
505
+ case 'baseType':
506
+ typeId = await select({ message: "Base type:", choices: [
507
+ { value: "section", description: "Target all section types", name: "Section" },
508
+ { value: "element", description: "Target all element types", name: "Element" }
509
+ ] });
510
+ break;
511
+ }
512
+ const isDefault = await confirm({ message: "Should this style be marked as default?" });
513
+ //const commitToCms = await confirm({ message: "Do you want to upload this style immediately to the CMS?"})
514
+ const definition = { key, displayName, isDefault };
515
+ definition[type] = typeId;
516
+ definition['settings'] = {};
517
+ const contentBaseType = type == "contentType" ? contentTypes.filter(x => x.key == typeId).map(x => x.baseType).at(0) : undefined;
518
+ const styleFilePath = await getStyleFilePath(definition, { contentBaseType, client });
519
+ if (!fs.existsSync(path.join(basePath, path.dirname(styleFilePath))))
520
+ fs.mkdirSync(path.join(basePath, path.dirname(styleFilePath)), { recursive: true });
521
+ if (fs.existsSync(path.join(basePath, styleFilePath))) {
522
+ const overwrite = await confirm({ message: "The style definition file already exists, do you want to overwrite it?" });
523
+ if (!overwrite) {
524
+ process.stdout.write("\n");
525
+ process.stdout.write(chalk.redBright(chalk.bold(figures.cross + " Aborted")) + "\n");
526
+ process.exit(0);
527
+ }
528
+ }
529
+ fs.writeFileSync(path.join(basePath, styleFilePath), JSON.stringify(definition, undefined, 4));
530
+ process.stdout.write(chalk.yellowBright(chalk.bold(`\nWritten style defintion template to: ${path.normalize(path.join(basePath, styleFilePath))}\n`)));
531
+ process.stdout.write(chalk.yellowBright(`\n1. Add your properties to the 'settings' list in the file`));
532
+ process.stdout.write(chalk.yellowBright(`\n2. Run `));
533
+ process.stdout.write(chalk.whiteBright(`yarn opti-cms styles:push -t ${definition.key}`));
534
+ process.stdout.write(chalk.yellowBright(` to upload the Display Template to Optimizely CMS`));
535
+ process.stdout.write(chalk.yellowBright(`\n3. Run `));
536
+ process.stdout.write(chalk.whiteBright(`yarn opti-cms styles:pull -d ${definition.key}`));
537
+ process.stdout.write(chalk.yellowBright(` to fully create the TypeScript definitions for this Display Template\n`));
538
+ process.stdout.write("\n");
539
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
540
+ }
541
+ };
542
+
327
543
  const TypesPullCommand = {
328
544
  command: "types:pull",
329
545
  describe: "Pull content type definition files into the project",
330
546
  builder: (yargs) => {
547
+ const newArgs = contentTypesBuilder(yargs);
331
548
  yargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
332
- yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
333
- yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
334
- yargs.option("baseTypes", { alias: 'b', description: "Select only these base types", string: true, type: 'array', demandOption: false, default: [] });
335
- yargs.option("types", { alias: 't', description: "Select only these types", string: true, type: 'array', demandOption: false, default: [] });
336
- return yargs;
549
+ return newArgs;
337
550
  },
338
551
  handler: async (args) => {
339
- const { _config: cfg, components: basePath, excludeBaseTypes, excludeTypes, baseTypes, types, force } = parseArgs(args);
340
- const client = createClient(cfg);
341
- const pageSize = 100;
342
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
343
- if (cfg.debug)
344
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
345
- let templatesPage = await client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
346
- const results = templatesPage.items ?? [];
347
- let pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
348
- while (pagesRemaining > 0 && results.length < templatesPage.totalItemCount) {
349
- if (cfg.debug)
350
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${templatesPage.pageIndex + 2} of ${Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize)} (${templatesPage.pageSize} items per page)\n`));
351
- templatesPage = await client.contentTypes.contentTypesList(undefined, undefined, templatesPage.pageIndex + 1, templatesPage.pageSize);
352
- results.push(...templatesPage.items);
353
- pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
354
- }
355
- if (cfg.debug)
356
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
357
- if (!fs.existsSync(basePath))
358
- fs.mkdirSync(basePath, { recursive: true });
359
- if (!fs.statSync(basePath).isDirectory()) {
360
- process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The components path ${basePath} is not a folder\n`));
361
- process.exit(1);
362
- }
363
- const updatedTypes = results.filter(data => {
364
- return (!excludeBaseTypes.includes(data.baseType)) &&
365
- (!excludeTypes.includes(data.key)) &&
366
- (!baseTypes || baseTypes.length == 0 || baseTypes.includes(data.baseType)) &&
367
- (!types || types.length == 0 || types.includes(data.key));
368
- }).map(contentType => {
552
+ const { _config: { debug }, components: basePath, force } = parseArgs(args);
553
+ const client = createCmsClient(args);
554
+ const { contentTypes } = await getContentTypes(client, args);
555
+ const updatedTypes = contentTypes.map(contentType => {
369
556
  const typePath = path.join(basePath, contentType.baseType, contentType.key);
370
557
  const typeFile = path.join(typePath, `${contentType.key}.opti-type.json`);
371
558
  if (!fs.existsSync(typePath))
372
559
  fs.mkdirSync(typePath, { recursive: true });
373
560
  if (fs.existsSync(typeFile) && !force) {
374
- process.stdout.write(chalk.yellow(`${figures.cross} Skipping type definition for ${contentType.displayName} (${contentType.key}) - File already exists\n`));
561
+ if (debug)
562
+ process.stdout.write(chalk.yellow(`${figures.cross} Skipping type definition for ${contentType.displayName} (${contentType.key}) - File already exists\n`));
375
563
  return contentType.key;
376
564
  }
377
- if (cfg.debug)
565
+ if (debug)
378
566
  process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key})\n`));
379
567
  fs.writeFileSync(typeFile, JSON.stringify(contentType, undefined, 2));
380
568
  return contentType.key;
381
569
  }).filter(x => x);
382
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated type definitions for ${updatedTypes.join(', ')}\n`));
383
- process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
570
+ process.stdout.write(chalk.green(chalk.bold(`${figures.tick} Created/updated type definitions for ${updatedTypes.join(', ')}\n`)));
384
571
  }
385
572
  };
386
573
 
@@ -444,209 +631,81 @@ const TypesPushCommand = {
444
631
  }
445
632
  };
446
633
 
447
- const NextJsCreateCommand = {
448
- command: "nextjs:create",
449
- describe: "Scaffold a complete Next.JS / Optimizely Graph structure",
450
- builder: (yargs) => {
451
- yargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
452
- yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
453
- yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
454
- return yargs;
455
- },
456
- handler: async (args) => {
457
- const { _config: cfg, components: basePath, excludeBaseTypes, excludeTypes, force } = parseArgs(args);
458
- const client = createClient(cfg);
459
- const pageSize = 100;
460
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
461
- if (cfg.debug)
462
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
463
- let templatesPage = await client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
464
- const results = templatesPage.items ?? [];
465
- let pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
466
- while (pagesRemaining > 0 && results.length < templatesPage.totalItemCount) {
467
- if (cfg.debug)
468
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${templatesPage.pageIndex + 2} of ${Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize)} (${templatesPage.pageSize} items per page)\n`));
469
- templatesPage = await client.contentTypes.contentTypesList(undefined, undefined, templatesPage.pageIndex + 1, templatesPage.pageSize);
470
- results.push(...templatesPage.items);
471
- pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
472
- }
473
- if (cfg.debug)
474
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
475
- if (!fs.existsSync(basePath))
476
- fs.mkdirSync(basePath, { recursive: true });
477
- if (!fs.statSync(basePath).isDirectory()) {
478
- process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The components path ${basePath} is not a folder\n`));
479
- process.exit(1);
480
- }
481
- // Apply content type filters
482
- if (cfg.debug)
483
- process.stdout.write(chalk.gray(`${figures.arrowRight} Applying content type filters\n`));
484
- const contentTypes = results.filter(contentType => {
485
- if (contentType.source == 'system') {
486
- if (cfg.debug)
487
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) - Internal CMS type\n`));
488
- return false;
489
- }
490
- const baseType = contentType.baseType ?? 'default';
491
- if (excludeBaseTypes.includes(baseType)) {
492
- if (cfg.debug)
493
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) - Base type excluded\n`));
494
- return false;
495
- }
496
- if (excludeTypes.includes(contentType.key)) {
497
- if (cfg.debug)
498
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) - Content type excluded\n`));
499
- return false;
500
- }
501
- return true;
502
- });
503
- if (cfg.debug)
504
- process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${contentTypes.length} items\n`));
505
- // Create the type specific
506
- const updatedTypes = contentTypes.map(contentType => {
507
- const baseType = contentType.baseType ?? 'default';
508
- // Create the type folder
509
- const typePath = path.join(basePath, baseType, contentType.key);
510
- if (!fs.existsSync(typePath))
511
- fs.mkdirSync(typePath, { recursive: true });
512
- // Create fragments
513
- createGraphFragments(contentType, typePath, basePath, force, cfg, contentTypes);
514
- // Create component
515
- createComponent(contentType, typePath, force, cfg);
516
- return contentType.key;
517
- }).filter(x => x);
518
- createFactory(contentTypes, basePath, force, cfg);
519
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated type definitions for ${updatedTypes.join(', ')}\n`));
520
- process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
521
- }
634
+ const builder = yargs => {
635
+ const updatedArgs = contentTypesBuilder(yargs);
636
+ updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
637
+ return updatedArgs;
522
638
  };
523
- function createFactory(contentTypes, basePath, force, cfg) {
524
- // Get the list of all base types
525
- const baseTypes = contentTypes.map(x => x.baseType).concat(['default']).filter((x, i, a) => x && !a.slice(0, i).includes(x)).sort();
526
- const baseTypeFactories = baseTypes.map(baseType => {
527
- // Get the list of inheriting types and return if it's empty
528
- const inheritingTypes = contentTypes.filter(x => x.baseType == baseType);
529
- if (inheritingTypes.length == 0)
530
- return;
531
- if (cfg.debug)
532
- process.stdout.write(chalk.gray(`${figures.arrowRight} Factory for ${ucFirst(baseType)} will contain these components: ${inheritingTypes.map(x => x.displayName ?? x.key).join(", ")}\n`));
533
- // Check the file presence and if we should overwrite
534
- const baseTypeFactory = path.join(basePath, baseType, 'index.ts');
535
- if (fs.existsSync(baseTypeFactory)) {
536
- if (cfg.debug)
537
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${ucFirst(baseType)} factory\n`));
639
+ function createTypeFolders(contentTypes, basePath, debug = false) {
640
+ const folders = contentTypes.map(contentType => {
641
+ const baseType = contentType.baseType ?? 'default';
642
+ // Create the type folder
643
+ const typePath = path.join(basePath, baseType, contentType.key);
644
+ if (!fs.existsSync(typePath)) {
645
+ fs.mkdirSync(typePath, { recursive: true });
646
+ if (debug)
647
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Created folder ${typePath} for Content-Type ${contentType.key}\n`));
538
648
  }
539
- // Build the actual factory
540
- const lines = [
541
- '// Auto generated dictionary',
542
- 'import { ComponentTypeDictionary } from "@remkoj/optimizely-cms-react";'
543
- ];
544
- inheritingTypes.forEach(type => {
545
- lines.push(`import ${type.key} from "./${type.key}";`);
546
- });
547
- lines.push('');
548
- lines.push(`export const ${baseType}Dictionary : ComponentTypeDictionary = [`);
549
- inheritingTypes.forEach(type => {
550
- lines.push(' {', ` type: '${type.key}',`, ` component: ${type.key}`, ' },');
551
- });
552
- lines.push(']', '', `export default ${baseType}Dictionary`);
553
- fs.writeFileSync(baseTypeFactory, lines.join("\n"));
554
- return baseType;
555
- }).filter(x => x);
556
- const cmsFactoryFile = path.join(basePath, 'index.ts');
557
- if (fs.existsSync(cmsFactoryFile)) {
558
- if (cfg.debug)
559
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting CMS Components factory\n`));
560
- }
561
- const lines = [
562
- '// Auto generated dictionary',
563
- 'import { ComponentTypeDictionary } from "@remkoj/optimizely-cms-react";'
564
- ];
565
- baseTypeFactories.forEach(type => {
566
- lines.push(`import ${type}Components from "./${type}";`);
567
- });
568
- lines.push('');
569
- baseTypeFactories.forEach(type => {
570
- lines.push(`prefixDictionaryEntries(${type}Components, '${ucFirst(type)}');`);
571
- if (type == "experience")
572
- lines.push(`prefixDictionaryEntries(${type}Components, 'Page'); // Experiences are a subtype of Page`);
573
- if (type == "element")
574
- lines.push(`prefixDictionaryEntries(${type}Components, 'Component'); // Elements are a subtype of Component`);
575
- });
576
- lines.push('');
577
- lines.push('export const cmsComponentDictionary : ComponentTypeDictionary = [');
578
- baseTypeFactories.forEach(type => {
579
- lines.push(` ...${type}Components,`);
649
+ // Check folders
650
+ if (!fs.statSync(typePath).isDirectory())
651
+ throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
652
+ return {
653
+ type: contentType.key,
654
+ path: typePath,
655
+ };
580
656
  });
581
- lines.push(']', '', `export default cmsComponentDictionary`, `function prefixDictionaryEntries(list: ComponentTypeDictionary, prefix: string) : ComponentTypeDictionary
582
- {
583
- list.forEach((component, idx, dictionary) => {
584
- dictionary[idx].type = typeof component.type == 'string' ? prefix + "/" + component.type : [ prefix, ...component.type ]
585
- })
586
- return list
587
- }`);
588
- fs.writeFileSync(cmsFactoryFile, lines.join("\n"));
657
+ return folders;
589
658
  }
590
- function createComponent(contentType, typePath, force, cfg) {
591
- const componentFile = path.join(typePath, 'index.tsx');
592
- if (fs.existsSync(componentFile)) {
593
- if (force) {
594
- if (cfg.debug)
595
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) component\n`));
596
- }
597
- else {
598
- if (cfg.debug)
599
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) component - file already exists\n`));
600
- return undefined;
601
- }
602
- }
603
- const isPage = contentType.baseType == 'page' || contentType.baseType == 'experience';
604
- const varName = `${contentType.key}${ucFirst(contentType.baseType ?? 'part')}`;
605
- const component = `import ${isPage ? '{ OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs"' : '{ CmsComponent } from "@remkoj/optimizely-cms-react"'};
606
- import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";
607
-
608
- /**
609
- * ${contentType.displayName}
610
- * ${contentType.description}
611
- */
612
- export const ${varName} : CmsComponent<${contentType.key}DataFragment> = ({ data }) => {
613
- const componentName = '${contentType.displayName}'
614
- const componentInfo = '${contentType.description ?? ''}'
615
- return <div className="mx-auto px-2 container">
616
- <div>{ componentName }</div>
617
- <div>{ componentInfo }</div>
618
- <pre className="w-full overflow-x-hidden font-mono text-sm">{ JSON.stringify(data, undefined, 4) }</pre>
619
- </div>
620
- }
621
- ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
622
- ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
623
- ${isPage && `${varName}.getMetaData = async (contentLink) => {
624
- // Add your metadata logic here
625
- return {}
626
- }`.trim()}
627
-
628
- export default ${varName}`;
629
- fs.writeFileSync(componentFile, component);
630
- }
631
- function ucFirst(current) {
632
- return current[0]?.toUpperCase() + current.substring(1);
659
+ function getTypeFolder(list, type) {
660
+ return list.filter(x => x.type == type).at(0)?.path;
633
661
  }
634
- function createGraphFragments(contentType, typePath, basePath, force, cfg, contentTypes) {
662
+
663
+ const NextJsQueriesCommand = {
664
+ command: "nextjs:fragments",
665
+ describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
666
+ builder,
667
+ handler: async (args, opts) => {
668
+ // Prepare
669
+ const { loadedContentTypes, createdTypeFolders } = opts || {};
670
+ const { components: basePath, _config: { debug }, force } = parseArgs(args);
671
+ const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(createCmsClient(args), args);
672
+ // Start process
673
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments for ${contentTypes.map(x => x.key).join(', ')}\n`));
674
+ const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
675
+ const updatedTypes = contentTypes.map(contentType => {
676
+ const typePath = getTypeFolder(typeFolders, contentType.key);
677
+ return createGraphFragments(contentType, typePath, basePath, force, debug, allContentTypes);
678
+ }).filter(x => x).flat();
679
+ // Report outcome
680
+ if (updatedTypes.length > 0)
681
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
682
+ else
683
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
684
+ if (!opts)
685
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
686
+ }
687
+ };
688
+ function createGraphFragments(contentType, typePath, basePath, force, debug, contentTypes) {
689
+ const returnValue = [];
635
690
  const baseType = contentType.baseType ?? 'default';
636
691
  const baseQueryFile = path.join(typePath, `${contentType.key}.${baseType}.graphql`);
637
692
  if (fs.existsSync(baseQueryFile)) {
638
693
  if (force) {
639
- if (cfg.debug)
694
+ if (debug)
640
695
  process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
641
696
  }
642
697
  else {
643
- if (cfg.debug)
698
+ if (debug)
644
699
  process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
645
700
  return undefined;
646
701
  }
647
702
  }
703
+ else if (debug) {
704
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
705
+ }
648
706
  const { fragment, propertyTypes } = createInitialFragment(contentType);
649
707
  fs.writeFileSync(baseQueryFile, fragment);
708
+ returnValue.push(contentType.key);
650
709
  let dependencies = Array.isArray(propertyTypes) ? [...propertyTypes] : [];
651
710
  while (Array.isArray(dependencies) && dependencies.length > 0) {
652
711
  let newDependencies = [];
@@ -661,15 +720,18 @@ function createGraphFragments(contentType, typePath, basePath, force, cfg, conte
661
720
  if (!fs.existsSync(propertyFragmentDir))
662
721
  fs.mkdirSync(propertyFragmentDir, { recursive: true });
663
722
  if (!fs.existsSync(propertyFragmentFile) || force) {
664
- process.stdout.write(` - Writing property fragment: ${propContentType.displayName ?? propContentType.key}\n`);
723
+ if (debug)
724
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Writing ${propContentType.displayName} (${propContentType.key}) property fragment\n`));
665
725
  const propContentTypeInfo = createInitialFragment(propContentType, true);
666
726
  fs.writeFileSync(propertyFragmentFile, propContentTypeInfo.fragment);
727
+ returnValue.push(propContentType.key);
667
728
  if (Array.isArray(propContentTypeInfo.propertyTypes))
668
729
  newDependencies.push(...propContentTypeInfo.propertyTypes);
669
730
  }
670
731
  });
671
732
  dependencies = newDependencies;
672
733
  }
734
+ return returnValue.length > 0 ? returnValue : undefined;
673
735
  }
674
736
  function createInitialFragment(contentType, forProperty = false) {
675
737
  const propertyTypes = [];
@@ -727,6 +789,8 @@ function createInitialFragment(contentType, forProperty = false) {
727
789
  break;
728
790
  }
729
791
  });
792
+ if (contentType.baseType == "experience")
793
+ fragmentFields.push('...ExperienceData');
730
794
  if (fragmentFields.length == 0)
731
795
  fragmentFields.push('_metadata { key }');
732
796
  const tpl = `fragment ${contentType.key}${forProperty ? 'Property' : ''}Data on ${contentType.key}${forProperty ? 'Property' : ''} {
@@ -738,6 +802,499 @@ function createInitialFragment(contentType, forProperty = false) {
738
802
  };
739
803
  }
740
804
 
805
+ function ucFirst(current) {
806
+ if (typeof current != 'string')
807
+ throw new Error("Only strings can be transformed");
808
+ if (current == "")
809
+ return current;
810
+ return current[0]?.toUpperCase() + current.substring(1);
811
+ }
812
+
813
+ const NextJsComponentsCommand = {
814
+ command: "nextjs:components",
815
+ describe: "Create the React Components for a Next.JS / Optimizely Graph structure",
816
+ builder,
817
+ handler: async (args, opts) => {
818
+ // Prepare
819
+ const { loadedContentTypes, createdTypeFolders } = opts || {};
820
+ const { components: basePath, _config: { debug }, force } = parseArgs(args);
821
+ const { contentTypes } = loadedContentTypes ?? await getContentTypes(createCmsClient(args), args);
822
+ // Start process
823
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating React Components for ${contentTypes.map(x => x.key).join(', ')}\n`));
824
+ const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
825
+ const updatedTypes = contentTypes.map(contentType => {
826
+ const typePath = getTypeFolder(typeFolders, contentType.key);
827
+ return createComponent(contentType, typePath, force, debug);
828
+ }).filter(x => x);
829
+ // Report outcome
830
+ if (updatedTypes.length > 0)
831
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated React Components for ${updatedTypes.join(', ')}\n`));
832
+ else
833
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No React Components created/updated\n`));
834
+ if (!opts)
835
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
836
+ }
837
+ };
838
+ function createComponent(contentType, typePath, force, debug = false) {
839
+ const componentFile = path.join(typePath, 'index.tsx');
840
+ if (fs.existsSync(componentFile)) {
841
+ if (!force) {
842
+ if (debug)
843
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) component - file already exists\n`));
844
+ return undefined;
845
+ }
846
+ if (debug)
847
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) component\n`));
848
+ }
849
+ else if (debug)
850
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) component\n`));
851
+ // Get type information & short-hands
852
+ const displayTemplate = getDisplayTemplateInfo$1(contentType, typePath);
853
+ const baseDisplayTemplate = getBaseTypeTemplateInfo(contentType, typePath);
854
+ const varName = `${contentType.key}${ucFirst(contentType.baseType ?? 'part')}`;
855
+ const tplFn = Templates[contentType.baseType] ?? Templates['default'];
856
+ if (!tplFn) {
857
+ if (debug)
858
+ process.stdout.write(chalk.redBright(`${figures.cross} Skipping ${contentType.displayName} (${contentType.key}) component - no template for ${contentType.baseType} found\n`));
859
+ return undefined;
860
+ }
861
+ // Apply template
862
+ fs.writeFileSync(componentFile, tplFn(contentType, varName, displayTemplate, baseDisplayTemplate));
863
+ return contentType.key;
864
+ }
865
+ function getBaseTypeTemplateInfo(contentType, typePath) {
866
+ const displayTemplatesFile = path.join(typePath, '..', 'styles', 'displayTemplates.ts');
867
+ if (fs.existsSync(displayTemplatesFile)) {
868
+ const baseTypeVarName = contentType.baseType ? contentType.baseType[0].toUpperCase() + contentType.baseType.substring(1) : undefined;
869
+ if (!baseTypeVarName)
870
+ return undefined;
871
+ const displayTemplateContent = fs.readFileSync(displayTemplatesFile).toString();
872
+ const displayTemplateName = baseTypeVarName + 'LayoutProps';
873
+ if (displayTemplateContent.includes(`export type ${displayTemplateName} =`)) {
874
+ return displayTemplateName;
875
+ }
876
+ }
877
+ return undefined;
878
+ }
879
+ function getDisplayTemplateInfo$1(contentType, typePath) {
880
+ const displayTemplatesFile = path.join(typePath, 'displayTemplates.ts');
881
+ if (fs.existsSync(displayTemplatesFile)) {
882
+ const displayTemplateContent = fs.readFileSync(displayTemplatesFile).toString();
883
+ const displayTemplateName = contentType.key + 'LayoutProps';
884
+ if (displayTemplateContent.includes(`export type ${displayTemplateName} =`)) {
885
+ return displayTemplateName;
886
+ }
887
+ }
888
+ return undefined;
889
+ }
890
+ const Templates = {
891
+ // Default Template for all components without specifics
892
+ default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsComponent } from "@remkoj/optimizely-cms-react";
893
+ import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
894
+ import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
895
+ import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
896
+
897
+ /**
898
+ * ${contentType.displayName}
899
+ * ${contentType.description}
900
+ */
901
+ export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, children }) => {
902
+ const componentName = '${contentType.displayName}'
903
+ const componentInfo = '${contentType.description ?? ''}'
904
+ return <div className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4">
905
+ <div className="font-bold italic">{ componentName }</div>
906
+ <div>{ componentInfo }</div>
907
+ { 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> }
908
+ { children && <div className="mt-4 mx-4 flex flex-col">{ children }</div>}
909
+ </div>
910
+ }
911
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
912
+ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
913
+
914
+ export default ${varName}`,
915
+ // Template for all page component types
916
+ page: (contentType, varName, displayTemplate) => `import { OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
917
+ import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
918
+ import { ${displayTemplate} } from "./displayTemplates";` : ''}
919
+ import { getSdk } from "@/gql"
920
+
921
+ /**
922
+ * ${contentType.displayName}
923
+ * ${contentType.description}
924
+ */
925
+ export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, children }) => {
926
+ const componentName = '${contentType.displayName}'
927
+ const componentInfo = '${contentType.description ?? ''}'
928
+ return <div className="mx-auto px-2 container">
929
+ <div className="font-bold italic">{ componentName }</div>
930
+ <div>{ componentInfo }</div>
931
+ { 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> }
932
+ { children && <div className="flex flex-col mt-4 mx-4">{ children }</div>}
933
+ </div>
934
+ }
935
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
936
+ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
937
+ ${varName}.getMetaData = async (contentLink, locale, client) => {
938
+ const sdk = getSdk(client);
939
+ // Add your metadata logic here
940
+ return {}
941
+ }
942
+
943
+ export default ${varName}`,
944
+ // Template for all experience component types
945
+ experience: (contentType, varName, displayTemplate) => `import { OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
946
+ import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";
947
+ import { type Maybe, type ICompositionNode, type ExperienceDataFragment } from "@/gql/graphql";
948
+ import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
949
+ import { ${displayTemplate} } from "./displayTemplates";` : ''}
950
+ import { getSdk } from "@/gql"
951
+
952
+ /**
953
+ * ${contentType.displayName}
954
+ * ${contentType.description}
955
+ */
956
+ export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''} }) => {
957
+ const composition = (data as ExperienceDataFragment).composition as Maybe<ICompositionNode>
958
+ return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData">
959
+ { composition && isNode(composition) && <OptimizelyComposition node={composition} /> }
960
+ </CmsEditable>
961
+ }
962
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
963
+ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
964
+ ${varName}.getMetaData = async (contentLink, locale, client) => {
965
+ const sdk = getSdk(client);
966
+ // Add your metadata logic here
967
+ return {}
968
+ }
969
+
970
+ export default ${varName}`
971
+ };
972
+
973
+ const NextJsVisualBuilderCommand = {
974
+ command: "nextjs:visualbuilder",
975
+ describe: "Create the React Components for Visual Builder in a Next.JS / Optimizely Graph structure",
976
+ builder,
977
+ handler: async (args) => {
978
+ // Prepare
979
+ const { components: basePath, _config: { debug }, force } = parseArgs(args);
980
+ // Create the fall-back node and style defintions
981
+ await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node'], definitions: true });
982
+ createGenericNode(basePath, force, debug);
983
+ // Get all styles
984
+ const { styles } = await getStyles(createCmsClient(args), { ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'] });
985
+ // Process node styles
986
+ styles.filter(x => typeof (x.nodeType) == 'string' && x.nodeType.length > 0).map(styleDefintion => {
987
+ const templatePath = path.join(basePath, 'nodes', styleDefintion.nodeType, styleDefintion.key);
988
+ createSpecificNode(styleDefintion, templatePath, force, debug);
989
+ });
990
+ // Process base styles
991
+ styles.filter(x => typeof (x.baseType) == 'string' && x.baseType.length > 0).map(styleDefinition => {
992
+ const templatePath = path.join(basePath, styleDefinition.baseType, 'styles', styleDefinition.key);
993
+ createSpecificNode(styleDefinition, templatePath, force, debug);
994
+ });
995
+ }
996
+ };
997
+ function createSpecificNode(template, templatePath, force = false, debug = false) {
998
+ const nodeFile = path.join(templatePath, 'index.tsx');
999
+ if (fs.existsSync(nodeFile)) {
1000
+ if (!force) {
1001
+ if (debug)
1002
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${template.displayName} (${template.key}) component - file already exists\n`));
1003
+ return undefined;
1004
+ }
1005
+ if (debug)
1006
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${template.displayName} (${template.key}) component\n`));
1007
+ }
1008
+ else if (debug)
1009
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${template.displayName} (${template.key}) component\n`));
1010
+ if (!fs.existsSync(templatePath))
1011
+ fs.mkdirSync(templatePath, { recursive: true });
1012
+ const displayTemplateName = getDisplayTemplateInfo(template, templatePath);
1013
+ const component = `import { type CmsLayoutComponent } from "@remkoj/optimizely-cms-react";
1014
+ import { extractSettings } from "@remkoj/optimizely-cms-react/components";${displayTemplateName ? `
1015
+ import { ${displayTemplateName} } from "../displayTemplates";` : ''}
1016
+
1017
+ export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ contentLink, layoutProps, children }) => {
1018
+ const layout = extractSettings(layoutProps);
1019
+ return (<div className="vb:${template.nodeType} vb:${template.nodeType}:${template.key}">{ children }</div>);
1020
+ }
1021
+
1022
+ export default ${template.key};`;
1023
+ fs.writeFileSync(nodeFile, component);
1024
+ }
1025
+ function createGenericNode(basePath, force, debug) {
1026
+ const nodeItem = path.join(basePath, 'node.tsx');
1027
+ if (fs.existsSync(nodeItem)) {
1028
+ if (!force) {
1029
+ if (debug)
1030
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping generic node component - file already exists\n`));
1031
+ return undefined;
1032
+ }
1033
+ if (debug)
1034
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting generic node component\n`));
1035
+ }
1036
+ else if (debug)
1037
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating generic node component\n`));
1038
+ const nodeContent = `import { type CmsLayoutComponent } from "@remkoj/optimizely-cms-react"
1039
+ import { CmsEditable } from '@remkoj/optimizely-cms-react/rsc'
1040
+
1041
+ export const VisualBuilderNode : CmsLayoutComponent = ({ contentLink, layoutProps, children }) =>
1042
+ {
1043
+ let className = \`vb:\${layoutProps?.layoutType}\`
1044
+ if (layoutProps && layoutProps.layoutType == "section")
1045
+ return <CmsEditable as="div" className={ className } cmsId={ contentLink.key }>{ children }</CmsEditable>
1046
+ return <div className={ className }>{ children }</div>
1047
+ }
1048
+
1049
+ export default VisualBuilderNode`;
1050
+ fs.writeFileSync(nodeItem, nodeContent);
1051
+ }
1052
+ function getDisplayTemplateInfo(template, typePath) {
1053
+ const displayTemplatesFile = path.join(typePath, '..', 'displayTemplates.ts');
1054
+ if (fs.existsSync(displayTemplatesFile)) {
1055
+ const displayTemplateContent = fs.readFileSync(displayTemplatesFile).toString();
1056
+ const displayTemplateName = template.key + 'Props';
1057
+ if (displayTemplateContent.includes(`export type ${displayTemplateName} =`)) {
1058
+ return displayTemplateName;
1059
+ }
1060
+ }
1061
+ return undefined;
1062
+ }
1063
+
1064
+ const NextJsFactoryCommand = {
1065
+ command: "nextjs:factory",
1066
+ describe: "Create the ComponentFactory for a Next.JS / Optimizely Graph structure",
1067
+ builder,
1068
+ handler: async (args) => {
1069
+ const { components: basePath, force, _config: { debug } } = parseArgs(args);
1070
+ if (debug)
1071
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Start generating component factories\n`));
1072
+ const baseDirEntries = fs.readdirSync(basePath);
1073
+ const entries = baseDirEntries
1074
+ .filter(entry => fs.statSync(path.join(basePath, entry)).isDirectory())
1075
+ .map(entry => entry == "nodes" ? processSubGroupFolder(entry, path.join(basePath, entry), force, debug) : processTypeListFolder(entry, path.join(basePath, entry), force, debug))
1076
+ .filter(x => x);
1077
+ // Post process the entries
1078
+ if (debug)
1079
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Post processing resolved top-level factory entries\n`));
1080
+ entries.forEach((e, i) => {
1081
+ if (isImportInfoList(e) && typeof e.prefix == 'string')
1082
+ switch (e.prefix) {
1083
+ case "Experience":
1084
+ entries[i].prefix = [e.prefix, "Page"];
1085
+ break;
1086
+ case "Element":
1087
+ entries[i].prefix = [e.prefix, "Component"];
1088
+ break;
1089
+ }
1090
+ });
1091
+ // Add any global components
1092
+ if (debug)
1093
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Adding global components\n`));
1094
+ baseDirEntries.filter(x => x.endsWith('.tsx') && fs.statSync(path.join(basePath, x)).isFile()).forEach(entry => {
1095
+ const entryName = entry.substring(0, entry.length - 4).replaceAll('.', '').split('-').map(p => ucFirst(p)).join('');
1096
+ const importPath = './' + entry.substring(0, entry.length - 4);
1097
+ entries.push({
1098
+ isList: false,
1099
+ component: (entryName[0].toLowerCase() + entryName.substring(1)) + "Component",
1100
+ path: importPath,
1101
+ type: entryName
1102
+ });
1103
+ });
1104
+ // We're always overwriting the main factory
1105
+ if (debug)
1106
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating/overwriting main factory\n`));
1107
+ const mainFactoryFile = path.join(basePath, "index.ts");
1108
+ fs.writeFileSync(mainFactoryFile, generateFactory(entries, "cms"));
1109
+ process.stdout.write(chalk.yellow(`${figures.arrowRight} Generated main factory in: ${mainFactoryFile}.\n`));
1110
+ }
1111
+ };
1112
+ function processSubGroupFolder(groupName, groupTypePath, force = false, debug = false) {
1113
+ const subgroupInfo = fs
1114
+ .readdirSync(groupTypePath)
1115
+ .filter(entry => {
1116
+ return fs.statSync(path.join(groupTypePath, entry)).isDirectory();
1117
+ }).map(entry => processTypeListFolder(entry, path.join(groupTypePath, entry), force, debug))
1118
+ .filter(x => x);
1119
+ if (subgroupInfo.length == 0)
1120
+ return null;
1121
+ const baseTypeIndex = path.join(groupTypePath, "index.ts");
1122
+ /* Check file existence and determine if we can continue */
1123
+ if (debug)
1124
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Identified base type ${groupName}\n`));
1125
+ if (fs.existsSync(baseTypeIndex)) {
1126
+ if (!force) {
1127
+ if (debug)
1128
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping factory for ${groupName} - file already exists\n`));
1129
+ return {
1130
+ isList: true,
1131
+ component: groupName + 'Dictionary',
1132
+ prefix: groupName[0].toUpperCase() + groupName.substring(1),
1133
+ path: './' + groupName
1134
+ };
1135
+ }
1136
+ if (debug)
1137
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting factory for ${groupName}\n`));
1138
+ }
1139
+ else if (debug)
1140
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Generating new factory for ${groupName}\n`));
1141
+ fs.writeFileSync(baseTypeIndex, generateFactory(subgroupInfo, groupName));
1142
+ process.stdout.write(chalk.yellow(`${figures.arrowRight} Generated partial factory in: ${baseTypeIndex}.\n`));
1143
+ return {
1144
+ isList: true,
1145
+ component: groupName + 'Dictionary',
1146
+ prefix: groupName[0].toUpperCase() + groupName.substring(1),
1147
+ path: './' + groupName
1148
+ };
1149
+ }
1150
+ function isImportInfoList(toTest) {
1151
+ return toTest.isList == true;
1152
+ }
1153
+ function isPrefixedImportInfoList(toTest) {
1154
+ return isImportInfoList(toTest) && ((typeof toTest.prefix == 'string' && toTest.prefix.length > 0) || (Array.isArray(toTest.prefix) && toTest.prefix.length > 0 && toTest.prefix.every(x => typeof x == 'string' && x.length > 0)));
1155
+ }
1156
+ function processTypeListFolder(baseType, baseTypePath, force = false, debug = false) {
1157
+ const baseTypeIndex = path.join(baseTypePath, "index.ts");
1158
+ /* Check file existence and determine if we can continue */
1159
+ if (debug)
1160
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Identified base type ${baseType}\n`));
1161
+ if (fs.existsSync(baseTypeIndex)) {
1162
+ if (!force) {
1163
+ if (debug)
1164
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping factory for ${baseType} - file already exists\n`));
1165
+ return {
1166
+ isList: true,
1167
+ component: baseType + 'Dictionary',
1168
+ prefix: baseType[0].toUpperCase() + baseType.substring(1),
1169
+ path: './' + baseType
1170
+ };
1171
+ }
1172
+ if (debug)
1173
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting factory for ${baseType}\n`));
1174
+ }
1175
+ else if (debug)
1176
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Generating new factory for ${baseType}\n`));
1177
+ // Get the components to import
1178
+ let hasStyles = false;
1179
+ const components = fs
1180
+ .readdirSync(baseTypePath)
1181
+ .filter(entry => {
1182
+ if (entry == 'styles') {
1183
+ hasStyles = true;
1184
+ return false;
1185
+ }
1186
+ return fs.statSync(path.join(baseTypePath, entry)).isDirectory() &&
1187
+ fs.existsSync(path.join(baseTypePath, entry, "index.tsx"));
1188
+ })
1189
+ .map(entry => {
1190
+ return {
1191
+ component: entry + 'Component',
1192
+ path: './' + entry,
1193
+ type: entry
1194
+ };
1195
+ });
1196
+ // Handle styles folder
1197
+ if (hasStyles) {
1198
+ const styles = processTypeListFolder("styles", path.join(baseTypePath, 'styles'), force, debug);
1199
+ if (styles) {
1200
+ components.push({
1201
+ path: "./styles",
1202
+ component: "styleDictionary",
1203
+ isList: true,
1204
+ prefix: "Styles"
1205
+ });
1206
+ }
1207
+ }
1208
+ components.sort((a, b) => {
1209
+ if (isImportInfoList(a) && !isImportInfoList(b))
1210
+ return -1;
1211
+ if (!isImportInfoList(a) && isImportInfoList(b))
1212
+ return 1;
1213
+ return 0;
1214
+ });
1215
+ if (components.length == 0) {
1216
+ if (debug)
1217
+ process.stdout.write(chalk.gray(`${figures.arrowRight} The base type ${baseType} has no components.\n`));
1218
+ return null;
1219
+ }
1220
+ if (debug)
1221
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Generating factory for ${baseType} with the components: ${components.map(x => x.isList == true ? x.component : x.type).join(", ")}.\n`));
1222
+ // Create the file
1223
+ const factoryContent = generateFactory(components, baseType);
1224
+ fs.writeFileSync(baseTypeIndex, factoryContent);
1225
+ process.stdout.write(chalk.yellow(`${figures.arrowRight} Generated partial factory in: ${baseTypeIndex}.\n`));
1226
+ // Build the result
1227
+ return {
1228
+ isList: true,
1229
+ component: baseType + 'Dictionary',
1230
+ prefix: baseType[0].toUpperCase() + baseType.substring(1),
1231
+ path: './' + baseType
1232
+ };
1233
+ }
1234
+ function generateFactory(components, typeName) {
1235
+ const needsPrefixFunction = components.some(isPrefixedImportInfoList);
1236
+ const factoryContent = `// Auto generated dictionary
1237
+ import { ComponentTypeDictionary } from "@remkoj/optimizely-cms-react";
1238
+ ${components.map(x => `import ${x.component} from "${x.path}";`).join("\n")}
1239
+
1240
+ ${needsPrefixFunction ? `// Prefix entries - if needed
1241
+ ${components.filter(isPrefixedImportInfoList).map(x => Array.isArray(x.prefix) ?
1242
+ x.prefix.map(z => `prefixDictionaryEntries(${x.component}, "${z}");`).join("\n") :
1243
+ `prefixDictionaryEntries(${x.component}, "${x.prefix}");`).join("\n")}
1244
+
1245
+ ` : ''}// Build dictionary
1246
+ export const ${typeName}Dictionary : ComponentTypeDictionary = [
1247
+ ${components.map(x => x.isList == true ? `...${x.component}` : `{
1248
+ type: "${x.type}",
1249
+ component: ${x.component}
1250
+ }`).join(",\n ")}
1251
+ ];
1252
+
1253
+ // Export dictionary
1254
+ export default ${typeName}Dictionary;${needsPrefixFunction ? `
1255
+
1256
+ // Helper functions
1257
+ function prefixDictionaryEntries(list: ComponentTypeDictionary, prefix: string) : ComponentTypeDictionary
1258
+ {
1259
+ list.forEach((component, idx, dictionary) => {
1260
+ dictionary[idx].type = typeof component.type == 'string' ? prefix + "/" + component.type : [ prefix, ...component.type ]
1261
+ });
1262
+ return list;
1263
+ }` : ''}
1264
+ `;
1265
+ return factoryContent;
1266
+ }
1267
+
1268
+ const NextJsCreateCommand = {
1269
+ command: "nextjs:create",
1270
+ describe: "Scaffold a complete Next.JS / Optimizely Graph structure",
1271
+ builder,
1272
+ handler: async (args) => {
1273
+ const { _config: cfg, components: basePath } = parseArgs(args);
1274
+ const client = createCmsClient(args);
1275
+ const getContentTypesResult = await getContentTypes(client, args);
1276
+ const { contentTypes } = getContentTypesResult;
1277
+ const folders = createTypeFolders(contentTypes, basePath, cfg.debug);
1278
+ process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared context, to limit duplicate work") + "\n");
1279
+ // First create the base files in parallel
1280
+ await Promise.all([
1281
+ TypesPullCommand.handler(args),
1282
+ StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: [], definitions: true }),
1283
+ NextJsQueriesCommand.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
1284
+ ]);
1285
+ process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared types, styles and fragments") + "\n");
1286
+ // Then create the components
1287
+ await NextJsComponentsCommand.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders });
1288
+ await NextJsVisualBuilderCommand.handler(args);
1289
+ process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Created components") + "\n");
1290
+ // Finally create the factories
1291
+ await NextJsFactoryCommand.handler(args);
1292
+ process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Created component factory") + "\n");
1293
+ process.stdout.write("\n");
1294
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Scaffolded a complete Next.JS / Optimizely Graph structure")) + "\n");
1295
+ }
1296
+ };
1297
+
741
1298
  const CmsVersionCommand = {
742
1299
  command: "cms:version",
743
1300
  describe: "Get the CMS Version information",
@@ -766,16 +1323,21 @@ const CmsVersionCommand = {
766
1323
 
767
1324
  // Style processing
768
1325
  const commands = [
1326
+ StylesCreateCommand,
769
1327
  StylesPushCommand,
770
1328
  StylesListCommand,
771
1329
  StylesPullCommand,
772
1330
  TypesPullCommand,
773
1331
  TypesPushCommand,
774
1332
  NextJsCreateCommand,
1333
+ NextJsQueriesCommand,
1334
+ NextJsComponentsCommand,
1335
+ NextJsVisualBuilderCommand,
1336
+ NextJsFactoryCommand,
775
1337
  CmsVersionCommand
776
1338
  ];
777
1339
 
778
- var version = "2.0.0";
1340
+ var version = "2.0.1";
779
1341
  var name = "opti-cms";
780
1342
  var APP = {
781
1343
  version: version,