@remkoj/optimizely-cms-cli 2.0.0-pre5 → 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,
@@ -77,30 +86,35 @@ function parseArgs({ client_id, client_secret, cms_url, user_id, verbose, path:
77
86
  const StylesPushCommand = {
78
87
  command: "styles:push",
79
88
  describe: "Push Visual Builder style definitions into the CMS (create/replace)",
89
+ builder: (yargs) => {
90
+ yargs.option('excludeTemplates', { alias: 'e', description: "Exclude these templates", string: true, type: 'array', demandOption: false, default: [] });
91
+ yargs.option("templates", { alias: 't', description: "Select only these templates", string: true, type: 'array', demandOption: false, default: [] });
92
+ return yargs;
93
+ },
80
94
  handler: async (args) => {
81
- const { _config: cfg, ...opts } = parseArgs(args);
95
+ const { _config: cfg, excludeTemplates, templates, ...opts } = parseArgs(args);
82
96
  const client = createClient(cfg);
83
- /*const currentTemplates = await client.displayTemplates.displayTemplatesList()
84
- currentTemplates.items?.map(tpl => {
85
- console.log(JSON.stringify(tpl, undefined, 4))
86
- })*/
87
97
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/replace) DisplayStyles into Optimizely CMS\n`));
88
98
  const styleDefinitionFiles = await glob("./**/*.opti-style.json", {
89
99
  cwd: opts.components
90
100
  });
91
- const results = await Promise.all(styleDefinitionFiles.map(async (styleDefinitionFile) => {
101
+ const results = (await Promise.all(styleDefinitionFiles.map(async (styleDefinitionFile) => {
92
102
  const filePath = path.normalize(path.join(opts.components, styleDefinitionFile));
93
103
  const styleDefinition = tryReadJsonFile(filePath, cfg.debug);
94
104
  const styleKey = styleDefinition.key;
95
105
  if (!styleKey) {
96
106
  process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The style definition in ${path.relative(opts.path, filePath)} does not have a key defined\n`));
97
- return;
107
+ return undefined;
98
108
  }
109
+ if (excludeTemplates.includes(styleKey))
110
+ return undefined; // Skip excluded styles
111
+ if (templates.length > 0 && !templates.includes(styleKey))
112
+ return undefined; // Only include defined styles, if any
99
113
  if (cfg.debug)
100
114
  process.stdout.write(chalk.gray(`${figures.arrowRight} Pushing: ${styleKey}\n`));
101
115
  const newTemplate = await client.displayTemplates.displayTemplatesPut(styleKey, styleDefinition);
102
116
  return newTemplate;
103
- }));
117
+ }))).filter(isNotNullOrUndefined);
104
118
  const styles = new Table({
105
119
  head: [
106
120
  chalk.yellow(chalk.bold("Name")),
@@ -134,27 +148,146 @@ function tryReadJsonFile(filePath, debug = false) {
134
148
  }
135
149
  return undefined;
136
150
  }
151
+ function isNotNullOrUndefined(i) {
152
+ return i ? true : false;
153
+ }
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
+ }
137
285
 
138
286
  const StylesListCommand = {
139
287
  command: "styles:list",
140
288
  describe: "List Visual Builder style definitions from the CMS",
141
289
  handler: async (args) => {
142
- const { _config: cfg, ...opts } = parseArgs(args);
143
- const client = createClient(cfg);
144
- const pageSize = 100;
145
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Reading DisplayStyles from Optimizely CMS\n`));
146
- if (cfg.debug)
147
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
148
- let templatesPage = await client.displayTemplates.displayTemplatesList(0, pageSize);
149
- const results = templatesPage.items ?? [];
150
- let pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
151
- while (pagesRemaining > 0 && results.length < templatesPage.totalItemCount) {
152
- if (cfg.debug)
153
- 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`));
154
- templatesPage = await client.displayTemplates.displayTemplatesList(templatesPage.pageIndex + 1, templatesPage.pageSize);
155
- results.push(...templatesPage.items);
156
- pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
157
- }
290
+ const { all: results } = await getStyles(createCmsClient(args), { ...args, excludeBaseTypes: [], excludeNodeTypes: [], excludeTemplates: [], excludeTypes: [], baseTypes: [], nodes: [], templates: [], types: [], templateTypes: [] });
158
291
  const styles = new Table({
159
292
  head: [
160
293
  chalk.yellow(chalk.bold("Name")),
@@ -181,39 +314,25 @@ const StylesListCommand = {
181
314
  const StylesPullCommand = {
182
315
  command: "styles:pull",
183
316
  describe: "Create Visual Builder style definitions from the CMS",
317
+ builder: (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;
322
+ },
184
323
  handler: async (args) => {
185
- const { _config: cfg, components: basePath } = parseArgs(args);
186
- const client = createClient(cfg);
187
- const pageSize = 100;
188
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Reading DisplayStyles from Optimizely CMS\n`));
189
- if (cfg.debug)
190
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
191
- let templatesPage = await client.displayTemplates.displayTemplatesList(0, pageSize);
192
- const results = templatesPage.items ?? [];
193
- let pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
194
- while (pagesRemaining > 0 && results.length < templatesPage.totalItemCount) {
195
- if (cfg.debug)
196
- 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`));
197
- templatesPage = await client.displayTemplates.displayTemplatesList(templatesPage.pageIndex + 1, templatesPage.pageSize);
198
- results.push(...templatesPage.items);
199
- pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
200
- }
201
- if (cfg.debug)
202
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
203
- if (!fs.existsSync(basePath))
204
- fs.mkdirSync(basePath, { recursive: true });
205
- if (!fs.statSync(basePath).isDirectory()) {
206
- process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The components path ${basePath} is not a folder\n`));
207
- process.exit(1);
208
- }
324
+ const { _config: cfg, components: basePath, force, definitions } = parseArgs(args);
325
+ const client = createCmsClient(args);
326
+ const { styles: filteredResults } = await getStyles(client, args);
327
+ //#region Create & Write opti-style.json files
209
328
  const typeFiles = {};
210
- const updatedTemplates = await Promise.all(results.map(async (displayTemplate) => {
329
+ const updatedTemplates = (await Promise.all(filteredResults.map(async (displayTemplate) => {
211
330
  let itemPath = undefined;
212
331
  let targetType;
213
332
  let typesPath;
214
333
  if (displayTemplate.nodeType) {
215
- itemPath = path.join(basePath, 'styles', displayTemplate.nodeType, displayTemplate.key);
216
- 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);
217
336
  targetType = 'node/' + displayTemplate.nodeType;
218
337
  }
219
338
  else if (displayTemplate.baseType) {
@@ -231,6 +350,17 @@ const StylesPullCommand = {
231
350
  fs.mkdirSync(itemPath, { recursive: true });
232
351
  // Write Style JSON
233
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`));
234
364
  fs.writeFileSync(filePath, JSON.stringify(displayTemplate, undefined, 2));
235
365
  if (!typeFiles[targetType]) {
236
366
  typeFiles[targetType] = {
@@ -238,292 +368,234 @@ const StylesPullCommand = {
238
368
  templates: []
239
369
  };
240
370
  }
241
- typeFiles[targetType].templates.push(displayTemplate);
371
+ typeFiles[targetType].templates.push({ file: filePath, data: displayTemplate });
242
372
  return displayTemplate.key;
243
- }));
244
- for (const targetId of Object.getOwnPropertyNames(typeFiles)) {
245
- const { filePath: typeFilePath, templates } = typeFiles[targetId];
246
- // Write Style definition
247
- const typeContents = [];
248
- let typeId = undefined;
249
- templates.forEach(displayTemplate => {
250
- if (Object.getOwnPropertyNames(displayTemplate.settings).length > 0) {
251
- typeContents.push(`export type ${displayTemplate.key}Settings = Array<`);
252
- const typeSettings = [];
253
- for (const settingName of Object.getOwnPropertyNames(displayTemplate.settings)) {
254
- const settingOptions = '"' + Object.getOwnPropertyNames(displayTemplate.settings[settingName].choices).join('" | "') + '"';
255
- typeSettings.push(` { key: "${settingName}", value: ${settingOptions}}`);
373
+ }))).filter(x => x);
374
+ //#endregion
375
+ //#region Create needed definition files
376
+ if (definitions) {
377
+ for (const targetId of Object.getOwnPropertyNames(typeFiles)) {
378
+ const { filePath: typeFilePath, templates } = typeFiles[targetId];
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;
256
384
  }
257
- typeContents.push(typeSettings.join(" |\n"));
258
- typeContents.push('>');
385
+ if (cfg.debug)
386
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting definition file for ${targetId}\n`));
259
387
  }
260
- else {
261
- typeContents.push(`export type ${displayTemplate.key}Settings = []`);
388
+ else if (cfg.debug)
389
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating definition file for ${targetId}\n`));
390
+ // Write Style definition
391
+ const imports = ['import type { LayoutProps } from "@remkoj/optimizely-cms-react/components"', 'import type { ReactNode } from "react"'];
392
+ const typeContents = [];
393
+ const props = [];
394
+ let typeId = targetId.split('/', 2)[1];
395
+ templates.forEach(({ file: displayTemplateFile, data: displayTemplate }) => {
396
+ const importPath = path.relative(path.dirname(typeFilePath), displayTemplateFile).replaceAll('\\', '/');
397
+ imports.push(`import type ${displayTemplate.key}Styles from "./${importPath}"`);
398
+ typeContents.push(`export type ${displayTemplate.key}Props = LayoutProps<typeof ${displayTemplate.key}Styles>`);
399
+ typeContents.push(`export type ${displayTemplate.key}ComponentProps<DT extends Record<string, any> = Record<string, any>> = {
400
+ data: DT
401
+ layoutProps: ${displayTemplate.key}Props | undefined
402
+ } & JSX.IntrinsicElements['div']`);
403
+ typeContents.push(`export type ${displayTemplate.key}Component<DT extends Record<string, any> = Record<string, any>> = (props: ${displayTemplate.key}ComponentProps<DT>) => ReactNode`);
404
+ typeContents.push('');
405
+ props.push(`${displayTemplate.key}Props`);
406
+ if (!typeId)
407
+ typeId = displayTemplate.nodeType ?? displayTemplate.baseType ?? displayTemplate.contentType;
408
+ });
409
+ if (typeId) {
410
+ typeId = ucFirst$1(typeId);
411
+ typeContents.push('');
412
+ typeContents.push(`export type ${typeId}LayoutProps = ${props.join(' | ')}
413
+ export type ${typeId}ComponentProps<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = {
414
+ data: DT
415
+ layoutProps: LP | undefined
416
+ } & JSX.IntrinsicElements['div']
417
+
418
+ export type ${typeId}Component<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = (props: ${typeId}ComponentProps<DT,LP>) => ReactNode`);
262
419
  }
263
- typeContents.push(`export type ${displayTemplate.key}LayoutProps = {`);
264
- typeContents.push(` template: "${displayTemplate.key}"`);
265
- typeContents.push(` settings: ${displayTemplate.key}Settings`);
266
- typeContents.push(`}`);
267
- typeId = displayTemplate.contentType ?? displayTemplate.baseType ?? displayTemplate.nodeType;
268
- typeId = typeId[0]?.toUpperCase() + typeId.substring(1) + "LayoutProps";
269
- });
270
- typeContents.push(`export type ${typeId} = ${templates.map(t => t.key + "LayoutProps").join(' | ')}`);
271
- typeContents.push(`export default ${typeId}`);
272
- fs.writeFileSync(typeFilePath, typeContents.join("\n"));
420
+ fs.writeFileSync(typeFilePath, imports.join("\n") + "\n\n" + typeContents.join("\n"));
421
+ }
273
422
  }
274
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated style definitions for ${updatedTemplates.join(', ')}\n`));
275
- process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
423
+ //#endregion
424
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + ` Created/updated style definitions for ${updatedTemplates.join(', ')}`)) + "\n");
276
425
  }
277
426
  };
427
+ function ucFirst$1(input) {
428
+ if (typeof (input) != 'string' || input.length < 1)
429
+ return input;
430
+ return input[0].toUpperCase() + input.substring(1);
431
+ }
278
432
 
279
433
  const TypesPullCommand = {
280
434
  command: "types:pull",
281
435
  describe: "Pull content type definition files into the project",
282
436
  builder: (yargs) => {
437
+ const newArgs = contentTypesBuilder(yargs);
283
438
  yargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
284
- yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
285
- yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
286
- return yargs;
439
+ return newArgs;
287
440
  },
288
441
  handler: async (args) => {
289
- const { _config: cfg, components: basePath, excludeBaseTypes, excludeTypes } = parseArgs(args);
290
- const client = createClient(cfg);
291
- const pageSize = 100;
292
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
293
- if (cfg.debug)
294
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
295
- let templatesPage = await client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
296
- const results = templatesPage.items ?? [];
297
- let pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
298
- while (pagesRemaining > 0 && results.length < templatesPage.totalItemCount) {
299
- if (cfg.debug)
300
- 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`));
301
- templatesPage = await client.contentTypes.contentTypesList(undefined, undefined, templatesPage.pageIndex + 1, templatesPage.pageSize);
302
- results.push(...templatesPage.items);
303
- pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
304
- }
305
- if (cfg.debug)
306
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
307
- if (!fs.existsSync(basePath))
308
- fs.mkdirSync(basePath, { recursive: true });
309
- if (!fs.statSync(basePath).isDirectory()) {
310
- process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The components path ${basePath} is not a folder\n`));
311
- process.exit(1);
312
- }
313
- const updatedTypes = results.map(contentType => {
314
- if (excludeBaseTypes.includes(contentType.baseType)) {
315
- if (cfg.debug)
316
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) - Base type excluded\n`));
317
- return undefined;
318
- }
319
- if (excludeTypes.includes(contentType.key)) {
320
- if (cfg.debug)
321
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) - Content type excluded\n`));
322
- return undefined;
323
- }
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 => {
324
446
  const typePath = path.join(basePath, contentType.baseType, contentType.key);
325
447
  const typeFile = path.join(typePath, `${contentType.key}.opti-type.json`);
326
448
  if (!fs.existsSync(typePath))
327
449
  fs.mkdirSync(typePath, { recursive: true });
328
- if (cfg.debug)
450
+ if (fs.existsSync(typeFile) && !force) {
451
+ if (debug)
452
+ process.stdout.write(chalk.yellow(`${figures.cross} Skipping type definition for ${contentType.displayName} (${contentType.key}) - File already exists\n`));
453
+ return contentType.key;
454
+ }
455
+ if (debug)
329
456
  process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key})\n`));
330
457
  fs.writeFileSync(typeFile, JSON.stringify(contentType, undefined, 2));
331
458
  return contentType.key;
332
459
  }).filter(x => x);
333
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated type definitions for ${updatedTypes.join(', ')}\n`));
334
- 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`)));
335
461
  }
336
462
  };
337
463
 
338
- const NextJsCreateCommand = {
339
- command: "nextjs:create",
340
- describe: "Scaffold a complete Next.JS / Optimizely Graph structure",
464
+ const TypesPushCommand = {
465
+ command: "types:push",
466
+ describe: "Push content type definition into Optimizely CMS (create / replace)",
341
467
  builder: (yargs) => {
342
468
  yargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
343
469
  yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
344
470
  yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
471
+ yargs.option("baseTypes", { alias: 'b', description: "Select only these base types", string: true, type: 'array', demandOption: false, default: [] });
472
+ yargs.option("types", { alias: 't', description: "Select only these types", string: true, type: 'array', demandOption: false, default: [] });
345
473
  return yargs;
346
474
  },
347
475
  handler: async (args) => {
348
- const { _config: cfg, components: basePath, excludeBaseTypes, excludeTypes, force } = parseArgs(args);
476
+ const { _config: cfg, components: basePath, excludeBaseTypes, excludeTypes, baseTypes, types, force } = parseArgs(args);
349
477
  const client = createClient(cfg);
350
- const pageSize = 100;
351
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
352
- if (cfg.debug)
353
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
354
- let templatesPage = await client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
355
- const results = templatesPage.items ?? [];
356
- let pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
357
- while (pagesRemaining > 0 && results.length < templatesPage.totalItemCount) {
358
- if (cfg.debug)
359
- 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`));
360
- templatesPage = await client.contentTypes.contentTypesList(undefined, undefined, templatesPage.pageIndex + 1, templatesPage.pageSize);
361
- results.push(...templatesPage.items);
362
- pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
363
- }
364
- if (cfg.debug)
365
- process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
366
- if (!fs.existsSync(basePath))
367
- fs.mkdirSync(basePath, { recursive: true });
368
- if (!fs.statSync(basePath).isDirectory()) {
369
- process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The components path ${basePath} is not a folder\n`));
370
- process.exit(1);
371
- }
372
- // Apply content type filters
373
- if (cfg.debug)
374
- process.stdout.write(chalk.gray(`${figures.arrowRight} Applying content type filters\n`));
375
- const contentTypes = results.filter(contentType => {
376
- if (contentType.source == 'system') {
377
- if (cfg.debug)
378
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) - Internal CMS type\n`));
379
- return false;
380
- }
381
- const baseType = contentType.baseType ?? 'default';
382
- if (excludeBaseTypes.includes(baseType)) {
383
- if (cfg.debug)
384
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) - Base type excluded\n`));
385
- return false;
386
- }
387
- if (excludeTypes.includes(contentType.key)) {
388
- if (cfg.debug)
389
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) - Content type excluded\n`));
390
- return false;
391
- }
392
- return true;
478
+ // Find all type files
479
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/replace) Content Types into Optimizely CMS\n`));
480
+ const typeDefinitionFiles = await glob("./**/*.opti-type.json", { cwd: basePath });
481
+ // Read & filter all identified type files
482
+ const typeDefinitions = typeDefinitionFiles.map(file => {
483
+ return {
484
+ file: file,
485
+ definition: JSON.parse(fs.readFileSync(path.join(basePath, file), { encoding: 'utf-8' }).toString())
486
+ };
487
+ }).filter(data => {
488
+ return (!excludeBaseTypes.includes(data.definition.baseType)) &&
489
+ (!excludeTypes.includes(data.definition.key)) &&
490
+ (!baseTypes || baseTypes.length == 0 || baseTypes.includes(data.definition.baseType)) &&
491
+ (!types || types.length == 0 || types.includes(data.definition.key));
393
492
  });
394
- if (cfg.debug)
395
- process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${contentTypes.length} items\n`));
396
- // Create the type specific
397
- const updatedTypes = contentTypes.map(contentType => {
398
- const baseType = contentType.baseType ?? 'default';
399
- // Create the type folder
400
- const typePath = path.join(basePath, baseType, contentType.key);
401
- if (!fs.existsSync(typePath))
402
- fs.mkdirSync(typePath, { recursive: true });
403
- // Create fragments
404
- createGraphFragments(contentType, typePath, basePath, force, cfg, contentTypes);
405
- // Create component
406
- createComponent(contentType, typePath, force, cfg);
407
- return contentType.key;
408
- }).filter(x => x);
409
- createFactory(contentTypes, basePath, force, cfg);
410
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated type definitions for ${updatedTypes.join(', ')}\n`));
493
+ // Output selected types
494
+ const results = await Promise.all(typeDefinitions.map(type => {
495
+ process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Pushing ${type.definition.displayName} from ${type.file}\n`));
496
+ return client.contentTypes.contentTypesPut(type.definition.key, type.definition, force)
497
+ .then(ct => { return { key: type.definition.key, type: ct, file: type.file }; })
498
+ .catch(e => { return { key: type.definition.key, type: type.definition, file: type.file, error: e }; });
499
+ }));
500
+ const overview = new Table({
501
+ head: [
502
+ chalk.yellow(chalk.bold("Name")),
503
+ chalk.yellow(chalk.bold("Key")),
504
+ chalk.yellow(chalk.bold("Status")),
505
+ chalk.yellow(chalk.bold("Message"))
506
+ ],
507
+ colWidths: [31, 20, 9, 20],
508
+ colAligns: ["left", "left", "center", "left"]
509
+ });
510
+ results.forEach(item => {
511
+ overview.push([
512
+ item.type.displayName,
513
+ item.key,
514
+ item.error ? chalk.redBright(chalk.bold(figures.cross)) : chalk.green(chalk.bold(figures.tick)),
515
+ item.error ?? ''
516
+ ]);
517
+ });
518
+ process.stdout.write(overview.toString() + "\n");
519
+ // Mark us as done
411
520
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
412
521
  }
413
522
  };
414
- function createFactory(contentTypes, basePath, force, cfg) {
415
- // Get the list of all base types
416
- const baseTypes = contentTypes.map(x => x.baseType).concat(['default']).filter((x, i, a) => x && !a.slice(0, i).includes(x)).sort();
417
- const baseTypeFactories = baseTypes.map(baseType => {
418
- // Get the list of inheriting types and return if it's empty
419
- const inheritingTypes = contentTypes.filter(x => x.baseType == baseType);
420
- if (inheritingTypes.length == 0)
421
- return;
422
- if (cfg.debug)
423
- process.stdout.write(chalk.gray(`${figures.arrowRight} Factory for ${ucFirst(baseType)} will contain these components: ${inheritingTypes.map(x => x.displayName ?? x.key).join(", ")}\n`));
424
- // Check the file presence and if we should overwrite
425
- const baseTypeFactory = path.join(basePath, baseType, 'index.ts');
426
- if (fs.existsSync(baseTypeFactory)) {
427
- if (cfg.debug)
428
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${ucFirst(baseType)} factory\n`));
523
+
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;
528
+ };
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`));
429
538
  }
430
- // Build the actual factory
431
- const lines = [
432
- '// Auto generated dictionary',
433
- 'import { ComponentTypeDictionary } from "@remkoj/optimizely-cms-react";'
434
- ];
435
- inheritingTypes.forEach(type => {
436
- lines.push(`import ${type.key} from "./${type.key}";`);
437
- });
438
- lines.push('');
439
- lines.push(`export const ${baseType}Dictionary : ComponentTypeDictionary = [`);
440
- inheritingTypes.forEach(type => {
441
- lines.push(' {', ` type: '${type.key}',`, ` component: ${type.key}`, ' },');
442
- });
443
- lines.push(']', '', `export default ${baseType}Dictionary`);
444
- fs.writeFileSync(baseTypeFactory, lines.join("\n"));
445
- return baseType;
446
- }).filter(x => x);
447
- const cmsFactoryFile = path.join(basePath, 'index.ts');
448
- if (fs.existsSync(cmsFactoryFile)) {
449
- if (cfg.debug)
450
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting CMS Components factory\n`));
451
- }
452
- const lines = [
453
- '// Auto generated dictionary',
454
- 'import { ComponentTypeDictionary } from "@remkoj/optimizely-cms-react";'
455
- ];
456
- baseTypeFactories.forEach(type => {
457
- lines.push(`import ${type}Components from "./${type}";`);
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
+ };
458
546
  });
459
- lines.push('');
460
- baseTypeFactories.forEach(type => {
461
- lines.push(`prefixDictionaryEntries(${type}Components, '${ucFirst(type)}');`);
462
- });
463
- lines.push('');
464
- lines.push('export const cmsComponentDictionary : ComponentTypeDictionary = [');
465
- baseTypeFactories.forEach(type => {
466
- lines.push(` ...${type}Components,`);
467
- });
468
- lines.push(']', '', `export default cmsComponentDictionary`, `function prefixDictionaryEntries(list: ComponentTypeDictionary, prefix: string) : ComponentTypeDictionary
469
- {
470
- list.forEach((component, idx, dictionary) => {
471
- dictionary[idx].type = typeof component.type == 'string' ? prefix + "/" + component.type : [ prefix, ...component.type ]
472
- })
473
- return list
474
- }`);
475
- fs.writeFileSync(cmsFactoryFile, lines.join("\n"));
547
+ return folders;
476
548
  }
477
- function createComponent(contentType, typePath, force, cfg) {
478
- const componentFile = path.join(typePath, 'index.tsx');
479
- if (fs.existsSync(componentFile)) {
480
- if (force) {
481
- if (cfg.debug)
482
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) component\n`));
483
- }
484
- else {
485
- if (cfg.debug)
486
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) component - file already exists\n`));
487
- return undefined;
488
- }
489
- }
490
- const varName = `${contentType.key}${ucFirst(contentType.baseType ?? 'part')}`;
491
- const component = `import { CmsComponent } from "@remkoj/optimizely-cms-react";
492
- import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";
493
-
494
- export const ${varName} : CmsComponent<${contentType.key}DataFragment> = ({ data }) => {
495
- const componentName = '${contentType.displayName}'
496
- const componentInfo = '${contentType.description ?? ''}'
497
- return <div className="mx-auto px-2 container">
498
- <div>{ componentName }</div>
499
- <div>{ componentInfo }</div>
500
- <pre className="w-full overflow-x-hidden font-mono text-sm">{ JSON.stringify(data, undefined, 4) }</pre>
501
- </div>
502
- }
503
- ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
504
-
505
- export default ${varName}`;
506
- fs.writeFileSync(componentFile, component);
549
+ function getTypeFolder(list, type) {
550
+ return list.filter(x => x.type == type).at(0)?.path;
507
551
  }
508
- function ucFirst(current) {
509
- return current[0]?.toUpperCase() + current.substring(1);
510
- }
511
- 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 = [];
512
580
  const baseType = contentType.baseType ?? 'default';
513
581
  const baseQueryFile = path.join(typePath, `${contentType.key}.${baseType}.graphql`);
514
582
  if (fs.existsSync(baseQueryFile)) {
515
583
  if (force) {
516
- if (cfg.debug)
584
+ if (debug)
517
585
  process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
518
586
  }
519
587
  else {
520
- if (cfg.debug)
588
+ if (debug)
521
589
  process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
522
590
  return undefined;
523
591
  }
524
592
  }
593
+ else if (debug) {
594
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
595
+ }
525
596
  const { fragment, propertyTypes } = createInitialFragment(contentType);
526
597
  fs.writeFileSync(baseQueryFile, fragment);
598
+ returnValue.push(contentType.key);
527
599
  let dependencies = Array.isArray(propertyTypes) ? [...propertyTypes] : [];
528
600
  while (Array.isArray(dependencies) && dependencies.length > 0) {
529
601
  let newDependencies = [];
@@ -538,15 +610,18 @@ function createGraphFragments(contentType, typePath, basePath, force, cfg, conte
538
610
  if (!fs.existsSync(propertyFragmentDir))
539
611
  fs.mkdirSync(propertyFragmentDir, { recursive: true });
540
612
  if (!fs.existsSync(propertyFragmentFile) || force) {
541
- 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`));
542
615
  const propContentTypeInfo = createInitialFragment(propContentType, true);
543
616
  fs.writeFileSync(propertyFragmentFile, propContentTypeInfo.fragment);
617
+ returnValue.push(propContentType.key);
544
618
  if (Array.isArray(propContentTypeInfo.propertyTypes))
545
619
  newDependencies.push(...propContentTypeInfo.propertyTypes);
546
620
  }
547
621
  });
548
622
  dependencies = newDependencies;
549
623
  }
624
+ return returnValue.length > 0 ? returnValue : undefined;
550
625
  }
551
626
  function createInitialFragment(contentType, forProperty = false) {
552
627
  const propertyTypes = [];
@@ -604,6 +679,8 @@ function createInitialFragment(contentType, forProperty = false) {
604
679
  break;
605
680
  }
606
681
  });
682
+ if (contentType.baseType == "experience")
683
+ fragmentFields.push('...ExperienceData');
607
684
  if (fragmentFields.length == 0)
608
685
  fragmentFields.push('_metadata { key }');
609
686
  const tpl = `fragment ${contentType.key}${forProperty ? 'Property' : ''}Data on ${contentType.key}${forProperty ? 'Property' : ''} {
@@ -615,6 +692,499 @@ function createInitialFragment(contentType, forProperty = false) {
615
692
  };
616
693
  }
617
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
+
618
1188
  const CmsVersionCommand = {
619
1189
  command: "cms:version",
620
1190
  describe: "Get the CMS Version information",
@@ -641,16 +1211,22 @@ const CmsVersionCommand = {
641
1211
  }
642
1212
  };
643
1213
 
1214
+ // Style processing
644
1215
  const commands = [
645
1216
  StylesPushCommand,
646
1217
  StylesListCommand,
647
1218
  StylesPullCommand,
648
1219
  TypesPullCommand,
1220
+ TypesPushCommand,
649
1221
  NextJsCreateCommand,
1222
+ NextJsQueriesCommand,
1223
+ NextJsComponentsCommand,
1224
+ NextJsVisualBuilderCommand,
1225
+ NextJsFactoryCommand,
650
1226
  CmsVersionCommand
651
1227
  ];
652
1228
 
653
- var version = "2.0.0-pre5";
1229
+ var version = "2.0.1-pre1";
654
1230
  var name = "opti-cms";
655
1231
  var APP = {
656
1232
  version: version,