@remkoj/optimizely-cms-cli 5.1.7 → 5.3.0

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
@@ -3,11 +3,12 @@ import dotenv from 'dotenv';
3
3
  import { expand } from 'dotenv-expand';
4
4
  import path from 'node:path';
5
5
  import fs from 'node:fs';
6
- import { getCmsIntegrationApiConfigFromEnvironment, createClient, OptiCmsVersion, IntegrationApi, ContentRoots } from '@remkoj/optimizely-cms-api';
6
+ import { getCmsIntegrationApiConfigFromEnvironment, createClient, ContentBaseType, OptiCmsVersion, PropertyDataType, ContentRoots } from '@remkoj/optimizely-cms-api';
7
7
  import yargs from 'yargs';
8
8
  import chalk from 'chalk';
9
9
  import figures from 'figures';
10
10
  import Table from 'cli-table3';
11
+ import { diff } from 'deep-object-diff';
11
12
  import fsAsync from 'node:fs/promises';
12
13
  import { input, select, confirm } from '@inquirer/prompts';
13
14
 
@@ -15,7 +16,7 @@ function getProjectDir() {
15
16
  let testPath = process.cwd();
16
17
  let hasPackageJson = false;
17
18
  do {
18
- hasPackageJson = fs.statSync('/' + path.join(testPath, 'package.json'), { throwIfNoEntry: false })?.isFile() ?? false;
19
+ hasPackageJson = fs.statSync(path.join(testPath, 'package.json'), { throwIfNoEntry: false })?.isFile() ?? false;
19
20
  if (!hasPackageJson)
20
21
  testPath = path.normalize(path.join(testPath, '..'));
21
22
  } while (!hasPackageJson && testPath.length > 2);
@@ -31,7 +32,7 @@ function getProjectDir() {
31
32
  * @returns A string array with the files processed
32
33
  */
33
34
  function prepare(pDir) {
34
- const projectDir = getProjectDir();
35
+ const projectDir = pDir ?? getProjectDir();
35
36
  const envFiles = globSync(".env*", { cwd: projectDir })
36
37
  .sort((a, b) => b.length - a.length)
37
38
  .filter(n => n == ".env" || n == ".env.local" || (process.env.NODE_ENV && n == `.env.${process.env.NODE_ENV}.local`));
@@ -79,7 +80,7 @@ function createOptiCmsApp(scriptName, version, epilogue, envFiles, projectDir) {
79
80
  console.error(msg + "\n");
80
81
  if (error)
81
82
  console.error(`[${chalk.bold(error.name ?? 'Error')}]: ${error.message ?? ''}\n`);
82
- args.showHelp("error");
83
+ //args.showHelp("error")
83
84
  });
84
85
  }
85
86
  function isDemanded(value) {
@@ -138,79 +139,6 @@ function createCmsClient(args) {
138
139
  return client;
139
140
  }
140
141
 
141
- const StylesPushCommand = {
142
- command: "styles:push",
143
- describe: "Push Visual Builder style definitions into the CMS (create/replace)",
144
- builder: (yargs) => {
145
- yargs.option('excludeTemplates', { alias: 'e', description: "Exclude these templates", string: true, type: 'array', demandOption: false, default: [] });
146
- yargs.option("templates", { alias: 't', description: "Select only these templates", string: true, type: 'array', demandOption: false, default: [] });
147
- return yargs;
148
- },
149
- handler: async (args) => {
150
- const { _config: cfg, excludeTemplates, templates, ...opts } = parseArgs(args);
151
- const client = createCmsClient(args);
152
- if (client.runtimeCmsVersion == OptiCmsVersion.CMS12) {
153
- process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
154
- return;
155
- }
156
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/replace) DisplayStyles into Optimizely CMS\n`));
157
- const styleDefinitionFiles = await glob("./**/*.opti-style.json", {
158
- cwd: opts.components
159
- });
160
- const results = (await Promise.all(styleDefinitionFiles.map(async (styleDefinitionFile) => {
161
- const filePath = path.normalize(path.join(opts.components, styleDefinitionFile));
162
- const styleDefinition = tryReadJsonFile(filePath, cfg.debug);
163
- const styleKey = styleDefinition.key;
164
- if (!styleKey) {
165
- 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`));
166
- return undefined;
167
- }
168
- if (excludeTemplates.includes(styleKey))
169
- return undefined; // Skip excluded styles
170
- if (templates.length > 0 && !templates.includes(styleKey))
171
- return undefined; // Only include defined styles, if any
172
- if (cfg.debug)
173
- process.stdout.write(chalk.gray(`${figures.arrowRight} Pushing: ${styleKey}\n`));
174
- const newTemplate = await client.displayTemplates.displayTemplatesPut(styleKey, styleDefinition);
175
- return newTemplate;
176
- }))).filter(isNotNullOrUndefined);
177
- const styles = new Table({
178
- head: [
179
- chalk.yellow(chalk.bold("Name")),
180
- chalk.yellow(chalk.bold("Key")),
181
- chalk.yellow(chalk.bold("Default")),
182
- chalk.yellow(chalk.bold("Target"))
183
- ],
184
- colWidths: [31, 20, 9, 20],
185
- colAligns: ["left", "left", "center", "left"]
186
- });
187
- results.forEach(tpl => {
188
- styles.push([
189
- tpl.displayName,
190
- tpl.key,
191
- tpl.isDefault ? figures.tick : figures.cross,
192
- tpl.contentType ? `${tpl.contentType} (C)` : tpl.baseType ? `${tpl.baseType} (B)` : `${tpl.nodeType} (N)`
193
- ]);
194
- });
195
- process.stdout.write(styles.toString() + "\n");
196
- process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
197
- }
198
- };
199
- function tryReadJsonFile(filePath, debug = false) {
200
- try {
201
- if (debug)
202
- process.stdout.write(chalk.gray(`${figures.arrowRight} Reading style definition from ${filePath}\n`));
203
- return JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' }));
204
- }
205
- catch (e) {
206
- process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} Error while reading ${filePath}\n`));
207
- }
208
- return undefined;
209
- }
210
- function isNotNullOrUndefined(i) {
211
- return i ? true : false;
212
- }
213
-
214
142
  const ContentTypesArgsDefaults = {
215
143
  excludeBaseTypes: [],
216
144
  excludeTypes: ['folder', 'media', 'image', 'video'],
@@ -250,7 +178,7 @@ async function getContentTypes(client, args, pageSize = 100, allowSystem = false
250
178
  process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
251
179
  process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Content-Types based upon arguments\n`));
252
180
  }
253
- const validBaseTypes = getEnumOptions(IntegrationApi.ContentBaseType).map(x => x.toLowerCase());
181
+ const validBaseTypes = getEnumOptions(ContentBaseType).map(x => x.toLowerCase());
254
182
  if (cfg.debug)
255
183
  process.stdout.write(`Allowing base types: ${validBaseTypes.join(', ')}\n`);
256
184
  const allContentTypes = all ?
@@ -409,6 +337,269 @@ async function getStyleFilePath(definition, opts) {
409
337
  throw new Error(`Unable to resolve the target for the DisplayTemplate: ${definition.key}`);
410
338
  }
411
339
 
340
+ function normalizeMergePatch(value, omitKeys, requiredKeys = [], requiredSource) {
341
+ if (value === undefined || value === null)
342
+ return null;
343
+ if (Array.isArray(value))
344
+ return value.map(item => normalizeMergePatch(item, omitKeys));
345
+ if (typeof value !== 'object')
346
+ return value;
347
+ const output = Object.entries(value).reduce((acc, [key, entry]) => {
348
+ if (!omitKeys.includes(key))
349
+ acc[key] = normalizeMergePatch(entry, omitKeys);
350
+ return acc;
351
+ }, {});
352
+ const withRequiredKeys = requiredKeys.reduce((acc, key) => {
353
+ if (requiredSource && !Object.keys(acc).includes(key))
354
+ acc[key] = requiredSource[key];
355
+ return acc;
356
+ }, output);
357
+ return withRequiredKeys;
358
+ }
359
+ /**
360
+ * Computes a JSON Merge Patch (RFC 7396) that transforms `currentValue` into `newValue`.
361
+ *
362
+ * The resulting patch can be sent directly as the request body of an `application/merge-patch+json`
363
+ * request. Changed and added properties are included with their new value; removed properties are
364
+ * included with a `null` value (as required by RFC 7396); unchanged properties are omitted entirely.
365
+ *
366
+ * @see https://datatracker.ietf.org/doc/html/rfc7396
367
+ *
368
+ * @example Basic usage
369
+ * ```ts
370
+ * const current = { name: 'Alice', age: 30, role: 'user' }
371
+ * const updated = { name: 'Alice', age: 31 }
372
+ *
373
+ * generatePatch(current, updated)
374
+ * // => { age: 31, role: null }
375
+ * // `age` is replaced, `role` is removed (null), `name` is unchanged (omitted)
376
+ * ```
377
+ *
378
+ * @example Excluding keys from the patch
379
+ * ```ts
380
+ * const current = { id: '123', name: 'Alice', version: 1 }
381
+ * const updated = { id: '123', name: 'Bob', version: 2 }
382
+ *
383
+ * generatePatch(current, updated, { readonlyFields: ['id', 'version'] })
384
+ * // => { name: 'Bob' }
385
+ * // `id` and `version` are excluded even though they are present in the diff
386
+ * ```
387
+ *
388
+ * @example Always include required fields
389
+ * ```ts
390
+ * const current = { id: '123', name: 'Alice', version: 1 }
391
+ * const updated = { id: '123', name: 'Alice', version: 2 }
392
+ *
393
+ * generatePatch(current, updated, { requiredFields: ['id'] })
394
+ * // => { id: '123', version: 2 }
395
+ * // `id` is included even though it did not change
396
+ * ```
397
+ *
398
+ * @example Nested objects
399
+ * ```ts
400
+ * const current = { address: { city: 'Amsterdam', zip: '1000AA' } }
401
+ * const updated = { address: { city: 'Utrecht' } }
402
+ *
403
+ * generatePatch(current, updated)
404
+ * // => { address: { city: 'Utrecht', zip: null } }
405
+ * ```
406
+ *
407
+ * @param currentValue - The current state of the resource.
408
+ * @param newValue - The desired state of the resource.
409
+ * @param options - Optional configuration.
410
+ * @param options.readonlyFields - Keys that should never appear in the generated patch,
411
+ * regardless of whether they changed.
412
+ * @param options.requiredFields - Keys that should always appear in the generated patch,
413
+ * using values from `newValue`.
414
+ * @returns A {@link MergePatch} object ready to be serialised as an `application/merge-patch+json` body.
415
+ */
416
+ function generatePatch(currentValue, newValue, options = {}) {
417
+ const requiredKeys = options.requiredFields ?? [];
418
+ const omitKeys = (options.readonlyFields ?? []).filter(key => !requiredKeys.includes(key));
419
+ const patch = diff(currentValue, newValue);
420
+ return normalizeMergePatch(patch, omitKeys, requiredKeys, newValue);
421
+ }
422
+ /**
423
+ * Extracts all field paths from a {@link MergePatch} as dot-separated strings.
424
+ * Nested objects are flattened recursively; leaf values (including `null`) produce a path entry.
425
+ *
426
+ * @example
427
+ * ```ts
428
+ * getPatchFields({ a: { b: 0 }, c: 'value' })
429
+ * // => ['a.b', 'c']
430
+ * ```
431
+ *
432
+ * @param patch - The merge patch to extract fields from.
433
+ * @param prefix - Internal prefix used during recursion; omit when calling directly.
434
+ * @returns An array of dot-separated field path strings.
435
+ */
436
+ function getPatchFields(patch, prefix = '') {
437
+ return Object.entries(patch).flatMap(([key, value]) => {
438
+ const path = prefix ? `${prefix}.${key}` : key;
439
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
440
+ ? getPatchFields(value, path)
441
+ : [path];
442
+ });
443
+ }
444
+
445
+ /**
446
+ * CLI command that synchronizes local `.opti-style.json` definitions with Optimizely CMS.
447
+ *
448
+ * Existing styles are patched, missing styles are created, and command options allow
449
+ * selecting or excluding template keys during the push.
450
+ */
451
+ const StylesPushCommand = {
452
+ command: 'styles:push',
453
+ describe: 'Push Visual Builder style definitions into the CMS (create/patch)',
454
+ builder: (yargs) => {
455
+ yargs.option('excludeTemplates', {
456
+ alias: 'e',
457
+ description: 'Exclude these templates',
458
+ string: true,
459
+ type: 'array',
460
+ demandOption: false,
461
+ default: [],
462
+ });
463
+ yargs.option('templates', {
464
+ alias: 't',
465
+ description: 'Select only these templates',
466
+ string: true,
467
+ type: 'array',
468
+ demandOption: false,
469
+ default: [],
470
+ });
471
+ return yargs;
472
+ },
473
+ handler: async (args) => {
474
+ const { _config: { debug = false }, excludeTemplates, templates, ...opts } = parseArgs(args);
475
+ const client = createCmsClient(args);
476
+ if (client.runtimeCmsVersion == OptiCmsVersion.CMS12) {
477
+ process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
478
+ return;
479
+ }
480
+ const { styles: displayTemplates } = await getStyles(client, {
481
+ // Defaults
482
+ all: true,
483
+ baseTypes: [],
484
+ excludeBaseTypes: [],
485
+ excludeNodeTypes: [],
486
+ excludeTemplates: [],
487
+ excludeTypes: [],
488
+ nodes: [],
489
+ templates: [],
490
+ templateTypes: [],
491
+ types: [],
492
+ // Arguments from this method
493
+ ...args,
494
+ }, 50);
495
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/patch) DisplayStyles into Optimizely CMS\n`));
496
+ // Listing all style files
497
+ const styleDefinitionFiles = await glob('./**/*.opti-style.json', {
498
+ cwd: opts.components,
499
+ });
500
+ // Helper function to read the files from disk
501
+ function readStyleFile(styleDefinitionFile) {
502
+ const filePath = path.normalize(path.join(opts.components, styleDefinitionFile));
503
+ const styleDefinition = tryReadJsonFile(filePath, debug);
504
+ return styleDefinition ? {
505
+ styleKey: styleDefinition.key,
506
+ styleDefinition,
507
+ styleFile: filePath
508
+ } : undefined;
509
+ }
510
+ // Helper function to filter the files read from disk
511
+ function filterStyleDefinition(data) {
512
+ if (!data || !data.styleKey)
513
+ return false;
514
+ const { styleKey } = data;
515
+ if (excludeTemplates.includes(styleKey)) {
516
+ if (debug)
517
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${styleKey} - explicitly excluded\n`));
518
+ return false;
519
+ }
520
+ // Skip if there a selectin and this template is not in it
521
+ if (templates.length > 0 && !templates.includes(styleKey)) {
522
+ if (debug)
523
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${styleKey} - not in selected list of templates\n`));
524
+ return false; // Only include defined styles, if any
525
+ }
526
+ return true;
527
+ }
528
+ // Processing each file
529
+ const results = await Promise.allSettled(styleDefinitionFiles
530
+ .map(readStyleFile)
531
+ .filter(filterStyleDefinition)
532
+ .map(async ({ styleKey, styleDefinition, styleFile }) => {
533
+ const displayTemplate = displayTemplates.find(dt => dt.key === styleKey);
534
+ // Confirm we're including
535
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} ${displayTemplate ? 'Updating' : 'Creating'} ${styleKey}\n`));
536
+ if (debug)
537
+ process.stdout.write(chalk.gray(`${figures.arrowRight} from ${styleFile || 'unknown source'}\n`));
538
+ // Patch or create the template
539
+ const newTemplate = await (displayTemplate ? (async () => {
540
+ const patch = generatePatch(displayTemplate, styleDefinition, {
541
+ readonlyFields: ['key', 'created', 'createdBy', 'lastModified', 'lastModifiedBy'],
542
+ });
543
+ if (!path || Object.entries(patch).length === 0)
544
+ return displayTemplate;
545
+ // @ts-expect-error: We're expecting an error here due to the CMS Logic not matching
546
+ // the OpenAPI Specification.
547
+ return client.displayTemplates.displayTemplatesPatch(styleKey, patch);
548
+ })() : client.displayTemplates.displayTemplatesCreate(displayTemplate));
549
+ // Validate the result
550
+ const unpatchedFields = generatePatch(newTemplate, styleDefinition, {
551
+ readonlyFields: ['key', 'created', 'createdBy', 'lastModified', 'lastModifiedBy']
552
+ });
553
+ if (unpatchedFields && Object.entries(unpatchedFields).length > 0)
554
+ throw new Error(`Creating/patching of displayTemplate failed, the following fields failed: ${getPatchFields(unpatchedFields).join('; ')}`);
555
+ // Return the template after Create/Patch
556
+ return newTemplate;
557
+ }));
558
+ const styles = new Table({
559
+ head: [
560
+ chalk.yellow(chalk.bold('Name')),
561
+ chalk.yellow(chalk.bold('Key')),
562
+ chalk.yellow(chalk.bold('Default')),
563
+ chalk.yellow(chalk.bold('Target')),
564
+ ],
565
+ colWidths: [31, 20, 9, 20],
566
+ colAligns: ['left', 'left', 'center', 'left'],
567
+ });
568
+ results.forEach((result) => {
569
+ if (result.status === 'fulfilled') {
570
+ const tpl = result.value;
571
+ styles.push([
572
+ tpl.displayName,
573
+ tpl.key,
574
+ tpl.isDefault ? figures.tick : figures.cross,
575
+ tpl.contentType
576
+ ? `${tpl.contentType} (C)`
577
+ : tpl.baseType
578
+ ? `${tpl.baseType} (B)`
579
+ : `${tpl.nodeType} (N)`,
580
+ ]);
581
+ }
582
+ else {
583
+ const error = result.reason;
584
+ process.stderr.write(`Error creating/updating style: ${error}`);
585
+ }
586
+ });
587
+ process.stdout.write(styles.toString() + '\n');
588
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + ' Done')) + '\n');
589
+ },
590
+ };
591
+ function tryReadJsonFile(filePath, debug = false) {
592
+ try {
593
+ if (debug)
594
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Reading style definition from ${filePath}\n`));
595
+ return JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' }));
596
+ }
597
+ catch (e) {
598
+ process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} Error while reading ${filePath}\n`));
599
+ }
600
+ return undefined;
601
+ }
602
+
412
603
  const StylesListCommand = {
413
604
  command: "styles:list",
414
605
  describe: "List Visual Builder style definitions from the CMS",
@@ -864,7 +1055,7 @@ const TypesPullCommand = {
864
1055
 
865
1056
  const TypesPushCommand = {
866
1057
  command: "types:push",
867
- describe: "Push content type definition into Optimizely CMS (create / replace)",
1058
+ describe: "Push content type definition into Optimizely CMS (create / patch)",
868
1059
  builder: (yargs) => {
869
1060
  yargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
870
1061
  yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
@@ -877,7 +1068,7 @@ const TypesPushCommand = {
877
1068
  const { _config: cfg, components: basePath, excludeBaseTypes, excludeTypes, baseTypes, types, force } = parseArgs(args);
878
1069
  const client = createClient(cfg);
879
1070
  // Find all type files
880
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/replace) Content Types into Optimizely CMS\n`));
1071
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/patch) Content Types into Optimizely CMS\n`));
881
1072
  const typeDefinitionFiles = await glob("./**/*.opti-type.json", { cwd: basePath });
882
1073
  // Read & filter all identified type files
883
1074
  const typeDefinitions = typeDefinitionFiles.map(file => {
@@ -907,7 +1098,7 @@ const TypesPushCommand = {
907
1098
  delete outType.features;
908
1099
  if (outType.usage)
909
1100
  delete outType.usage;
910
- return client.contentTypes.contentTypesPut(outType.key, outType, force)
1101
+ return client.contentTypes.contentTypesPatch(outType.key, outType, force)
911
1102
  .then(ct => { return { key: type.definition.key, type: ct, file: type.file }; })
912
1103
  .catch(e => { return { key: type.definition.key, type: type.definition, file: type.file, error: e }; });
913
1104
  }));
@@ -963,40 +1154,55 @@ function createTypeFolders(contentTypes, basePath, debug = false) {
963
1154
  function getTypeFolder(list, type) {
964
1155
  return list.filter(x => x.type == type).at(0)?.path;
965
1156
  }
1157
+ const PROPS_LOCK_FILE = '.opti-props.lock';
1158
+ function getGeneratedProps(appPath) {
1159
+ const lockPath = path.join(appPath, PROPS_LOCK_FILE);
1160
+ try {
1161
+ const lockData = JSON.parse(fs.readFileSync(lockPath).toString());
1162
+ const generatedProps = Array.isArray(lockData) ? lockData.map(x => { return { propName: x.propertyName, propType: x.propertyType }; }) : [];
1163
+ return generatedProps;
1164
+ }
1165
+ catch (e) {
1166
+ return [];
1167
+ }
1168
+ }
1169
+ function writeGeneratedProps(appPath, generatedProps) {
1170
+ const lockPath = path.join(appPath, PROPS_LOCK_FILE);
1171
+ fs.writeFileSync(lockPath, JSON.stringify(generatedProps.map(x => { return { propertyName: x.propName, propertyType: x.propType }; }), undefined, 2));
1172
+ }
966
1173
 
967
- /**
968
- * Keep track of all generated properties
969
- */
970
- let generatedProps = [];
971
1174
  const NextJsFragmentsCommand = {
972
1175
  command: "nextjs:fragments",
973
1176
  describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
974
1177
  builder,
975
1178
  handler: async (args, opts) => {
976
- generatedProps = [];
977
1179
  // Prepare
978
1180
  const { loadedContentTypes, createdTypeFolders } = opts || {};
979
- const { components: basePath, _config: { debug }, force } = parseArgs(args);
1181
+ const { components: basePath, _config: { debug }, force, path: appPath } = parseArgs(args);
980
1182
  const client = createCmsClient(args);
1183
+ // Get locked property names
1184
+ const generatedProps = getGeneratedProps(appPath);
1185
+ // Get content types
981
1186
  const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
982
1187
  // Start process
983
1188
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments for ${contentTypes.map(x => x.key).join(', ')}\n`));
984
1189
  const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
985
1190
  const updatedTypes = contentTypes.map(contentType => {
986
1191
  const typePath = getTypeFolder(typeFolders, contentType.key);
987
- return createGraphFragments(contentType, typePath, basePath, force, debug, allContentTypes, client.runtimeCmsVersion == OptiCmsVersion.CMS12);
1192
+ return createGraphFragments(contentType, typePath, basePath, force, debug, allContentTypes, generatedProps, client.runtimeCmsVersion == OptiCmsVersion.CMS12);
988
1193
  }).filter(x => x).flat();
989
1194
  // Report outcome
990
1195
  if (updatedTypes.length > 0)
991
1196
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
992
1197
  else
993
1198
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
1199
+ // Write updated lock
1200
+ writeGeneratedProps(appPath, generatedProps);
994
1201
  if (!opts)
995
1202
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
996
- generatedProps = [];
997
1203
  }
998
1204
  };
999
- function createGraphFragments(contentType, typePath, basePath, force, debug, contentTypes, forCms12 = false) {
1205
+ function createGraphFragments(contentType, typePath, basePath, force, debug, contentTypes, generatedProps = [], forCms12 = false) {
1000
1206
  const returnValue = [];
1001
1207
  const baseType = contentType.baseType ?? 'default';
1002
1208
  const baseQueryFile = path.join(typePath, `${contentType.key.split(':').pop()}.${baseType}.graphql`);
@@ -1015,7 +1221,7 @@ function createGraphFragments(contentType, typePath, basePath, force, debug, con
1015
1221
  else if (debug) {
1016
1222
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
1017
1223
  }
1018
- const { fragment, propertyTypes } = createInitialFragment(contentType, false, undefined, forCms12);
1224
+ const { fragment, propertyTypes } = createInitialFragment(contentType, false, undefined, generatedProps, forCms12);
1019
1225
  fs.writeFileSync(baseQueryFile, fragment);
1020
1226
  returnValue.push(contentType.key);
1021
1227
  let dependencies = Array.isArray(propertyTypes) ? [...propertyTypes] : [];
@@ -1035,7 +1241,7 @@ function createGraphFragments(contentType, typePath, basePath, force, debug, con
1035
1241
  if (!fs.existsSync(propertyFragmentFile) || force) {
1036
1242
  if (debug)
1037
1243
  process.stdout.write(chalk.gray(`${figures.arrowRight} Writing ${propContentType.displayName} (${propContentType.key}) property fragment\n`));
1038
- const propContentTypeInfo = createInitialFragment(propContentType, true, contentType, forCms12);
1244
+ const propContentTypeInfo = createInitialFragment(propContentType, true, contentType, generatedProps, forCms12);
1039
1245
  fs.writeFileSync(propertyFragmentFile, propContentTypeInfo.fragment);
1040
1246
  returnValue.push(propContentType.key);
1041
1247
  if (Array.isArray(propContentTypeInfo.propertyTypes))
@@ -1046,8 +1252,8 @@ function createGraphFragments(contentType, typePath, basePath, force, debug, con
1046
1252
  }
1047
1253
  return returnValue.length > 0 ? returnValue : undefined;
1048
1254
  }
1049
- function createInitialFragment(contentType, forProperty = false, forBaseType, forCms12 = false) {
1050
- const { fragmentFields, propertyTypes } = renderProperties(contentType, forCms12);
1255
+ function createInitialFragment(contentType, forProperty = false, forBaseType, generatedProps = [], forCms12 = false) {
1256
+ const { fragmentFields, propertyTypes } = renderProperties(contentType, generatedProps, forCms12);
1051
1257
  const contentTypeKey = contentType.key.split(':').pop();
1052
1258
  const fragmentTarget = forProperty ? (forCms12 ? (forBaseType?.key ?? '') + contentTypeKey : contentTypeKey + 'Property') : contentTypeKey;
1053
1259
  const tpl = `fragment ${fragmentTarget}Data on ${fragmentTarget} {
@@ -1058,7 +1264,7 @@ function createInitialFragment(contentType, forProperty = false, forBaseType, fo
1058
1264
  propertyTypes: propertyTypes.length == 0 ? null : propertyTypes
1059
1265
  };
1060
1266
  }
1061
- function renderProperties(contentType, forCms12 = false) {
1267
+ function renderProperties(contentType, generatedProps = [], forCms12 = false) {
1062
1268
  const propertyTypes = [];
1063
1269
  const fragmentFields = [];
1064
1270
  const typeProps = contentType.properties ?? {};
@@ -1070,30 +1276,33 @@ function renderProperties(contentType, forCms12 = false) {
1070
1276
  if (forCms12 && ['Categories'].includes(propKey))
1071
1277
  return;
1072
1278
  const propType = typeProps[propKey].type;
1073
- const isConflict = generatedProps.some(x => x.propName == propKey && x.propType != propType);
1279
+ const propDataType = getPropDataType(typeProps[propKey]);
1280
+ const isConflict = generatedProps.some(x => x.propName == propKey && x.propType != propDataType);
1074
1281
  const propName = isConflict ? `${contentType.key}${propKey}: ${propKey}` : propKey;
1075
1282
  // Write the property
1076
1283
  switch (propType) {
1077
- case IntegrationApi.PropertyDataType.ARRAY:
1284
+ case PropertyDataType.ARRAY:
1078
1285
  {
1079
1286
  const typeData = typeProps[propKey];
1080
1287
  switch (typeData.items.type) {
1081
- case IntegrationApi.PropertyDataType.INTEGER:
1082
- if (typeData.format == 'categorization')
1083
- fragmentFields.push(`${propName} { Id, Name, Description }`);
1288
+ case PropertyDataType.INTEGER:
1289
+ if (typeData.format == 'categorization') {
1290
+ //fragmentFields.push(`${propName} { Id, Name, Description }`)
1291
+ console.warn(chalk.redBright(`❗ Property ${propName} is a 'categorization', this is not supported. If you need to use this property, add it manually into the generated files`));
1292
+ }
1084
1293
  else
1085
1294
  fragmentFields.push(propName);
1086
1295
  break;
1087
- case IntegrationApi.PropertyDataType.STRING:
1296
+ case PropertyDataType.STRING:
1088
1297
  fragmentFields.push(propName);
1089
1298
  break;
1090
- case IntegrationApi.PropertyDataType.CONTENT:
1299
+ case PropertyDataType.CONTENT:
1091
1300
  if (contentType.baseType == 'page' || contentType.baseType == 'experience')
1092
1301
  fragmentFields.push(`${propName} { ...${forCms12 ? 'PageIContentListItem' : 'BlockData'} }`);
1093
1302
  else
1094
1303
  fragmentFields.push(`${propName} { ...IContentListItem }`);
1095
1304
  break;
1096
- case IntegrationApi.PropertyDataType.COMPONENT:
1305
+ case PropertyDataType.COMPONENT:
1097
1306
  const componentType = typeData.items.contentType.split(':').pop();
1098
1307
  switch (componentType) {
1099
1308
  case 'link':
@@ -1108,7 +1317,7 @@ function renderProperties(contentType, forCms12 = false) {
1108
1317
  }
1109
1318
  }
1110
1319
  break;
1111
- case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
1320
+ case PropertyDataType.CONTENT_REFERENCE:
1112
1321
  fragmentFields.push(`${propName} { ...ReferenceData }`);
1113
1322
  break;
1114
1323
  default:
@@ -1117,7 +1326,7 @@ function renderProperties(contentType, forCms12 = false) {
1117
1326
  }
1118
1327
  break;
1119
1328
  }
1120
- case IntegrationApi.PropertyDataType.STRING: {
1329
+ case PropertyDataType.STRING: {
1121
1330
  const propDetails = typeProps[propKey];
1122
1331
  switch (propDetails.format ?? "") {
1123
1332
  case 'html':
@@ -1135,15 +1344,15 @@ function renderProperties(contentType, forCms12 = false) {
1135
1344
  }
1136
1345
  break;
1137
1346
  }
1138
- case IntegrationApi.PropertyDataType.URL:
1347
+ case PropertyDataType.URL:
1139
1348
  fragmentFields.push(forCms12 ? propName : `${propName} { ...LinkData }`);
1140
1349
  break;
1141
- case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
1350
+ case PropertyDataType.CONTENT_REFERENCE:
1142
1351
  fragmentFields.push(`${propName} { ...ReferenceData }`);
1143
1352
  break;
1144
- case IntegrationApi.PropertyDataType.COMPONENT: {
1353
+ case PropertyDataType.COMPONENT: {
1145
1354
  const componentType = typeProps[propKey].contentType.split(':').pop();
1146
- if (forCms12 && componentType == "link") {
1355
+ if (componentType == "link") {
1147
1356
  fragmentFields.push(`${propName} { ...LinkItemData }`);
1148
1357
  }
1149
1358
  else {
@@ -1153,17 +1362,21 @@ function renderProperties(contentType, forCms12 = false) {
1153
1362
  }
1154
1363
  break;
1155
1364
  }
1156
- case IntegrationApi.PropertyDataType.BINARY:
1157
- fragmentFields.push(`${propName} { ...BinaryData }`);
1365
+ case PropertyDataType.BINARY:
1366
+ fragmentFields.push(propName);
1367
+ break;
1368
+ case PropertyDataType.CONTENT:
1369
+ fragmentFields.push(`${propName} { ...${forCms12 ? 'PageIContentListItem' : 'BlockData'} }`);
1158
1370
  break;
1159
1371
  default:
1160
1372
  fragmentFields.push(propName);
1161
1373
  break;
1162
1374
  }
1163
- generatedProps.push({
1164
- propType: typeProps[propKey].type,
1165
- propName: propKey
1166
- });
1375
+ if (propName === propKey && !generatedProps.some(x => x.propName === propKey))
1376
+ generatedProps.push({
1377
+ propType: propDataType,
1378
+ propName: propKey
1379
+ });
1167
1380
  });
1168
1381
  if (contentType.baseType == "experience")
1169
1382
  fragmentFields.push('...ExperienceData');
@@ -1175,6 +1388,18 @@ function renderProperties(contentType, forCms12 = false) {
1175
1388
  }
1176
1389
  return { fragmentFields, propertyTypes };
1177
1390
  }
1391
+ function getPropDataType(baseInfo) {
1392
+ const baseType = baseInfo.type;
1393
+ if (!baseType)
1394
+ throw new Error("Invalid property type definition");
1395
+ const propInfo = baseInfo.type === "array" ? baseInfo.items : baseInfo;
1396
+ switch (propInfo.type) {
1397
+ case "string":
1398
+ return propInfo.format === 'html' ? 'richtext' : propInfo.type;
1399
+ default:
1400
+ return propInfo.type;
1401
+ }
1402
+ }
1178
1403
 
1179
1404
  function ucFirst(current) {
1180
1405
  if (typeof current != 'string')
@@ -1446,6 +1671,7 @@ function getDisplayTemplateInfo(template, typePath) {
1446
1671
 
1447
1672
  const ROOT_FACTORY_KEY = ".";
1448
1673
  const FACTORY_FILE_NAME = "index.ts";
1674
+ const reservedNames = ["loading.tsx", "loading.jsx", "suspense.tsx", "suspense.jsx"];
1449
1675
  const NextJsFactoryCommand = {
1450
1676
  command: "nextjs:factory",
1451
1677
  describe: "Create the ComponentFactory for a Next.JS / Optimizely Graph structure",
@@ -1454,17 +1680,22 @@ const NextJsFactoryCommand = {
1454
1680
  const { components: basePath, force, _config: { debug } } = parseArgs(args);
1455
1681
  if (debug)
1456
1682
  process.stdout.write(chalk.gray(`${figures.arrowRight} Start generating component factories\n`));
1683
+ // Get & filter all component files
1457
1684
  const components = globSync(["./**/*.jsx", "./**/*.tsx"], {
1458
1685
  cwd: basePath
1459
1686
  }).map(p => p.split(path.sep)).filter(p => {
1687
+ // Get the filename
1688
+ const fileName = p.at(p.length - 1);
1689
+ if (!fileName)
1690
+ return false;
1460
1691
  // Consider components in a file starting with "_" as a partial
1461
- if (p.at(p.length - 1)?.startsWith('_') == true)
1692
+ if (fileName.startsWith('_') == true)
1462
1693
  return false;
1463
1694
  // Consider components in a folder named "partials" as a partial
1464
- if (p.at(p.length - 2)?.toLowerCase() == 'partials')
1695
+ if (p.some(folder => folder === 'partials'))
1465
1696
  return false;
1466
1697
  // Skip the special loader component
1467
- if (p.at(p.length - 1) == "loading.tsx" || p.at(p.length - 1) == "loading.jsx")
1698
+ if (reservedNames.includes(fileName))
1468
1699
  return false;
1469
1700
  // Check if the file has a default export
1470
1701
  const fileBuffer = fs.readFileSync(path.join(basePath, p.join(path.sep)));
@@ -1475,87 +1706,70 @@ const NextJsFactoryCommand = {
1475
1706
  }
1476
1707
  return true;
1477
1708
  });
1709
+ // Report what we have found
1478
1710
  if (debug)
1479
1711
  process.stdout.write(chalk.gray(`${figures.arrowRight} Identified ${components.length} components in ${basePath}\n`));
1712
+ // Build factory / component structure
1480
1713
  const componentFactoryDefintions = new Map();
1481
1714
  components.forEach(component => {
1715
+ // Determine component target
1716
+ const componentKey = processName(component.length == 1 ? ucFirst(path.basename(component[0], path.extname(component[0]))) : component.at(component.length - 2));
1717
+ let componentVariant = (component.length > 1 ? component.at(component.length - 1) ?? 'default' : 'default').replace('index', 'default');
1718
+ componentVariant = path.basename(componentVariant, path.extname(componentVariant));
1719
+ const componentDir = path.dirname(path.join(...component));
1720
+ // Get factory information
1482
1721
  const factorySegments = component.length > 2 ? component.slice(0, -2) : [ROOT_FACTORY_KEY];
1483
- const factoryKey = factorySegments.join(path.sep);
1722
+ const factoryKey = path.posix.join(...factorySegments);
1484
1723
  const factoryFile = path.join(factoryKey, FACTORY_FILE_NAME);
1485
- // Add component to factory
1724
+ // Check dynamic & suspense
1725
+ const useDynamic = [
1726
+ path.join(basePath, componentDir, 'loading.tsx'),
1727
+ path.join(basePath, componentDir, 'loading.jsx')
1728
+ ].some(fs.existsSync);
1729
+ const useSuspense = [
1730
+ path.join(basePath, componentDir, 'suspense.tsx'),
1731
+ path.join(basePath, componentDir, 'suspense.jsx')
1732
+ ].some(fs.existsSync);
1733
+ // Prepare data
1734
+ const componentImport = component.length == 1 ?
1735
+ `.${path.posix.sep}${path.basename(component[0], path.extname(component[0]))}` :
1736
+ `.${path.posix.sep}${path.posix.relative(factoryKey, componentDir)}${componentVariant !== 'default' ? path.posix.sep + componentVariant : ''}`;
1737
+ const loaderImport = useDynamic ? componentImport + path.posix.sep + 'loading' : undefined;
1738
+ const suspenseImport = useSuspense ? componentImport + path.posix.sep + 'suspense' : undefined;
1739
+ const componentVariablesBase = componentKey + (componentVariant !== 'default' ? processName(componentVariant) : '');
1740
+ // Add to the factory
1486
1741
  const factory = componentFactoryDefintions.get(factoryKey) || { file: factoryFile, entries: [], subfactories: [] };
1487
- const useSuspense = fs.existsSync(path.join(basePath, component.slice(0, -1).join(path.sep), 'loading.tsx')) || fs.existsSync(path.join(basePath, component.slice(0, -1).join(path.sep), 'loading.jsx'));
1488
- if (useSuspense && debug)
1489
- process.stdout.write(chalk.gray(`${figures.arrowRight} Components in ${component.slice(0, -1).join(path.sep)} will use suspense\n`));
1490
- const componentSegments = component.slice(-2).map(p => {
1491
- const entry = p.substring(0, p.length - path.extname(p).length);
1492
- return entry.toLowerCase() == "index" ? null : entry;
1493
- }).filter(x => x);
1494
- const componentImport = "./" + componentSegments.join('/');
1495
- const loaderImport = useSuspense ? "./" + component.slice(-2).map((p, i, a) => {
1496
- const entry = p.substring(0, p.length - path.extname(p).length);
1497
- if (i == a.length - 1)
1498
- return 'loading';
1499
- return entry.toLowerCase() == "index" ? null : entry;
1500
- }).join('/') : undefined;
1501
1742
  factory.entries.push({
1743
+ key: componentKey,
1744
+ variant: componentVariant,
1502
1745
  import: componentImport,
1503
- variable: [...componentSegments.map(processName), 'Component'].join(''),
1504
- key: componentSegments.join('/') == "node" ? "Node" : componentSegments.join('/'),
1746
+ variable: componentVariablesBase + 'Component',
1505
1747
  loaderImport,
1506
- loaderVariable: useSuspense ? [...componentSegments.map(processName), 'Loader'].join('') : undefined,
1748
+ loaderVariable: useDynamic ? componentVariablesBase + 'Loader' : undefined,
1749
+ suspenseImport,
1750
+ suspenseVariable: useSuspense ? componentVariablesBase + 'Placeholder' : undefined
1507
1751
  });
1508
1752
  componentFactoryDefintions.set(factoryKey, factory);
1509
- // Register with all appropriate parents
1753
+ // Add/update parent factories
1510
1754
  const parentSegements = factoryKey == ROOT_FACTORY_KEY ? factorySegments.slice(0, -1) : [ROOT_FACTORY_KEY, ...factorySegments.slice(0, -1)];
1511
- let currentFactory = {
1512
- key: factoryKey,
1513
- import: './' + factorySegments.slice(-1).join('/'),
1514
- prefix: processName(factorySegments.slice(-1).at(0)),
1515
- variable: factorySegments.map(processName).join("") + "Factory"
1516
- };
1517
- for (let i = parentSegements.length; i > 0; i--) {
1518
- const parentFactoryKey = parentSegements.slice(0, i).filter(x => x != ROOT_FACTORY_KEY).join(path.sep) || ROOT_FACTORY_KEY;
1755
+ parentSegements.forEach((_, idx, data) => {
1756
+ const parentFactoryKey = path.posix.join(...data.slice(0, idx + 1)) || ROOT_FACTORY_KEY;
1757
+ const childFactoryKey = factorySegments.slice(idx, idx + 1).at(0);
1519
1758
  const parentFactory = componentFactoryDefintions.get(parentFactoryKey) ?? { file: path.join(parentFactoryKey, FACTORY_FILE_NAME), entries: [], subfactories: [] };
1520
- if (!parentFactory.subfactories.some(x => x.key == currentFactory.key))
1521
- parentFactory.subfactories.push(currentFactory);
1522
- componentFactoryDefintions.set(parentFactoryKey, parentFactory);
1523
- if (i > 1) {
1524
- currentFactory = {
1525
- key: parentFactoryKey,
1526
- import: './' + parentFactoryKey.split(path.sep).slice(-1).at(0),
1527
- prefix: processName(parentFactoryKey.split(path.sep).slice(-1).at(0)),
1528
- variable: parentFactoryKey.split(path.sep).map(processName).join("") + "Factory"
1529
- };
1759
+ if (!parentFactory.subfactories.some(x => x.key === childFactoryKey)) {
1760
+ parentFactory.subfactories.push({
1761
+ key: childFactoryKey,
1762
+ import: './' + childFactoryKey,
1763
+ variable: processName(childFactoryKey) + 'Factory'
1764
+ });
1765
+ componentFactoryDefintions.set(parentFactoryKey, parentFactory);
1530
1766
  }
1531
- }
1532
- });
1533
- const mainFactory = componentFactoryDefintions.get(ROOT_FACTORY_KEY);
1534
- if (mainFactory) {
1535
- if (debug)
1536
- process.stdout.write(chalk.gray(`${figures.arrowRight} Updating prefixes within RootFactory\n`));
1537
- mainFactory.subfactories = mainFactory.subfactories.map(subFactory => {
1538
- if (typeof (subFactory.prefix == 'string'))
1539
- switch (subFactory.prefix) {
1540
- case "Video":
1541
- case "Image":
1542
- subFactory.prefix = ["Media", subFactory.prefix, "Component"];
1543
- break;
1544
- case "Experience":
1545
- subFactory.prefix = [subFactory.prefix, "Page"];
1546
- break;
1547
- case "Element":
1548
- subFactory.prefix = ["Component"];
1549
- break;
1550
- case "Media":
1551
- subFactory.prefix = [subFactory.prefix, "Component"];
1552
- break;
1553
- }
1554
- return subFactory;
1555
1767
  });
1556
- }
1768
+ });
1769
+ // Report factory file count
1557
1770
  if (debug)
1558
1771
  process.stdout.write(chalk.gray(`${figures.arrowRight} Finished preparing ${componentFactoryDefintions.size} factories, start writing\n`));
1772
+ // Iterate over the factories and create them
1559
1773
  let updateCounter = 0;
1560
1774
  for (const key of componentFactoryDefintions.keys()) {
1561
1775
  const factory = componentFactoryDefintions.get(key);
@@ -1604,59 +1818,55 @@ function processName(input) {
1604
1818
  return nameSegements.map(ucFirst).join('');
1605
1819
  }
1606
1820
  function generateFactory(factoryInfo, factoryKey) {
1607
- const needsPrefixFunction = factoryInfo.subfactories.length > 0;
1821
+ // Get the factory name
1608
1822
  const factoryName = factoryKey.split(path.sep).map(processName).join("") + "Factory";
1609
- const components = factoryInfo.entries;
1610
- const subFactories = factoryInfo.subfactories;
1611
- const factoryContent = `// Auto generated dictionary
1823
+ // Get the components and sub-factories, sorted by key to minimize changes between runs
1824
+ const components = [...factoryInfo.entries].sort((a, b) => { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
1825
+ const subFactories = [...factoryInfo.subfactories].sort((a, b) => { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
1826
+ // Check if there's at least one component that uses next/dynamic
1827
+ const hasDynamic = factoryInfo.entries.some(x => x.loaderImport);
1828
+ // The intro for the factory
1829
+ const factoryIntro = `// Auto generated dictionary
1612
1830
  // @not-modified => When this line is removed, the "force" parameter of the CLI tool is required to overwrite this file
1613
- import { type ComponentTypeDictionary } from "@remkoj/optimizely-cms-react";
1614
- ${[...components, ...subFactories].map(x => {
1615
- let importLine = `import ${x.variable} from "${x.import}";`;
1616
- if (x.loaderImport && x.loaderVariable) {
1617
- importLine += `\nimport ${x.loaderVariable} from "${x.loaderImport}";`;
1618
- }
1619
- return importLine;
1620
- }).join("\n")}
1621
-
1622
- ${needsPrefixFunction ? `// Prefix entries - if needed
1623
- ${subFactories.map(subFactory => Array.isArray(subFactory.prefix) ?
1624
- subFactory.prefix.map(z => `prefixDictionaryEntries(${subFactory.variable}, "${z}");`).join("\n") :
1625
- `prefixDictionaryEntries(${subFactory.variable}, "${subFactory.prefix}");`).join("\n")}
1626
-
1627
- ` : ''}// Build dictionary
1628
- export const ${factoryName} : ComponentTypeDictionary = [
1629
- ${[...components.map(x => {
1630
- if (x.loaderVariable) {
1631
- return `{
1632
- type: "${x.key}",
1633
- component: ${x.variable},
1634
- useSuspense: true,
1635
- loader: ${x.loaderVariable}
1636
- }`;
1637
- }
1638
- else {
1639
- return `{
1640
- type: "${x.key}",
1641
- component: ${x.variable}
1642
- }`;
1643
- }
1644
- }), ...subFactories.map(x => `...${x.variable}`)].join(",\n ")}
1645
- ];
1646
-
1647
- // Export dictionary
1648
- export default ${factoryName};
1649
- ${needsPrefixFunction ? `
1650
- // Helper functions
1651
- function prefixDictionaryEntries(list: ComponentTypeDictionary, prefix: string) : ComponentTypeDictionary
1652
- {
1653
- list.forEach((component, idx, dictionary) => {
1654
- dictionary[idx].type = typeof component.type == 'string' ? prefix + "/" + component.type : [ prefix, ...component.type ]
1655
- });
1656
- return list;
1657
- }
1658
- ` : ''}`;
1659
- return factoryContent;
1831
+ import { type ComponentTypeDictionary } from '@remkoj/optimizely-cms-react';${hasDynamic ? `
1832
+ import dynamic from 'next/dynamic';` : ''}`;
1833
+ // The outry for the factory
1834
+ const factoryOutro = `// Export dictionary
1835
+ export default ${factoryName};`;
1836
+ // The imports of the factory
1837
+ const factoryImports = [...components, ...subFactories].map(x => {
1838
+ let imports = [x.loaderImport ?
1839
+ `import ${x.loaderVariable} from '${x.loaderImport}';` :
1840
+ `import ${x.variable} from '${x.import}';`];
1841
+ if (x.suspenseImport)
1842
+ imports.push(`import ${x.suspenseVariable} from '${x.suspenseImport}';`);
1843
+ return imports.join('\n');
1844
+ }).join('\n');
1845
+ // The dynamic imports of the factory
1846
+ const dynamicImports = hasDynamic ? `// Lazy load components that have a loading file, this only affects client components
1847
+ // See https://nextjs.org/docs/app/guides/lazy-loading#importing-server-components
1848
+ // for more details on how this affects server components in Next.js
1849
+ ` + components.filter(x => x.loaderImport).map(x => {
1850
+ return `const ${x.variable} = dynamic(() => import('${x.import}'), {
1851
+ ssr: true,
1852
+ loading: ${x.loaderVariable}
1853
+ });`;
1854
+ }).join('\n') : undefined;
1855
+ // The actual entries for the factory
1856
+ const factoryEntries = [...components.map(x => {
1857
+ return ` {
1858
+ type: '${x.key}',${x.variant && x.variant !== 'default' ? `
1859
+ variant: '${x.variant}',` : ''}
1860
+ component: ${x.variable}${x.suspenseVariable ? `,
1861
+ useSuspense: true,
1862
+ loader: ${x.suspenseVariable}` : ''}
1863
+ }`;
1864
+ }), ...subFactories.map(x => ` ...${x.variable}`)];
1865
+ // The body of the factory
1866
+ const factoryBody = `// Build dictionary
1867
+ export const ${factoryName} : ComponentTypeDictionary = [${factoryEntries.length > 0 ? '\n' + factoryEntries.join(',\n') + '\n' : ''}];`;
1868
+ // Combine everything into one string
1869
+ return [factoryIntro, factoryImports, dynamicImports, factoryBody, factoryOutro].filter(x => (x?.length || 0) > 0).join('\n\n') + '\n';
1660
1870
  }
1661
1871
 
1662
1872
  const NextJsCreateCommand = {
@@ -1700,15 +1910,16 @@ const NextJsQueriesCommand = {
1700
1910
  handler: async (args, opts) => {
1701
1911
  // Prepare
1702
1912
  const { loadedContentTypes, createdTypeFolders } = opts || {};
1703
- const { components: basePath, _config: { debug }, force } = parseArgs(args);
1913
+ const { components: basePath, _config: { debug }, force, path: appPath } = parseArgs(args);
1704
1914
  const client = createCmsClient(args);
1705
1915
  const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
1916
+ const generatedProps = getGeneratedProps(appPath);
1706
1917
  // Start process
1707
1918
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments for ${contentTypes.map(x => x.key).join(', ')}\n`));
1708
1919
  const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
1709
1920
  const updatedTypes = contentTypes.map(contentType => {
1710
1921
  const typePath = getTypeFolder(typeFolders, contentType.key);
1711
- return createGraphQueries(contentType, typePath, basePath, force, debug, allContentTypes, client.runtimeCmsVersion == OptiCmsVersion.CMS12);
1922
+ return createGraphQueries(contentType, typePath, basePath, force, debug, allContentTypes, generatedProps, client.runtimeCmsVersion == OptiCmsVersion.CMS12);
1712
1923
  }).filter(x => x).flat();
1713
1924
  // Report outcome
1714
1925
  if (updatedTypes.length > 0)
@@ -1717,9 +1928,10 @@ const NextJsQueriesCommand = {
1717
1928
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
1718
1929
  if (!opts)
1719
1930
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1931
+ writeGeneratedProps(appPath, generatedProps);
1720
1932
  }
1721
1933
  };
1722
- function createGraphQueries(contentType, typePath, basePath, force, debug, contentTypes, forCms12 = false) {
1934
+ function createGraphQueries(contentType, typePath, basePath, force, debug, contentTypes, generatedProps = [], forCms12 = false) {
1723
1935
  const returnValue = [];
1724
1936
  const baseQueryFile = path.join(typePath, `${contentType.key}.query.graphql`);
1725
1937
  if (fs.existsSync(baseQueryFile)) {
@@ -1736,7 +1948,7 @@ function createGraphQueries(contentType, typePath, basePath, force, debug, conte
1736
1948
  else if (debug) {
1737
1949
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
1738
1950
  }
1739
- const { fragment, propertyTypes } = createInitialQuery(contentType, false, undefined, forCms12);
1951
+ const { fragment, propertyTypes } = createInitialQuery(contentType, false, undefined, generatedProps, forCms12);
1740
1952
  fs.writeFileSync(baseQueryFile, fragment);
1741
1953
  returnValue.push(contentType.key);
1742
1954
  let dependencies = Array.isArray(propertyTypes) ? [...propertyTypes] : [];
@@ -1756,7 +1968,7 @@ function createGraphQueries(contentType, typePath, basePath, force, debug, conte
1756
1968
  if (!fs.existsSync(propertyFragmentFile) || force) {
1757
1969
  if (debug)
1758
1970
  process.stdout.write(chalk.gray(`${figures.arrowRight} Writing ${propContentType.displayName} (${propContentType.key}) property fragment\n`));
1759
- const propContentTypeInfo = createInitialFragment(propContentType, true, contentType, forCms12);
1971
+ const propContentTypeInfo = createInitialFragment(propContentType, true, contentType, generatedProps, forCms12);
1760
1972
  fs.writeFileSync(propertyFragmentFile, propContentTypeInfo.fragment);
1761
1973
  returnValue.push(propContentType.key);
1762
1974
  if (Array.isArray(propContentTypeInfo.propertyTypes))
@@ -1767,8 +1979,8 @@ function createGraphQueries(contentType, typePath, basePath, force, debug, conte
1767
1979
  }
1768
1980
  return returnValue.length > 0 ? returnValue : undefined;
1769
1981
  }
1770
- function createInitialQuery(contentType, forProperty = false, forBaseType, forCms12 = false) {
1771
- const { fragmentFields, propertyTypes } = renderProperties(contentType, forCms12);
1982
+ function createInitialQuery(contentType, forProperty = false, forBaseType, generatedProps = [], forCms12 = false) {
1983
+ const { fragmentFields, propertyTypes } = renderProperties(contentType, generatedProps, forCms12);
1772
1984
  const fragmentTarget = forProperty ? (forCms12 ? ('') + contentType.key : contentType.key + 'Property') : contentType.key;
1773
1985
  const tpl = `query get${fragmentTarget}Data($key: String!, $locale: [Locales], $version: String, $changeset: String, $variation: String) {
1774
1986
  data: ${fragmentTarget} (
@@ -2148,7 +2360,7 @@ const commands = [
2148
2360
  CmsVersionCommand
2149
2361
  ];
2150
2362
 
2151
- var version = "5.1.7";
2363
+ var version = "5.3.0";
2152
2364
  var name = "opti-cms";
2153
2365
  var APP = {
2154
2366
  version: version,
@@ -2156,8 +2368,8 @@ var APP = {
2156
2368
  };
2157
2369
 
2158
2370
  async function main() {
2159
- const envFiles = prepare();
2160
2371
  const projectDir = getProjectDir();
2372
+ const envFiles = prepare(projectDir);
2161
2373
  const app = createOptiCmsApp(APP.name, APP.version, undefined, envFiles, projectDir);
2162
2374
  app.command(commands);
2163
2375
  try {