@remkoj/optimizely-cms-cli 2.0.0 → 2.0.1-pre1

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
@@ -55,11 +55,20 @@ function isDemanded(value) {
55
55
  * @param param0 The parameters from the Command Line application
56
56
  * @returns The arguments, with the Optimizely CMS Client parameters transformed into a configuration object
57
57
  */
58
- function parseArgs({ client_id, client_secret, cms_url, user_id, verbose, path: argsPath, components: argsComponents, ...args }) {
58
+ function parseArgs({ client_id, client_secret, cms_url, user_id, verbose, path: argsPath, components: argsComponents, ...args }, createBasePath = true) {
59
59
  const appPath = path.isAbsolute(argsPath) ? argsPath : path.normalize(path.join(process.cwd(), argsPath));
60
60
  const componentDir = path.normalize(path.join(argsPath, argsComponents));
61
61
  if (!componentDir.startsWith(argsPath))
62
62
  throw new Error(`The component directory ${componentDir} is outside the application directory (${appPath})`);
63
+ if (createBasePath && !fs.existsSync(componentDir)) {
64
+ if (verbose)
65
+ process.stdout.write(chalk.gray(`${figures.arrowRight} The components directory ${componentDir} does not exist yet, it will be created\n`));
66
+ fs.mkdirSync(componentDir, { recursive: true });
67
+ }
68
+ if (!fs.statSync(componentDir).isDirectory()) {
69
+ process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The components path ${componentDir} exists, but is not a folder\n`));
70
+ process.exit(1);
71
+ }
63
72
  return {
64
73
  _config: {
65
74
  base: cms_url,
@@ -143,26 +152,142 @@ function isNotNullOrUndefined(i) {
143
152
  return i ? true : false;
144
153
  }
145
154
 
155
+ function createCmsClient(args) {
156
+ const { _config: cfg } = parseArgs(args);
157
+ const client = createClient(cfg);
158
+ if (cfg.debug)
159
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Connecting to ${cfg.base.href} as ${cfg.actAs ?? cfg.clientId}\n`));
160
+ return client;
161
+ }
162
+
163
+ const contentTypesBuilder = yargs => {
164
+ yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
165
+ yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
166
+ yargs.option("baseTypes", { alias: 'b', description: "Select only these base types", string: true, type: 'array', demandOption: false, default: [] });
167
+ yargs.option("types", { alias: 't', description: "Select only these types", string: true, type: 'array', demandOption: false, default: [] });
168
+ return yargs;
169
+ };
170
+ async function getContentTypes(client, args, pageSize = 100, allowSystem = false) {
171
+ const { _config: cfg, excludeBaseTypes, excludeTypes, baseTypes, types } = parseArgs(args);
172
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
173
+ if (cfg.debug)
174
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
175
+ let resultsPage = await client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
176
+ const results = resultsPage.items ?? [];
177
+ let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
178
+ while (pagesRemaining > 0 && results.length < resultsPage.totalItemCount) {
179
+ if (cfg.debug)
180
+ 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`));
181
+ resultsPage = await client.contentTypes.contentTypesList(undefined, undefined, resultsPage.pageIndex + 1, resultsPage.pageSize);
182
+ results.push(...resultsPage.items);
183
+ pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
184
+ }
185
+ if (cfg.debug) {
186
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
187
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Content-Types based upon arguments\n`));
188
+ }
189
+ const contentTypes = results.filter(data => {
190
+ // Remove items based upon filters
191
+ const keepType = (!excludeBaseTypes.includes(data.baseType)) &&
192
+ (!excludeTypes.includes(data.key)) &&
193
+ (baseTypes.length == 0 || baseTypes.includes(data.baseType)) &&
194
+ (types.length == 0 || types.includes(data.key));
195
+ if (!keepType) {
196
+ if (cfg.debug)
197
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${data.key} due to applied filters\n`));
198
+ return false;
199
+ }
200
+ // Skip system types if desired
201
+ if (data.source == 'system' && !allowSystem) {
202
+ if (cfg.debug)
203
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${data.key} due to it being a system type\n`));
204
+ return false;
205
+ }
206
+ return true;
207
+ });
208
+ if (cfg.debug)
209
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${contentTypes.length} items\n`));
210
+ return {
211
+ all: results,
212
+ contentTypes
213
+ };
214
+ }
215
+
216
+ const stylesBuilder = yargs => {
217
+ const newArgs = contentTypesBuilder(yargs);
218
+ newArgs.option('excludeNodeTypes', { alias: 'ent', description: "Exclude these node types", string: true, type: 'array', demandOption: false, default: [] });
219
+ newArgs.option('excludeTemplates', { alias: 'et', description: "Exclude these templates", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
220
+ newArgs.option("nodes", { alias: 'n', description: "Select only these node types", string: true, type: 'array', demandOption: false, default: [] });
221
+ newArgs.option("templates", { alias: 'd', description: "Select only these templates", string: true, type: 'array', demandOption: false, default: [] });
222
+ newArgs.option("templateTypes", { alias: 'tt', description: "Select only these template types", choices: ['node', 'base', 'component'], type: 'array', demandOption: false, default: [] });
223
+ return newArgs;
224
+ };
225
+ async function getStyles(client, args, pageSize = 100) {
226
+ const { _config: cfg, excludeBaseTypes, excludeTypes, excludeNodeTypes, excludeTemplates, baseTypes, types, nodes, templates, templateTypes } = parseArgs(args);
227
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Style-Definitions from Optimizely CMS\n`));
228
+ if (cfg.debug)
229
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
230
+ let resultsPage = await client.displayTemplates.displayTemplatesList(0, pageSize);
231
+ const results = resultsPage.items ?? [];
232
+ let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
233
+ while (pagesRemaining > 0 && results.length < resultsPage.totalItemCount) {
234
+ if (cfg.debug)
235
+ 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`));
236
+ resultsPage = await client.displayTemplates.displayTemplatesList(resultsPage.pageIndex + 1, resultsPage.pageSize);
237
+ results.push(...resultsPage.items);
238
+ pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
239
+ }
240
+ if (cfg.debug) {
241
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Style-Definitions from Optimizely CMS\n`));
242
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Style-Definitions based upon arguments\n`));
243
+ }
244
+ const styles = results.filter(data => {
245
+ if (isExcluded(data.key, excludeTemplates, templates)) {
246
+ if (cfg.debug)
247
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style defintion key filtering active\n`));
248
+ return false;
249
+ }
250
+ const templateType = data.baseType ? 'base' : data.nodeType ? 'node' : data.contentType ? 'component' : 'unknown';
251
+ if (isExcluded(templateType, [], templateTypes)) {
252
+ if (cfg.debug)
253
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style type filtering is active\n`));
254
+ return false;
255
+ }
256
+ if (data.baseType && isExcluded(data.baseType, excludeBaseTypes, baseTypes)) {
257
+ if (cfg.debug)
258
+ 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`));
259
+ return false;
260
+ }
261
+ if (data.contentType && isExcluded(data.contentType, excludeTypes, types)) {
262
+ if (cfg.debug)
263
+ 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`));
264
+ return false;
265
+ }
266
+ if (data.nodeType && isExcluded(data.nodeType, excludeNodeTypes, nodes)) {
267
+ if (cfg.debug)
268
+ 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`));
269
+ return false;
270
+ }
271
+ return true;
272
+ });
273
+ if (cfg.debug)
274
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${styles.length} items\n`));
275
+ return {
276
+ all: results,
277
+ styles
278
+ };
279
+ }
280
+ function isExcluded(value, exclusions, inclusions) {
281
+ if (value == undefined || value == null)
282
+ return false;
283
+ return exclusions.includes(value) || (inclusions.length > 0 && !inclusions.includes(value));
284
+ }
285
+
146
286
  const StylesListCommand = {
147
287
  command: "styles:list",
148
288
  describe: "List Visual Builder style definitions from the CMS",
149
289
  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
- }
290
+ const { all: results } = await getStyles(createCmsClient(args), { ...args, excludeBaseTypes: [], excludeNodeTypes: [], excludeTemplates: [], excludeTypes: [], baseTypes: [], nodes: [], templates: [], types: [], templateTypes: [] });
166
291
  const styles = new Table({
167
292
  head: [
168
293
  chalk.yellow(chalk.bold("Name")),
@@ -190,50 +315,15 @@ const StylesPullCommand = {
190
315
  command: "styles:pull",
191
316
  describe: "Create Visual Builder style definitions from the CMS",
192
317
  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;
318
+ const newYargs = stylesBuilder(yargs);
319
+ newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
320
+ newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true });
321
+ return newYargs;
198
322
  },
199
323
  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
324
+ const { _config: cfg, components: basePath, force, definitions } = parseArgs(args);
325
+ const client = createCmsClient(args);
326
+ const { styles: filteredResults } = await getStyles(client, args);
237
327
  //#region Create & Write opti-style.json files
238
328
  const typeFiles = {};
239
329
  const updatedTemplates = (await Promise.all(filteredResults.map(async (displayTemplate) => {
@@ -241,8 +331,8 @@ const StylesPullCommand = {
241
331
  let targetType;
242
332
  let typesPath;
243
333
  if (displayTemplate.nodeType) {
244
- itemPath = path.join(basePath, 'styles', displayTemplate.nodeType, displayTemplate.key);
245
- typesPath = path.join(basePath, 'styles', displayTemplate.nodeType);
334
+ itemPath = path.join(basePath, 'nodes', displayTemplate.nodeType, displayTemplate.key);
335
+ typesPath = path.join(basePath, 'nodes', displayTemplate.nodeType);
246
336
  targetType = 'node/' + displayTemplate.nodeType;
247
337
  }
248
338
  else if (displayTemplate.baseType) {
@@ -260,6 +350,17 @@ const StylesPullCommand = {
260
350
  fs.mkdirSync(itemPath, { recursive: true });
261
351
  // Write Style JSON
262
352
  const filePath = path.join(itemPath, `${displayTemplate.key}.opti-style.json`);
353
+ if (fs.existsSync(filePath)) {
354
+ if (!force) {
355
+ if (cfg.debug)
356
+ process.stdout.write(chalk.gray(`${figures.cross} Skipping style file for ${displayTemplate.key} - File already exists\n`));
357
+ return;
358
+ }
359
+ if (cfg.debug)
360
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting style file for ${displayTemplate.key}\n`));
361
+ }
362
+ else if (cfg.debug)
363
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating style file for ${displayTemplate.key}\n`));
263
364
  fs.writeFileSync(filePath, JSON.stringify(displayTemplate, undefined, 2));
264
365
  if (!typeFiles[targetType]) {
265
366
  typeFiles[targetType] = {
@@ -269,17 +370,23 @@ const StylesPullCommand = {
269
370
  }
270
371
  typeFiles[targetType].templates.push({ file: filePath, data: displayTemplate });
271
372
  return displayTemplate.key;
272
- })));
373
+ }))).filter(x => x);
273
374
  //#endregion
274
375
  //#region Create needed definition files
275
376
  if (definitions) {
276
377
  for (const targetId of Object.getOwnPropertyNames(typeFiles)) {
277
378
  const { filePath: typeFilePath, templates } = typeFiles[targetId];
278
- if (fs.existsSync(typeFilePath) && !force) {
379
+ if (fs.existsSync(typeFilePath)) {
380
+ if (!force) {
381
+ if (cfg.debug)
382
+ process.stdout.write(chalk.gray(`${figures.cross} Skipped writing definition file for ${targetId} - it already exists\n`));
383
+ continue;
384
+ }
279
385
  if (cfg.debug)
280
- process.stdout.write(chalk.gray(`${figures.cross} Skipped writing definition file ${typeFilePath} as it already exists\n`));
281
- continue;
386
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting definition file for ${targetId}\n`));
282
387
  }
388
+ else if (cfg.debug)
389
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating definition file for ${targetId}\n`));
283
390
  // Write Style definition
284
391
  const imports = ['import type { LayoutProps } from "@remkoj/optimizely-cms-react/components"', 'import type { ReactNode } from "react"'];
285
392
  const typeContents = [];
@@ -314,8 +421,7 @@ export type ${typeId}Component<DT extends Record<string, any> = Record<string, a
314
421
  }
315
422
  }
316
423
  //#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");
424
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + ` Created/updated style definitions for ${updatedTemplates.join(', ')}`)) + "\n");
319
425
  }
320
426
  };
321
427
  function ucFirst$1(input) {
@@ -328,59 +434,30 @@ const TypesPullCommand = {
328
434
  command: "types:pull",
329
435
  describe: "Pull content type definition files into the project",
330
436
  builder: (yargs) => {
437
+ const newArgs = contentTypesBuilder(yargs);
331
438
  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;
439
+ return newArgs;
337
440
  },
338
441
  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 => {
442
+ const { _config: { debug }, components: basePath, force } = parseArgs(args);
443
+ const client = createCmsClient(args);
444
+ const { contentTypes } = await getContentTypes(client, args);
445
+ const updatedTypes = contentTypes.map(contentType => {
369
446
  const typePath = path.join(basePath, contentType.baseType, contentType.key);
370
447
  const typeFile = path.join(typePath, `${contentType.key}.opti-type.json`);
371
448
  if (!fs.existsSync(typePath))
372
449
  fs.mkdirSync(typePath, { recursive: true });
373
450
  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`));
451
+ if (debug)
452
+ process.stdout.write(chalk.yellow(`${figures.cross} Skipping type definition for ${contentType.displayName} (${contentType.key}) - File already exists\n`));
375
453
  return contentType.key;
376
454
  }
377
- if (cfg.debug)
455
+ if (debug)
378
456
  process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key})\n`));
379
457
  fs.writeFileSync(typeFile, JSON.stringify(contentType, undefined, 2));
380
458
  return contentType.key;
381
459
  }).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");
460
+ process.stdout.write(chalk.green(chalk.bold(`${figures.tick} Created/updated type definitions for ${updatedTypes.join(', ')}\n`)));
384
461
  }
385
462
  };
386
463
 
@@ -444,209 +521,81 @@ const TypesPushCommand = {
444
521
  }
445
522
  };
446
523
 
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
- }
524
+ const builder = yargs => {
525
+ const updatedArgs = contentTypesBuilder(yargs);
526
+ updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
527
+ return updatedArgs;
522
528
  };
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`));
529
+ function createTypeFolders(contentTypes, basePath, debug = false) {
530
+ const folders = contentTypes.map(contentType => {
531
+ const baseType = contentType.baseType ?? 'default';
532
+ // Create the type folder
533
+ const typePath = path.join(basePath, baseType, contentType.key);
534
+ if (!fs.existsSync(typePath)) {
535
+ fs.mkdirSync(typePath, { recursive: true });
536
+ if (debug)
537
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Created folder ${typePath} for Content-Type ${contentType.key}\n`));
538
538
  }
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,`);
539
+ // Check folders
540
+ if (!fs.statSync(typePath).isDirectory())
541
+ throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
542
+ return {
543
+ type: contentType.key,
544
+ path: typePath,
545
+ };
580
546
  });
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"));
547
+ return folders;
589
548
  }
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);
549
+ function getTypeFolder(list, type) {
550
+ return list.filter(x => x.type == type).at(0)?.path;
633
551
  }
634
- function createGraphFragments(contentType, typePath, basePath, force, cfg, contentTypes) {
552
+
553
+ const NextJsQueriesCommand = {
554
+ command: "nextjs:fragments",
555
+ describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
556
+ builder,
557
+ handler: async (args, opts) => {
558
+ // Prepare
559
+ const { loadedContentTypes, createdTypeFolders } = opts || {};
560
+ const { components: basePath, _config: { debug }, force } = parseArgs(args);
561
+ const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(createCmsClient(args), args);
562
+ // Start process
563
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments for ${contentTypes.map(x => x.key).join(', ')}\n`));
564
+ const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
565
+ const updatedTypes = contentTypes.map(contentType => {
566
+ const typePath = getTypeFolder(typeFolders, contentType.key);
567
+ return createGraphFragments(contentType, typePath, basePath, force, debug, allContentTypes);
568
+ }).filter(x => x).flat();
569
+ // Report outcome
570
+ if (updatedTypes.length > 0)
571
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
572
+ else
573
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
574
+ if (!opts)
575
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
576
+ }
577
+ };
578
+ function createGraphFragments(contentType, typePath, basePath, force, debug, contentTypes) {
579
+ const returnValue = [];
635
580
  const baseType = contentType.baseType ?? 'default';
636
581
  const baseQueryFile = path.join(typePath, `${contentType.key}.${baseType}.graphql`);
637
582
  if (fs.existsSync(baseQueryFile)) {
638
583
  if (force) {
639
- if (cfg.debug)
584
+ if (debug)
640
585
  process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
641
586
  }
642
587
  else {
643
- if (cfg.debug)
588
+ if (debug)
644
589
  process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
645
590
  return undefined;
646
591
  }
647
592
  }
593
+ else if (debug) {
594
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
595
+ }
648
596
  const { fragment, propertyTypes } = createInitialFragment(contentType);
649
597
  fs.writeFileSync(baseQueryFile, fragment);
598
+ returnValue.push(contentType.key);
650
599
  let dependencies = Array.isArray(propertyTypes) ? [...propertyTypes] : [];
651
600
  while (Array.isArray(dependencies) && dependencies.length > 0) {
652
601
  let newDependencies = [];
@@ -661,15 +610,18 @@ function createGraphFragments(contentType, typePath, basePath, force, cfg, conte
661
610
  if (!fs.existsSync(propertyFragmentDir))
662
611
  fs.mkdirSync(propertyFragmentDir, { recursive: true });
663
612
  if (!fs.existsSync(propertyFragmentFile) || force) {
664
- process.stdout.write(` - Writing property fragment: ${propContentType.displayName ?? propContentType.key}\n`);
613
+ if (debug)
614
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Writing ${propContentType.displayName} (${propContentType.key}) property fragment\n`));
665
615
  const propContentTypeInfo = createInitialFragment(propContentType, true);
666
616
  fs.writeFileSync(propertyFragmentFile, propContentTypeInfo.fragment);
617
+ returnValue.push(propContentType.key);
667
618
  if (Array.isArray(propContentTypeInfo.propertyTypes))
668
619
  newDependencies.push(...propContentTypeInfo.propertyTypes);
669
620
  }
670
621
  });
671
622
  dependencies = newDependencies;
672
623
  }
624
+ return returnValue.length > 0 ? returnValue : undefined;
673
625
  }
674
626
  function createInitialFragment(contentType, forProperty = false) {
675
627
  const propertyTypes = [];
@@ -727,6 +679,8 @@ function createInitialFragment(contentType, forProperty = false) {
727
679
  break;
728
680
  }
729
681
  });
682
+ if (contentType.baseType == "experience")
683
+ fragmentFields.push('...ExperienceData');
730
684
  if (fragmentFields.length == 0)
731
685
  fragmentFields.push('_metadata { key }');
732
686
  const tpl = `fragment ${contentType.key}${forProperty ? 'Property' : ''}Data on ${contentType.key}${forProperty ? 'Property' : ''} {
@@ -738,6 +692,499 @@ function createInitialFragment(contentType, forProperty = false) {
738
692
  };
739
693
  }
740
694
 
695
+ function ucFirst(current) {
696
+ if (typeof current != 'string')
697
+ throw new Error("Only strings can be transformed");
698
+ if (current == "")
699
+ return current;
700
+ return current[0]?.toUpperCase() + current.substring(1);
701
+ }
702
+
703
+ const NextJsComponentsCommand = {
704
+ command: "nextjs:components",
705
+ describe: "Create the React Components for a Next.JS / Optimizely Graph structure",
706
+ builder,
707
+ handler: async (args, opts) => {
708
+ // Prepare
709
+ const { loadedContentTypes, createdTypeFolders } = opts || {};
710
+ const { components: basePath, _config: { debug }, force } = parseArgs(args);
711
+ const { contentTypes } = loadedContentTypes ?? await getContentTypes(createCmsClient(args), args);
712
+ // Start process
713
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating React Components for ${contentTypes.map(x => x.key).join(', ')}\n`));
714
+ const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
715
+ const updatedTypes = contentTypes.map(contentType => {
716
+ const typePath = getTypeFolder(typeFolders, contentType.key);
717
+ return createComponent(contentType, typePath, force, debug);
718
+ }).filter(x => x);
719
+ // Report outcome
720
+ if (updatedTypes.length > 0)
721
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated React Components for ${updatedTypes.join(', ')}\n`));
722
+ else
723
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No React Components created/updated\n`));
724
+ if (!opts)
725
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
726
+ }
727
+ };
728
+ function createComponent(contentType, typePath, force, debug = false) {
729
+ const componentFile = path.join(typePath, 'index.tsx');
730
+ if (fs.existsSync(componentFile)) {
731
+ if (!force) {
732
+ if (debug)
733
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) component - file already exists\n`));
734
+ return undefined;
735
+ }
736
+ if (debug)
737
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) component\n`));
738
+ }
739
+ else if (debug)
740
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) component\n`));
741
+ // Get type information & short-hands
742
+ const displayTemplate = getDisplayTemplateInfo$1(contentType, typePath);
743
+ const baseDisplayTemplate = getBaseTypeTemplateInfo(contentType, typePath);
744
+ const varName = `${contentType.key}${ucFirst(contentType.baseType ?? 'part')}`;
745
+ const tplFn = Templates[contentType.baseType] ?? Templates['default'];
746
+ if (!tplFn) {
747
+ if (debug)
748
+ process.stdout.write(chalk.redBright(`${figures.cross} Skipping ${contentType.displayName} (${contentType.key}) component - no template for ${contentType.baseType} found\n`));
749
+ return undefined;
750
+ }
751
+ // Apply template
752
+ fs.writeFileSync(componentFile, tplFn(contentType, varName, displayTemplate, baseDisplayTemplate));
753
+ return contentType.key;
754
+ }
755
+ function getBaseTypeTemplateInfo(contentType, typePath) {
756
+ const displayTemplatesFile = path.join(typePath, '..', 'styles', 'displayTemplates.ts');
757
+ if (fs.existsSync(displayTemplatesFile)) {
758
+ const baseTypeVarName = contentType.baseType ? contentType.baseType[0].toUpperCase() + contentType.baseType.substring(1) : undefined;
759
+ if (!baseTypeVarName)
760
+ return undefined;
761
+ const displayTemplateContent = fs.readFileSync(displayTemplatesFile).toString();
762
+ const displayTemplateName = baseTypeVarName + 'LayoutProps';
763
+ if (displayTemplateContent.includes(`export type ${displayTemplateName} =`)) {
764
+ return displayTemplateName;
765
+ }
766
+ }
767
+ return undefined;
768
+ }
769
+ function getDisplayTemplateInfo$1(contentType, typePath) {
770
+ const displayTemplatesFile = path.join(typePath, 'displayTemplates.ts');
771
+ if (fs.existsSync(displayTemplatesFile)) {
772
+ const displayTemplateContent = fs.readFileSync(displayTemplatesFile).toString();
773
+ const displayTemplateName = contentType.key + 'LayoutProps';
774
+ if (displayTemplateContent.includes(`export type ${displayTemplateName} =`)) {
775
+ return displayTemplateName;
776
+ }
777
+ }
778
+ return undefined;
779
+ }
780
+ const Templates = {
781
+ // Default Template for all components without specifics
782
+ default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsComponent } from "@remkoj/optimizely-cms-react";
783
+ import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
784
+ import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
785
+ import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
786
+
787
+ /**
788
+ * ${contentType.displayName}
789
+ * ${contentType.description}
790
+ */
791
+ export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, children }) => {
792
+ const componentName = '${contentType.displayName}'
793
+ const componentInfo = '${contentType.description ?? ''}'
794
+ return <div className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4">
795
+ <div className="font-bold italic">{ componentName }</div>
796
+ <div>{ componentInfo }</div>
797
+ { 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> }
798
+ { children && <div className="mt-4 mx-4 flex flex-col">{ children }</div>}
799
+ </div>
800
+ }
801
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
802
+ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
803
+
804
+ export default ${varName}`,
805
+ // Template for all page component types
806
+ page: (contentType, varName, displayTemplate) => `import { OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
807
+ import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
808
+ import { ${displayTemplate} } from "./displayTemplates";` : ''}
809
+ import { getSdk } from "@/gql"
810
+
811
+ /**
812
+ * ${contentType.displayName}
813
+ * ${contentType.description}
814
+ */
815
+ export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, children }) => {
816
+ const componentName = '${contentType.displayName}'
817
+ const componentInfo = '${contentType.description ?? ''}'
818
+ return <div className="mx-auto px-2 container">
819
+ <div className="font-bold italic">{ componentName }</div>
820
+ <div>{ componentInfo }</div>
821
+ { 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> }
822
+ { children && <div className="flex flex-col mt-4 mx-4">{ children }</div>}
823
+ </div>
824
+ }
825
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
826
+ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
827
+ ${varName}.getMetaData = async (contentLink, locale, client) => {
828
+ const sdk = getSdk(client);
829
+ // Add your metadata logic here
830
+ return {}
831
+ }
832
+
833
+ export default ${varName}`,
834
+ // Template for all experience component types
835
+ experience: (contentType, varName, displayTemplate) => `import { OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
836
+ import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";
837
+ import { type Maybe, type ICompositionNode, type ExperienceDataFragment } from "@/gql/graphql";
838
+ import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
839
+ import { ${displayTemplate} } from "./displayTemplates";` : ''}
840
+ import { getSdk } from "@/gql"
841
+
842
+ /**
843
+ * ${contentType.displayName}
844
+ * ${contentType.description}
845
+ */
846
+ export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''} }) => {
847
+ const composition = (data as ExperienceDataFragment).composition as Maybe<ICompositionNode>
848
+ return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData">
849
+ { composition && isNode(composition) && <OptimizelyComposition node={composition} /> }
850
+ </CmsEditable>
851
+ }
852
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
853
+ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
854
+ ${varName}.getMetaData = async (contentLink, locale, client) => {
855
+ const sdk = getSdk(client);
856
+ // Add your metadata logic here
857
+ return {}
858
+ }
859
+
860
+ export default ${varName}`
861
+ };
862
+
863
+ const NextJsVisualBuilderCommand = {
864
+ command: "nextjs:visualbuilder",
865
+ describe: "Create the React Components for Visual Builder in a Next.JS / Optimizely Graph structure",
866
+ builder,
867
+ handler: async (args) => {
868
+ // Prepare
869
+ const { components: basePath, _config: { debug }, force } = parseArgs(args);
870
+ // Create the fall-back node and style defintions
871
+ await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node'], definitions: true });
872
+ createGenericNode(basePath, force, debug);
873
+ // Get all styles
874
+ const { styles } = await getStyles(createCmsClient(args), { ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'] });
875
+ // Process node styles
876
+ styles.filter(x => typeof (x.nodeType) == 'string' && x.nodeType.length > 0).map(styleDefintion => {
877
+ const templatePath = path.join(basePath, 'nodes', styleDefintion.nodeType, styleDefintion.key);
878
+ createSpecificNode(styleDefintion, templatePath, force, debug);
879
+ });
880
+ // Process base styles
881
+ styles.filter(x => typeof (x.baseType) == 'string' && x.baseType.length > 0).map(styleDefinition => {
882
+ const templatePath = path.join(basePath, styleDefinition.baseType, 'styles', styleDefinition.key);
883
+ createSpecificNode(styleDefinition, templatePath, force, debug);
884
+ });
885
+ }
886
+ };
887
+ function createSpecificNode(template, templatePath, force = false, debug = false) {
888
+ const nodeFile = path.join(templatePath, 'index.tsx');
889
+ if (fs.existsSync(nodeFile)) {
890
+ if (!force) {
891
+ if (debug)
892
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${template.displayName} (${template.key}) component - file already exists\n`));
893
+ return undefined;
894
+ }
895
+ if (debug)
896
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${template.displayName} (${template.key}) component\n`));
897
+ }
898
+ else if (debug)
899
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${template.displayName} (${template.key}) component\n`));
900
+ if (!fs.existsSync(templatePath))
901
+ fs.mkdirSync(templatePath, { recursive: true });
902
+ const displayTemplateName = getDisplayTemplateInfo(template, templatePath);
903
+ const component = `import { type CmsLayoutComponent } from "@remkoj/optimizely-cms-react";
904
+ import { extractSettings } from "@remkoj/optimizely-cms-react/components";${displayTemplateName ? `
905
+ import { ${displayTemplateName} } from "../displayTemplates";` : ''}
906
+
907
+ export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ contentLink, layoutProps, children }) => {
908
+ const layout = extractSettings(layoutProps);
909
+ return (<div className="vb:${template.nodeType} vb:${template.nodeType}:${template.key}">{ children }</div>);
910
+ }
911
+
912
+ export default ${template.key};`;
913
+ fs.writeFileSync(nodeFile, component);
914
+ }
915
+ function createGenericNode(basePath, force, debug) {
916
+ const nodeItem = path.join(basePath, 'node.tsx');
917
+ if (fs.existsSync(nodeItem)) {
918
+ if (!force) {
919
+ if (debug)
920
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping generic node component - file already exists\n`));
921
+ return undefined;
922
+ }
923
+ if (debug)
924
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting generic node component\n`));
925
+ }
926
+ else if (debug)
927
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating generic node component\n`));
928
+ const nodeContent = `import { type CmsLayoutComponent } from "@remkoj/optimizely-cms-react"
929
+ import { CmsEditable } from '@remkoj/optimizely-cms-react/rsc'
930
+
931
+ export const VisualBuilderNode : CmsLayoutComponent = ({ contentLink, layoutProps, children }) =>
932
+ {
933
+ let className = \`vb:\${layoutProps?.layoutType}\`
934
+ if (layoutProps && layoutProps.layoutType == "section")
935
+ return <CmsEditable as="div" className={ className } cmsId={ contentLink.key }>{ children }</CmsEditable>
936
+ return <div className={ className }>{ children }</div>
937
+ }
938
+
939
+ export default VisualBuilderNode`;
940
+ fs.writeFileSync(nodeItem, nodeContent);
941
+ }
942
+ function getDisplayTemplateInfo(template, typePath) {
943
+ const displayTemplatesFile = path.join(typePath, '..', 'displayTemplates.ts');
944
+ if (fs.existsSync(displayTemplatesFile)) {
945
+ const displayTemplateContent = fs.readFileSync(displayTemplatesFile).toString();
946
+ const displayTemplateName = template.key + 'Props';
947
+ if (displayTemplateContent.includes(`export type ${displayTemplateName} =`)) {
948
+ return displayTemplateName;
949
+ }
950
+ }
951
+ return undefined;
952
+ }
953
+
954
+ const NextJsFactoryCommand = {
955
+ command: "nextjs:factory",
956
+ describe: "Create the ComponentFactory for a Next.JS / Optimizely Graph structure",
957
+ builder,
958
+ handler: async (args) => {
959
+ const { components: basePath, force, _config: { debug } } = parseArgs(args);
960
+ if (debug)
961
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Start generating component factories\n`));
962
+ const baseDirEntries = fs.readdirSync(basePath);
963
+ const entries = baseDirEntries
964
+ .filter(entry => fs.statSync(path.join(basePath, entry)).isDirectory())
965
+ .map(entry => entry == "nodes" ? processSubGroupFolder(entry, path.join(basePath, entry), force, debug) : processTypeListFolder(entry, path.join(basePath, entry), force, debug))
966
+ .filter(x => x);
967
+ // Post process the entries
968
+ if (debug)
969
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Post processing resolved top-level factory entries\n`));
970
+ entries.forEach((e, i) => {
971
+ if (isImportInfoList(e) && typeof e.prefix == 'string')
972
+ switch (e.prefix) {
973
+ case "Experience":
974
+ entries[i].prefix = [e.prefix, "Page"];
975
+ break;
976
+ case "Element":
977
+ entries[i].prefix = [e.prefix, "Component"];
978
+ break;
979
+ }
980
+ });
981
+ // Add any global components
982
+ if (debug)
983
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Adding global components\n`));
984
+ baseDirEntries.filter(x => x.endsWith('.tsx') && fs.statSync(path.join(basePath, x)).isFile()).forEach(entry => {
985
+ const entryName = entry.substring(0, entry.length - 4).replaceAll('.', '').split('-').map(p => ucFirst(p)).join('');
986
+ const importPath = './' + entry.substring(0, entry.length - 4);
987
+ entries.push({
988
+ isList: false,
989
+ component: (entryName[0].toLowerCase() + entryName.substring(1)) + "Component",
990
+ path: importPath,
991
+ type: entryName
992
+ });
993
+ });
994
+ // We're always overwriting the main factory
995
+ if (debug)
996
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating/overwriting main factory\n`));
997
+ const mainFactoryFile = path.join(basePath, "index.ts");
998
+ fs.writeFileSync(mainFactoryFile, generateFactory(entries, "cms"));
999
+ process.stdout.write(chalk.yellow(`${figures.arrowRight} Generated main factory in: ${mainFactoryFile}.\n`));
1000
+ }
1001
+ };
1002
+ function processSubGroupFolder(groupName, groupTypePath, force = false, debug = false) {
1003
+ const subgroupInfo = fs
1004
+ .readdirSync(groupTypePath)
1005
+ .filter(entry => {
1006
+ return fs.statSync(path.join(groupTypePath, entry)).isDirectory();
1007
+ }).map(entry => processTypeListFolder(entry, path.join(groupTypePath, entry), force, debug))
1008
+ .filter(x => x);
1009
+ if (subgroupInfo.length == 0)
1010
+ return null;
1011
+ const baseTypeIndex = path.join(groupTypePath, "index.ts");
1012
+ /* Check file existence and determine if we can continue */
1013
+ if (debug)
1014
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Identified base type ${groupName}\n`));
1015
+ if (fs.existsSync(baseTypeIndex)) {
1016
+ if (!force) {
1017
+ if (debug)
1018
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping factory for ${groupName} - file already exists\n`));
1019
+ return {
1020
+ isList: true,
1021
+ component: groupName + 'Dictionary',
1022
+ prefix: groupName[0].toUpperCase() + groupName.substring(1),
1023
+ path: './' + groupName
1024
+ };
1025
+ }
1026
+ if (debug)
1027
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting factory for ${groupName}\n`));
1028
+ }
1029
+ else if (debug)
1030
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Generating new factory for ${groupName}\n`));
1031
+ fs.writeFileSync(baseTypeIndex, generateFactory(subgroupInfo, groupName));
1032
+ process.stdout.write(chalk.yellow(`${figures.arrowRight} Generated partial factory in: ${baseTypeIndex}.\n`));
1033
+ return {
1034
+ isList: true,
1035
+ component: groupName + 'Dictionary',
1036
+ prefix: groupName[0].toUpperCase() + groupName.substring(1),
1037
+ path: './' + groupName
1038
+ };
1039
+ }
1040
+ function isImportInfoList(toTest) {
1041
+ return toTest.isList == true;
1042
+ }
1043
+ function isPrefixedImportInfoList(toTest) {
1044
+ 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)));
1045
+ }
1046
+ function processTypeListFolder(baseType, baseTypePath, force = false, debug = false) {
1047
+ const baseTypeIndex = path.join(baseTypePath, "index.ts");
1048
+ /* Check file existence and determine if we can continue */
1049
+ if (debug)
1050
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Identified base type ${baseType}\n`));
1051
+ if (fs.existsSync(baseTypeIndex)) {
1052
+ if (!force) {
1053
+ if (debug)
1054
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping factory for ${baseType} - file already exists\n`));
1055
+ return {
1056
+ isList: true,
1057
+ component: baseType + 'Dictionary',
1058
+ prefix: baseType[0].toUpperCase() + baseType.substring(1),
1059
+ path: './' + baseType
1060
+ };
1061
+ }
1062
+ if (debug)
1063
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting factory for ${baseType}\n`));
1064
+ }
1065
+ else if (debug)
1066
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Generating new factory for ${baseType}\n`));
1067
+ // Get the components to import
1068
+ let hasStyles = false;
1069
+ const components = fs
1070
+ .readdirSync(baseTypePath)
1071
+ .filter(entry => {
1072
+ if (entry == 'styles') {
1073
+ hasStyles = true;
1074
+ return false;
1075
+ }
1076
+ return fs.statSync(path.join(baseTypePath, entry)).isDirectory() &&
1077
+ fs.existsSync(path.join(baseTypePath, entry, "index.tsx"));
1078
+ })
1079
+ .map(entry => {
1080
+ return {
1081
+ component: entry + 'Component',
1082
+ path: './' + entry,
1083
+ type: entry
1084
+ };
1085
+ });
1086
+ // Handle styles folder
1087
+ if (hasStyles) {
1088
+ const styles = processTypeListFolder("styles", path.join(baseTypePath, 'styles'), force, debug);
1089
+ if (styles) {
1090
+ components.push({
1091
+ path: "./styles",
1092
+ component: "styleDictionary",
1093
+ isList: true,
1094
+ prefix: "Styles"
1095
+ });
1096
+ }
1097
+ }
1098
+ components.sort((a, b) => {
1099
+ if (isImportInfoList(a) && !isImportInfoList(b))
1100
+ return -1;
1101
+ if (!isImportInfoList(a) && isImportInfoList(b))
1102
+ return 1;
1103
+ return 0;
1104
+ });
1105
+ if (components.length == 0) {
1106
+ if (debug)
1107
+ process.stdout.write(chalk.gray(`${figures.arrowRight} The base type ${baseType} has no components.\n`));
1108
+ return null;
1109
+ }
1110
+ if (debug)
1111
+ 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`));
1112
+ // Create the file
1113
+ const factoryContent = generateFactory(components, baseType);
1114
+ fs.writeFileSync(baseTypeIndex, factoryContent);
1115
+ process.stdout.write(chalk.yellow(`${figures.arrowRight} Generated partial factory in: ${baseTypeIndex}.\n`));
1116
+ // Build the result
1117
+ return {
1118
+ isList: true,
1119
+ component: baseType + 'Dictionary',
1120
+ prefix: baseType[0].toUpperCase() + baseType.substring(1),
1121
+ path: './' + baseType
1122
+ };
1123
+ }
1124
+ function generateFactory(components, typeName) {
1125
+ const needsPrefixFunction = components.some(isPrefixedImportInfoList);
1126
+ const factoryContent = `// Auto generated dictionary
1127
+ import { ComponentTypeDictionary } from "@remkoj/optimizely-cms-react";
1128
+ ${components.map(x => `import ${x.component} from "${x.path}";`).join("\n")}
1129
+
1130
+ ${needsPrefixFunction ? `// Prefix entries - if needed
1131
+ ${components.filter(isPrefixedImportInfoList).map(x => Array.isArray(x.prefix) ?
1132
+ x.prefix.map(z => `prefixDictionaryEntries(${x.component}, "${z}");`).join("\n") :
1133
+ `prefixDictionaryEntries(${x.component}, "${x.prefix}");`).join("\n")}
1134
+
1135
+ ` : ''}// Build dictionary
1136
+ export const ${typeName}Dictionary : ComponentTypeDictionary = [
1137
+ ${components.map(x => x.isList == true ? `...${x.component}` : `{
1138
+ type: "${x.type}",
1139
+ component: ${x.component}
1140
+ }`).join(",\n ")}
1141
+ ];
1142
+
1143
+ // Export dictionary
1144
+ export default ${typeName}Dictionary;${needsPrefixFunction ? `
1145
+
1146
+ // Helper functions
1147
+ function prefixDictionaryEntries(list: ComponentTypeDictionary, prefix: string) : ComponentTypeDictionary
1148
+ {
1149
+ list.forEach((component, idx, dictionary) => {
1150
+ dictionary[idx].type = typeof component.type == 'string' ? prefix + "/" + component.type : [ prefix, ...component.type ]
1151
+ });
1152
+ return list;
1153
+ }` : ''}
1154
+ `;
1155
+ return factoryContent;
1156
+ }
1157
+
1158
+ const NextJsCreateCommand = {
1159
+ command: "nextjs:create",
1160
+ describe: "Scaffold a complete Next.JS / Optimizely Graph structure",
1161
+ builder,
1162
+ handler: async (args) => {
1163
+ const { _config: cfg, components: basePath } = parseArgs(args);
1164
+ const client = createCmsClient(args);
1165
+ const getContentTypesResult = await getContentTypes(client, args);
1166
+ const { contentTypes } = getContentTypesResult;
1167
+ const folders = createTypeFolders(contentTypes, basePath, cfg.debug);
1168
+ process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared context, to limit duplicate work") + "\n");
1169
+ // First create the base files in parallel
1170
+ await Promise.all([
1171
+ TypesPullCommand.handler(args),
1172
+ StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: [], definitions: true }),
1173
+ NextJsQueriesCommand.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
1174
+ ]);
1175
+ process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared types, styles and fragments") + "\n");
1176
+ // Then create the components
1177
+ await NextJsComponentsCommand.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders });
1178
+ await NextJsVisualBuilderCommand.handler(args);
1179
+ process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Created components") + "\n");
1180
+ // Finally create the factories
1181
+ await NextJsFactoryCommand.handler(args);
1182
+ process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Created component factory") + "\n");
1183
+ process.stdout.write("\n");
1184
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Scaffolded a complete Next.JS / Optimizely Graph structure")) + "\n");
1185
+ }
1186
+ };
1187
+
741
1188
  const CmsVersionCommand = {
742
1189
  command: "cms:version",
743
1190
  describe: "Get the CMS Version information",
@@ -772,10 +1219,14 @@ const commands = [
772
1219
  TypesPullCommand,
773
1220
  TypesPushCommand,
774
1221
  NextJsCreateCommand,
1222
+ NextJsQueriesCommand,
1223
+ NextJsComponentsCommand,
1224
+ NextJsVisualBuilderCommand,
1225
+ NextJsFactoryCommand,
775
1226
  CmsVersionCommand
776
1227
  ];
777
1228
 
778
- var version = "2.0.0";
1229
+ var version = "2.0.1-pre1";
779
1230
  var name = "opti-cms";
780
1231
  var APP = {
781
1232
  version: version,