@remkoj/optimizely-cms-cli 6.0.0-pre12 → 6.0.0-pre14

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/README.md CHANGED
@@ -1,4 +1,8 @@
1
1
  # Optimizely CMS Command Line Toolkit <!-- omit in toc -->
2
+
3
+ > [!WARNING]
4
+ > There'll be an update of Optimizely SaaS CMS that is incompatible with all SDK versions prior to 5.1.6. If you don't upgrade, you will see empty pages (main website) and "Component not found" messages (preview).
5
+
2
6
  A collection of Command Line tools used to increase productivity when working with the Optimizely CMS from a TypeScript / JavaScript based frontend.
3
7
 
4
8
  The defaults and methods are based upon using a Next.JS application with the conventions introduced by the [Create Next App template](https://github.com/remkoj/optimizely-saas-starter)
@@ -123,4 +127,4 @@ yarn opti-cms nextjs:factory -f
123
127
  | --baseTypes | -b | Select only content types with one of these base types. Add multiple times to build a list | [] |
124
128
  | --types | -t | Select content types with this key. Add multiple times to build a list | [] |
125
129
  | --all | -a | Include non-supported base types, non supported base types are skipped by default | |
126
- | --force | -f | By default, this method is none-destructive. Set this parameter to overwrite existing files. | |
130
+ | --force | -f | By default, this method is none-destructive. Set this parameter to overwrite existing files. | |
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { globSync, glob, globIterate } from 'glob';
2
2
  import { config } from 'dotenv';
3
3
  import { expand } from 'dotenv-expand';
4
+ import path from 'node:path';
5
+ import fs from 'node:fs';
4
6
  import { readPartialEnvConfig, createClient, OptiCmsVersion, ContentRoots } from '@remkoj/optimizely-cms-api';
5
7
  import yargs from 'yargs';
6
8
  import chalk from 'chalk';
7
- import path from 'node:path';
8
- import fs from 'node:fs';
9
9
  import figures from 'figures';
10
10
  import Table from 'cli-table3';
11
11
  import fsAsync from 'node:fs/promises';
@@ -13,23 +13,38 @@ import { input, select, confirm } from '@inquirer/prompts';
13
13
  import createDeepMerge from '@fastify/deepmerge';
14
14
  import { Ajv } from 'ajv';
15
15
  import addFormats from 'ajv-formats';
16
- import GraphQLGen from '@remkoj/optimizely-graph-functions/contenttype-loader';
16
+ import { DocumentGenerator, PropertyCollisionTracker } from '@remkoj/optimizely-graph-functions/generate';
17
17
  import deepEqual from 'fast-deep-equal';
18
18
 
19
+ function getProjectDir() {
20
+ let testPath = process.cwd();
21
+ let hasPackageJson = false;
22
+ do {
23
+ hasPackageJson = fs.statSync(path.join(testPath, 'package.json'), { throwIfNoEntry: false })?.isFile() ?? false;
24
+ if (!hasPackageJson)
25
+ testPath = path.normalize(path.join(testPath, '..'));
26
+ } while (!hasPackageJson && testPath.length > 2);
27
+ if (!hasPackageJson) {
28
+ throw new Error('No package.json found!');
29
+ }
30
+ return testPath;
31
+ }
19
32
  /**
20
33
  * Prepare the application context, by parsing the .env files in the main
21
34
  * application directory.
22
35
  *
23
36
  * @returns A string array with the files processed
24
37
  */
25
- function prepare() {
26
- const globResult = globSync(".env*");
27
- const envFiles = globResult.sort((a, b) => b.length - a.length).filter(n => n == ".env" || n == ".env.local" || (process.env.NODE_ENV && n == `.env.${process.env.NODE_ENV}.local`));
38
+ function prepare(pDir) {
39
+ const projectDir = pDir ?? getProjectDir();
40
+ const envFiles = globSync(".env*", { cwd: projectDir })
41
+ .sort((a, b) => b.length - a.length)
42
+ .filter(n => n == ".env" || n == ".env.local" || (process.env.NODE_ENV && n == `.env.${process.env.NODE_ENV}.local`));
28
43
  expand(config({ path: envFiles, debug: false, quiet: true }));
29
44
  return envFiles;
30
45
  }
31
46
 
32
- function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
47
+ function createOptiCmsApp(scriptName, version, epilogue, envFiles, projectDir) {
33
48
  if (envFiles) {
34
49
  process.stdout.write(chalk.bold(`✅ Loaded environment files:`) + "\n");
35
50
  envFiles.forEach(envFile => {
@@ -45,11 +60,12 @@ function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
45
60
  console.error(chalk.redBright(`${chalk.bold("[ERROR]:")} Error processing environment variables: ${e.message}`));
46
61
  process.exit(1);
47
62
  }
63
+ const defaultPath = projectDir ?? process.cwd();
48
64
  return yargs(process.argv)
49
65
  .scriptName(scriptName)
50
66
  .version(version)
51
67
  .usage('$0 <cmd> [args]')
52
- .option("path", { alias: "p", description: "Application root folder", string: true, type: "string", demandOption: false, default: process.cwd() })
68
+ .option("path", { alias: "p", description: "Application root folder", string: true, type: "string", demandOption: false, default: defaultPath })
53
69
  .option("components", { alias: "c", description: "Path to components folder", string: true, type: "string", demandOption: false, default: "./src/components/cms" })
54
70
  .option("cms_url", { alias: "cu", description: "Optimizely CMS URL", string: true, type: "string", demandOption: isDemanded(config.base), default: config.base, coerce: (val) => val ? new URL(val) : undefined })
55
71
  .option("client_id", { alias: "ci", description: "API Client ID", string: true, type: "string", demandOption: isDemanded(config.clientId), default: config.clientId })
@@ -588,6 +604,20 @@ function ucFirst$1(input) {
588
604
  return input;
589
605
  return input[0].toUpperCase() + input.substring(1);
590
606
  }
607
+ function toTypeFilesList(client, templates, basePath, createPaths = true) {
608
+ return templates.reduce(async (prevList, displayTemplate) => {
609
+ const typeFiles = await prevList;
610
+ const { targetType, styleFilePath: filePath, helperFilePath: helperPath } = await createTemplateMetadata(client, displayTemplate, basePath, createPaths);
611
+ if (!typeFiles[targetType]) {
612
+ typeFiles[targetType] = {
613
+ filePath: helperPath,
614
+ templates: []
615
+ };
616
+ }
617
+ typeFiles[targetType].templates.push({ key: displayTemplate.key, file: filePath, data: displayTemplate });
618
+ return typeFiles;
619
+ }, Promise.resolve({}));
620
+ }
591
621
  async function createTemplateMetadata(client, displayTemplate, basePath, createPaths = true) {
592
622
  let itemPath = undefined;
593
623
  let targetType;
@@ -811,6 +841,91 @@ const StylesCreateCommand = {
811
841
  }
812
842
  };
813
843
 
844
+ const StylesDeleteCommand = {
845
+ command: "styles:delete",
846
+ describe: "Remove Visual Builder style definitions from the CMS",
847
+ builder: (yargs) => {
848
+ const newYargs = stylesBuilder(yargs);
849
+ newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
850
+ newYargs.option('withStyleFile', { alias: 'w', description: "Delete the .opti-style.json file as weill", boolean: true, type: 'boolean', demandOption: false, default: true });
851
+ newYargs.option("definitions", { alias: 'u', description: "Update typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true });
852
+ return newYargs;
853
+ },
854
+ handler: async (args) => {
855
+ const { components: basePath } = parseArgs(args);
856
+ const client = createCmsClient(args);
857
+ const { styles, all: allStyles } = await getStyles(client, args, 100);
858
+ if (styles.findIndex(x => x.isDefault) >= 0)
859
+ process.stdout.write(chalk.redBright(chalk.bold((`\n${figures.warning} You are deleting a default Display Template this may lead to unpredicted behavior.\n\n`))));
860
+ if (!args.force) {
861
+ process.stdout.write(`This will remove the following display templates\n`);
862
+ for (const style of styles) {
863
+ process.stdout.write(` ${figures.arrowRight} ${style.displayName || style.key} [Key: ${style.key}; Default: ${style.isDefault ? 'Yes' : 'No'}]\n`);
864
+ }
865
+ process.stdout.write(`\nRun with -f to actually preform removal\n`);
866
+ return;
867
+ }
868
+ const keysToDelete = styles.map(displayTemplate => displayTemplate.key);
869
+ const styleHelpers = await toTypeFilesList(client, allStyles, basePath, false);
870
+ for (const displayTemplate of styles) {
871
+ const { styleFilePath, targetType, itemPath, typesPath } = await createTemplateMetadata(client, displayTemplate, basePath, false);
872
+ // Remove style file
873
+ if (args.withStyleFile) {
874
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing *.opti-style.json file for ${displayTemplate.displayName} [${displayTemplate.key}]\n`));
875
+ await fsAsync.rm(styleFilePath, { force: false, recursive: false, maxRetries: 1 }).catch((e) => {
876
+ if (e?.code === 'ENOENT') // The file can't be deleted as it does not exist - that's the desired state
877
+ return;
878
+ throw e;
879
+ });
880
+ await fsAsync.rmdir(itemPath, { recursive: false, maxRetries: 1 }).catch((e) => {
881
+ if (e?.code === 'ENOTEMPTY') {
882
+ process.stdout.write(chalk.yellowBright(`${figures.warning} The folder is not empty, please remove ${itemPath} manually if needed\n`));
883
+ return;
884
+ }
885
+ else if (e?.code === 'ENOENT')
886
+ return;
887
+ else
888
+ throw e;
889
+ });
890
+ }
891
+ // Remove/update typescript helper
892
+ if (args.definitions && styleHelpers[targetType]) {
893
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing/updating displayTemplates.ts file for ${displayTemplate.displayName} [${displayTemplate.key}]\n`));
894
+ const remainingTemplates = styleHelpers[targetType].templates.filter(x => !keysToDelete.includes(x.key));
895
+ if (remainingTemplates.length === 0) {
896
+ await fsAsync.rm(styleHelpers[targetType].filePath, { force: false, recursive: false, maxRetries: 1 }).catch((e) => {
897
+ if (e?.code === 'ENOENT') // The file can't be deleted as it does not exist - that's the desired state
898
+ return;
899
+ throw e;
900
+ });
901
+ await fsAsync.rmdir(typesPath, { recursive: false, maxRetries: 1 }).catch((e) => {
902
+ if (e?.code === 'ENOTEMPTY') {
903
+ process.stdout.write(chalk.yellowBright(`${figures.warning} The folder is not empty, please remove ${typesPath} manually if needed\n`));
904
+ return;
905
+ }
906
+ else if (e?.code === 'ENOENT')
907
+ return;
908
+ else
909
+ throw e;
910
+ });
911
+ }
912
+ else {
913
+ const newEntry = {
914
+ filePath: styleHelpers[targetType].filePath,
915
+ templates: remainingTemplates
916
+ };
917
+ if (!await createDisplayTemplateHelper(newEntry, targetType, false, false))
918
+ process.stdout.write(chalk.yellowBright(`${figures.warning} The display template was not updated, please update ${newEntry.filePath} manually`));
919
+ }
920
+ }
921
+ // Actually remove from CMS
922
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing the display template ${displayTemplate.displayName} [${displayTemplate.key}] from Optimizely CMS\n`));
923
+ await client.displayTemplatesDelete({ path: { key: displayTemplate.key } });
924
+ }
925
+ process.stdout.write("\n" + chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
926
+ }
927
+ };
928
+
814
929
  const TypesPullCommand = {
815
930
  command: "types:pull",
816
931
  describe: "Pull content type definition files into the project",
@@ -1500,6 +1615,7 @@ function getDisplayTemplateInfo(template, typePath) {
1500
1615
 
1501
1616
  const ROOT_FACTORY_KEY = ".";
1502
1617
  const FACTORY_FILE_NAME = "index.ts";
1618
+ const reservedNames = ["loading.tsx", "loading.jsx", "suspense.tsx", "suspense.jsx"];
1503
1619
  const NextJsFactoryCommand = {
1504
1620
  command: "nextjs:factory",
1505
1621
  describe: "Create the ComponentFactory for a Next.JS / Optimizely Graph structure",
@@ -1508,17 +1624,22 @@ const NextJsFactoryCommand = {
1508
1624
  const { components: basePath, force, _config: { debug } } = parseArgs(args);
1509
1625
  if (debug)
1510
1626
  process.stdout.write(chalk.gray(`${figures.arrowRight} Start generating component factories\n`));
1627
+ // Get & filter all component files
1511
1628
  const components = globSync(["./**/*.jsx", "./**/*.tsx"], {
1512
1629
  cwd: basePath
1513
1630
  }).map(p => p.split(path.sep)).filter(p => {
1631
+ // Get the filename
1632
+ const fileName = p.at(p.length - 1);
1633
+ if (!fileName)
1634
+ return false;
1514
1635
  // Consider components in a file starting with "_" as a partial
1515
- if (p.at(p.length - 1)?.startsWith('_') == true)
1636
+ if (fileName.startsWith('_') == true)
1516
1637
  return false;
1517
1638
  // Consider components in a folder named "partials" as a partial
1518
- if (p.at(p.length - 2)?.toLowerCase() == 'partials')
1639
+ if (p.some(folder => folder === 'partials'))
1519
1640
  return false;
1520
1641
  // Skip the special loader component
1521
- if (p.at(p.length - 1) == "loading.tsx" || p.at(p.length - 1) == "loading.jsx")
1642
+ if (reservedNames.includes(fileName))
1522
1643
  return false;
1523
1644
  // Check if the file has a default export
1524
1645
  const fileBuffer = fs.readFileSync(path.join(basePath, p.join(path.sep)));
@@ -1529,87 +1650,70 @@ const NextJsFactoryCommand = {
1529
1650
  }
1530
1651
  return true;
1531
1652
  });
1653
+ // Report what we have found
1532
1654
  if (debug)
1533
1655
  process.stdout.write(chalk.gray(`${figures.arrowRight} Identified ${components.length} components in ${basePath}\n`));
1656
+ // Build factory / component structure
1534
1657
  const componentFactoryDefintions = new Map();
1535
1658
  components.forEach(component => {
1659
+ // Determine component target
1660
+ const componentKey = processName(component.length == 1 ? ucFirst(path.basename(component[0], path.extname(component[0]))) : component.at(component.length - 2));
1661
+ let componentVariant = (component.length > 1 ? component.at(component.length - 1) ?? 'default' : 'default').replace('index', 'default');
1662
+ componentVariant = path.basename(componentVariant, path.extname(componentVariant));
1663
+ const componentDir = path.dirname(path.join(...component));
1664
+ // Get factory information
1536
1665
  const factorySegments = component.length > 2 ? component.slice(0, -2) : [ROOT_FACTORY_KEY];
1537
- const factoryKey = factorySegments.join(path.sep);
1666
+ const factoryKey = path.posix.join(...factorySegments);
1538
1667
  const factoryFile = path.join(factoryKey, FACTORY_FILE_NAME);
1539
- // Add component to factory
1668
+ // Check dynamic & suspense
1669
+ const useDynamic = [
1670
+ path.join(basePath, componentDir, 'loading.tsx'),
1671
+ path.join(basePath, componentDir, 'loading.jsx')
1672
+ ].some(fs.existsSync);
1673
+ const useSuspense = [
1674
+ path.join(basePath, componentDir, 'suspense.tsx'),
1675
+ path.join(basePath, componentDir, 'suspense.jsx')
1676
+ ].some(fs.existsSync);
1677
+ // Prepare data
1678
+ const componentImport = component.length == 1 ?
1679
+ `.${path.posix.sep}${path.basename(component[0], path.extname(component[0]))}` :
1680
+ `.${path.posix.sep}${path.posix.relative(factoryKey, componentDir)}${componentVariant !== 'default' ? path.posix.sep + componentVariant : ''}`;
1681
+ const loaderImport = useDynamic ? componentImport + path.posix.sep + 'loading' : undefined;
1682
+ const suspenseImport = useSuspense ? componentImport + path.posix.sep + 'suspense' : undefined;
1683
+ const componentVariablesBase = componentKey + (componentVariant !== 'default' ? processName(componentVariant) : '');
1684
+ // Add to the factory
1540
1685
  const factory = componentFactoryDefintions.get(factoryKey) || { file: factoryFile, entries: [], subfactories: [] };
1541
- 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'));
1542
- if (useSuspense && debug)
1543
- process.stdout.write(chalk.gray(`${figures.arrowRight} Components in ${component.slice(0, -1).join(path.sep)} will use suspense\n`));
1544
- const componentSegments = component.slice(-2).map(p => {
1545
- const entry = p.substring(0, p.length - path.extname(p).length);
1546
- return entry.toLowerCase() == "index" ? null : entry;
1547
- }).filter(x => x);
1548
- const componentImport = "./" + componentSegments.join('/');
1549
- const loaderImport = useSuspense ? "./" + component.slice(-2).map((p, i, a) => {
1550
- const entry = p.substring(0, p.length - path.extname(p).length);
1551
- if (i == a.length - 1)
1552
- return 'loading';
1553
- return entry.toLowerCase() == "index" ? null : entry;
1554
- }).join('/') : undefined;
1555
1686
  factory.entries.push({
1687
+ key: componentKey,
1688
+ variant: componentVariant,
1556
1689
  import: componentImport,
1557
- variable: [...componentSegments.map(processName), 'Component'].join(''),
1558
- key: componentSegments.join('/') == "node" ? "Node" : componentSegments.join('/'),
1690
+ variable: componentVariablesBase + 'Component',
1559
1691
  loaderImport,
1560
- loaderVariable: useSuspense ? [...componentSegments.map(processName), 'Loader'].join('') : undefined,
1692
+ loaderVariable: useDynamic ? componentVariablesBase + 'Loader' : undefined,
1693
+ suspenseImport,
1694
+ suspenseVariable: useSuspense ? componentVariablesBase + 'Placeholder' : undefined
1561
1695
  });
1562
1696
  componentFactoryDefintions.set(factoryKey, factory);
1563
- // Register with all appropriate parents
1697
+ // Add/update parent factories
1564
1698
  const parentSegements = factoryKey == ROOT_FACTORY_KEY ? factorySegments.slice(0, -1) : [ROOT_FACTORY_KEY, ...factorySegments.slice(0, -1)];
1565
- let currentFactory = {
1566
- key: factoryKey,
1567
- import: './' + factorySegments.slice(-1).join('/'),
1568
- prefix: processName(factorySegments.slice(-1).at(0)),
1569
- variable: factorySegments.map(processName).join("") + "Factory"
1570
- };
1571
- for (let i = parentSegements.length; i > 0; i--) {
1572
- const parentFactoryKey = parentSegements.slice(0, i).filter(x => x != ROOT_FACTORY_KEY).join(path.sep) || ROOT_FACTORY_KEY;
1699
+ parentSegements.forEach((_, idx, data) => {
1700
+ const parentFactoryKey = path.posix.join(...data.slice(0, idx + 1)) || ROOT_FACTORY_KEY;
1701
+ const childFactoryKey = factorySegments.slice(idx, idx + 1).at(0);
1573
1702
  const parentFactory = componentFactoryDefintions.get(parentFactoryKey) ?? { file: path.join(parentFactoryKey, FACTORY_FILE_NAME), entries: [], subfactories: [] };
1574
- if (!parentFactory.subfactories.some(x => x.key == currentFactory.key))
1575
- parentFactory.subfactories.push(currentFactory);
1576
- componentFactoryDefintions.set(parentFactoryKey, parentFactory);
1577
- if (i > 1) {
1578
- currentFactory = {
1579
- key: parentFactoryKey,
1580
- import: './' + parentFactoryKey.split(path.sep).slice(-1).at(0),
1581
- prefix: processName(parentFactoryKey.split(path.sep).slice(-1).at(0)),
1582
- variable: parentFactoryKey.split(path.sep).map(processName).join("") + "Factory"
1583
- };
1703
+ if (!parentFactory.subfactories.some(x => x.key === childFactoryKey)) {
1704
+ parentFactory.subfactories.push({
1705
+ key: childFactoryKey,
1706
+ import: './' + childFactoryKey,
1707
+ variable: processName(childFactoryKey) + 'Factory'
1708
+ });
1709
+ componentFactoryDefintions.set(parentFactoryKey, parentFactory);
1584
1710
  }
1585
- }
1586
- });
1587
- const mainFactory = componentFactoryDefintions.get(ROOT_FACTORY_KEY);
1588
- if (mainFactory) {
1589
- if (debug)
1590
- process.stdout.write(chalk.gray(`${figures.arrowRight} Updating prefixes within RootFactory\n`));
1591
- mainFactory.subfactories = mainFactory.subfactories.map(subFactory => {
1592
- if (typeof (subFactory.prefix == 'string'))
1593
- switch (subFactory.prefix) {
1594
- case "Video":
1595
- case "Image":
1596
- subFactory.prefix = ["Media", subFactory.prefix, "Component"];
1597
- break;
1598
- case "Experience":
1599
- subFactory.prefix = [subFactory.prefix, "Page"];
1600
- break;
1601
- case "Element":
1602
- subFactory.prefix = ["Component"];
1603
- break;
1604
- case "Media":
1605
- subFactory.prefix = [subFactory.prefix, "Component"];
1606
- break;
1607
- }
1608
- return subFactory;
1609
1711
  });
1610
- }
1712
+ });
1713
+ // Report factory file count
1611
1714
  if (debug)
1612
1715
  process.stdout.write(chalk.gray(`${figures.arrowRight} Finished preparing ${componentFactoryDefintions.size} factories, start writing\n`));
1716
+ // Iterate over the factories and create them
1613
1717
  let updateCounter = 0;
1614
1718
  for (const key of componentFactoryDefintions.keys()) {
1615
1719
  const factory = componentFactoryDefintions.get(key);
@@ -1658,59 +1762,55 @@ function processName(input) {
1658
1762
  return nameSegements.map(ucFirst).join('');
1659
1763
  }
1660
1764
  function generateFactory(factoryInfo, factoryKey) {
1661
- const needsPrefixFunction = factoryInfo.subfactories.length > 0;
1765
+ // Get the factory name
1662
1766
  const factoryName = factoryKey.split(path.sep).map(processName).join("") + "Factory";
1663
- const components = factoryInfo.entries;
1664
- const subFactories = factoryInfo.subfactories;
1665
- const factoryContent = `// Auto generated dictionary
1767
+ // Get the components and sub-factories, sorted by key to minimize changes between runs
1768
+ const components = [...factoryInfo.entries].sort((a, b) => { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
1769
+ const subFactories = [...factoryInfo.subfactories].sort((a, b) => { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
1770
+ // Check if there's at least one component that uses next/dynamic
1771
+ const hasDynamic = factoryInfo.entries.some(x => x.loaderImport);
1772
+ // The intro for the factory
1773
+ const factoryIntro = `// Auto generated dictionary
1666
1774
  // @not-modified => When this line is removed, the "force" parameter of the CLI tool is required to overwrite this file
1667
- import { type ComponentTypeDictionary } from "@remkoj/optimizely-cms-react";
1668
- ${[...components, ...subFactories].map(x => {
1669
- let importLine = `import ${x.variable} from "${x.import}";`;
1670
- if (x.loaderImport && x.loaderVariable) {
1671
- importLine += `\nimport ${x.loaderVariable} from "${x.loaderImport}";`;
1672
- }
1673
- return importLine;
1674
- }).join("\n")}
1675
-
1676
- ${needsPrefixFunction ? `// Prefix entries - if needed
1677
- ${subFactories.map(subFactory => Array.isArray(subFactory.prefix) ?
1678
- subFactory.prefix.map(z => `prefixDictionaryEntries(${subFactory.variable}, "${z}");`).join("\n") :
1679
- `prefixDictionaryEntries(${subFactory.variable}, "${subFactory.prefix}");`).join("\n")}
1680
-
1681
- ` : ''}// Build dictionary
1682
- export const ${factoryName} : ComponentTypeDictionary = [
1683
- ${[...components.map(x => {
1684
- if (x.loaderVariable) {
1685
- return `{
1686
- type: "${x.key}",
1687
- component: ${x.variable},
1688
- useSuspense: true,
1689
- loader: ${x.loaderVariable}
1690
- }`;
1691
- }
1692
- else {
1693
- return `{
1694
- type: "${x.key}",
1695
- component: ${x.variable}
1696
- }`;
1697
- }
1698
- }), ...subFactories.map(x => `...${x.variable}`)].join(",\n ")}
1699
- ];
1700
-
1701
- // Export dictionary
1702
- export default ${factoryName};
1703
- ${needsPrefixFunction ? `
1704
- // Helper functions
1705
- function prefixDictionaryEntries(list: ComponentTypeDictionary, prefix: string) : ComponentTypeDictionary
1706
- {
1707
- list.forEach((component, idx, dictionary) => {
1708
- dictionary[idx].type = typeof component.type == 'string' ? prefix + "/" + component.type : [ prefix, ...component.type ]
1709
- });
1710
- return list;
1711
- }
1712
- ` : ''}`;
1713
- return factoryContent;
1775
+ import { type ComponentTypeDictionary } from '@remkoj/optimizely-cms-react';${hasDynamic ? `
1776
+ import dynamic from 'next/dynamic';` : ''}`;
1777
+ // The outry for the factory
1778
+ const factoryOutro = `// Export dictionary
1779
+ export default ${factoryName};`;
1780
+ // The imports of the factory
1781
+ const factoryImports = [...components, ...subFactories].map(x => {
1782
+ let imports = [x.loaderImport ?
1783
+ `import ${x.loaderVariable} from '${x.loaderImport}';` :
1784
+ `import ${x.variable} from '${x.import}';`];
1785
+ if (x.suspenseImport)
1786
+ imports.push(`import ${x.suspenseVariable} from '${x.suspenseImport}';`);
1787
+ return imports.join('\n');
1788
+ }).join('\n');
1789
+ // The dynamic imports of the factory
1790
+ const dynamicImports = hasDynamic ? `// Lazy load components that have a loading file, this only affects client components
1791
+ // See https://nextjs.org/docs/app/guides/lazy-loading#importing-server-components
1792
+ // for more details on how this affects server components in Next.js
1793
+ ` + components.filter(x => x.loaderImport).map(x => {
1794
+ return `const ${x.variable} = dynamic(() => import('${x.import}'), {
1795
+ ssr: true,
1796
+ loading: ${x.loaderVariable}
1797
+ });`;
1798
+ }).join('\n') : undefined;
1799
+ // The actual entries for the factory
1800
+ const factoryEntries = [...components.map(x => {
1801
+ return ` {
1802
+ type: '${x.key}',${x.variant && x.variant !== 'default' ? `
1803
+ variant: '${x.variant}',` : ''}
1804
+ component: ${x.variable}${x.suspenseVariable ? `,
1805
+ useSuspense: true,
1806
+ loader: ${x.suspenseVariable}` : ''}
1807
+ }`;
1808
+ }), ...subFactories.map(x => ` ...${x.variable}`)];
1809
+ // The body of the factory
1810
+ const factoryBody = `// Build dictionary
1811
+ export const ${factoryName} : ComponentTypeDictionary = [${factoryEntries.length > 0 ? '\n' + factoryEntries.join(',\n') + '\n' : ''}];`;
1812
+ // Combine everything into one string
1813
+ return [factoryIntro, factoryImports, dynamicImports, factoryBody, factoryOutro].filter(x => (x?.length || 0) > 0).join('\n\n') + '\n';
1714
1814
  }
1715
1815
 
1716
1816
  const NextJsCreateCommand = {
@@ -1750,16 +1850,23 @@ const NextJsFragmentsCommand = {
1750
1850
  handler: async (args, opts) => {
1751
1851
  // Prepare
1752
1852
  const { loadedContentTypes, createdTypeFolders } = opts || {};
1753
- const { components: basePath, _config: { debug }, force } = parseArgs(args);
1853
+ const { components: basePath, _config: { debug }, force, path: appPath } = parseArgs(args);
1754
1854
  const client = createCmsClient(args);
1855
+ // Get content types
1755
1856
  const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
1857
+ const allContentTypesMap = new Map();
1858
+ allContentTypes.forEach(x => {
1859
+ if (x.key)
1860
+ allContentTypesMap.set(x.key, x);
1861
+ });
1862
+ const generator = new DocumentGenerator(allContentTypesMap);
1756
1863
  // Start process
1757
1864
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments\n`));
1758
1865
  const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
1759
- const tracker = new Map();
1866
+ const tracker = new PropertyCollisionTracker(appPath);
1760
1867
  const dependencies = [];
1761
1868
  const updatedTypes = contentTypes.map(contentType => {
1762
- const { written, propertyTypes } = createComponentFragments(contentType, getTypeFolder(typeFolders, contentType.key), force, debug, tracker);
1869
+ const { written, propertyTypes } = createComponentFragments(contentType, getTypeFolder(typeFolders, contentType.key), force, debug, tracker, generator);
1763
1870
  dependencies.push(...propertyTypes);
1764
1871
  return written ? contentType.key : undefined;
1765
1872
  }).filter(x => x);
@@ -1779,7 +1886,7 @@ const NextJsFragmentsCommand = {
1779
1886
  typeFolders.push(tf);
1780
1887
  }
1781
1888
  return tf;
1782
- }, force, debug);
1889
+ }, force, debug, tracker, generator);
1783
1890
  // Report outcome
1784
1891
  if (generatedProps.length > 0)
1785
1892
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
@@ -1789,7 +1896,7 @@ const NextJsFragmentsCommand = {
1789
1896
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1790
1897
  }
1791
1898
  };
1792
- function createComponentFragments(contentType, typePath, force, debug, propertyTracker = new Map()) {
1899
+ function createComponentFragments(contentType, typePath, force, debug, propertyTracker = new Map(), generator) {
1793
1900
  const baseQueryFile = typePath.fragmentFile;
1794
1901
  let writeFragment = true;
1795
1902
  if (fs.existsSync(baseQueryFile)) {
@@ -1808,13 +1915,13 @@ function createComponentFragments(contentType, typePath, force, debug, propertyT
1808
1915
  }
1809
1916
  let written = false;
1810
1917
  if (writeFragment) {
1811
- const fragment = GraphQLGen.buildFragment(contentType, undefined, false, propertyTracker);
1918
+ const fragment = generator.buildFragment(contentType, undefined, false, propertyTracker);
1812
1919
  fs.writeFileSync(baseQueryFile, fragment);
1813
1920
  written = true;
1814
1921
  }
1815
- return { written, propertyTypes: GraphQLGen.getReferencedPropertyComponents(contentType) };
1922
+ return { written, propertyTypes: DocumentGenerator.getReferencedPropertyComponents(contentType) };
1816
1923
  }
1817
- function createPropertyFragments(propertyTypesList, allContentTypes, selectTypeFolder, force = false, debug = false) {
1924
+ function createPropertyFragments(propertyTypesList, allContentTypes, selectTypeFolder, force = false, debug = false, propertyTracker = new Map(), generator) {
1818
1925
  const returnValue = [];
1819
1926
  for (const propertyContentTypeKey of propertyTypesList.filter((v, i, a) => a.indexOf(v, i + 1) <= i)) {
1820
1927
  // Load the ContentType definition
@@ -1846,16 +1953,16 @@ function createPropertyFragments(propertyTypesList, allContentTypes, selectTypeF
1846
1953
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) property fragment\n`));
1847
1954
  }
1848
1955
  if (mustWrite) {
1849
- const fragment = GraphQLGen.buildFragment(contentType, undefined, true);
1956
+ const fragment = generator.buildFragment(contentType, undefined, true, propertyTracker);
1850
1957
  fs.writeFileSync(propertyFragmentFile, fragment);
1851
1958
  returnValue.push(contentType.key);
1852
1959
  }
1853
1960
  // Recurse down for properties that we're not yet rendering
1854
- const referencedPropertyTypes = GraphQLGen.getReferencedPropertyComponents(contentType).filter(x => !propertyTypesList.includes(x));
1961
+ const referencedPropertyTypes = DocumentGenerator.getReferencedPropertyComponents(contentType).filter(x => !propertyTypesList.includes(x));
1855
1962
  if (referencedPropertyTypes.length > 0) {
1856
1963
  if (debug)
1857
1964
  process.stdout.write(chalk.gray(`${figures.arrowRight} Component property ${propertyContentTypeKey} uses components a property, recursing down\n`));
1858
- const additionalProperties = createPropertyFragments(referencedPropertyTypes, allContentTypes, selectTypeFolder, force, debug);
1965
+ const additionalProperties = createPropertyFragments(referencedPropertyTypes, allContentTypes, selectTypeFolder, force, debug, propertyTracker, generator);
1859
1966
  returnValue.push(...additionalProperties);
1860
1967
  }
1861
1968
  }
@@ -1873,16 +1980,23 @@ const NextJsQueriesCommand = {
1873
1980
  handler: async (args, opts) => {
1874
1981
  // Prepare
1875
1982
  const { loadedContentTypes, createdTypeFolders } = opts || {};
1876
- const { components: basePath, _config: { debug }, force } = parseArgs(args);
1983
+ const { components: basePath, _config: { debug }, force, path: appPath } = parseArgs(args);
1877
1984
  const client = createCmsClient(args);
1878
1985
  const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
1986
+ const allContentTypesMap = new Map();
1987
+ allContentTypes.forEach(x => {
1988
+ if (x.key)
1989
+ allContentTypesMap.set(x.key, x);
1990
+ });
1991
+ const generator = new DocumentGenerator(allContentTypesMap);
1992
+ const propertyTracker = new PropertyCollisionTracker(appPath);
1879
1993
  // Start process
1880
1994
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL queries\n`));
1881
1995
  const dependencies = [];
1882
1996
  const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
1883
1997
  const updatedTypes = contentTypes.map(contentType => {
1884
1998
  const typePath = getTypeFolder(typeFolders, contentType.key);
1885
- const { written, propertyTypes } = createGraphQueries(contentType, typePath, force, debug);
1999
+ const { written, propertyTypes } = createGraphQueries(contentType, typePath, force, debug, propertyTracker, generator);
1886
2000
  dependencies.push(...propertyTypes);
1887
2001
  return written ? contentType.key : undefined;
1888
2002
  }).filter(x => x).flat();
@@ -1901,7 +2015,7 @@ const NextJsQueriesCommand = {
1901
2015
  typeFolders.push(tf);
1902
2016
  }
1903
2017
  return tf;
1904
- }, force, debug);
2018
+ }, force, debug, propertyTracker, generator);
1905
2019
  // Report outcome
1906
2020
  if (generatedProps.length > 0)
1907
2021
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
@@ -1911,7 +2025,7 @@ const NextJsQueriesCommand = {
1911
2025
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1912
2026
  }
1913
2027
  };
1914
- function createGraphQueries(contentType, typePath, force, debug) {
2028
+ function createGraphQueries(contentType, typePath, force, debug, tracker, generator) {
1915
2029
  const baseQueryFile = typePath.queryFile;
1916
2030
  let mustWrite = true;
1917
2031
  if (fs.existsSync(baseQueryFile)) {
@@ -1929,12 +2043,12 @@ function createGraphQueries(contentType, typePath, force, debug) {
1929
2043
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
1930
2044
  }
1931
2045
  if (mustWrite) {
1932
- const fragment = GraphQLGen.buildGetQuery(contentType, undefined, new Map());
2046
+ const fragment = generator.buildGetQuery(contentType, undefined, tracker);
1933
2047
  fs.writeFileSync(baseQueryFile, fragment);
1934
2048
  }
1935
2049
  return {
1936
2050
  written: mustWrite,
1937
- propertyTypes: GraphQLGen.getReferencedPropertyComponents(contentType)
2051
+ propertyTypes: DocumentGenerator.getReferencedPropertyComponents(contentType)
1938
2052
  };
1939
2053
  }
1940
2054
 
@@ -2214,7 +2328,7 @@ function writeFileAsync(path, data) {
2214
2328
  });
2215
2329
  }
2216
2330
 
2217
- var version = "6.0.0-pre12";
2331
+ var version = "6.0.0-pre14";
2218
2332
  var name = "opti-cms";
2219
2333
  var APP = {
2220
2334
  version: version,
@@ -2582,20 +2696,22 @@ const commands = [
2582
2696
  StylesListCommand,
2583
2697
  StylesPullCommand,
2584
2698
  StylesPushCommand,
2699
+ StylesDeleteCommand,
2585
2700
  TypesPullCommand,
2586
2701
  TypesPushCommand
2587
2702
  ];
2588
2703
 
2589
2704
  async function main() {
2590
- const envFiles = prepare();
2591
- const app = createOptiCmsApp(APP.name, APP.version, undefined, envFiles);
2705
+ const projectDir = getProjectDir();
2706
+ const envFiles = prepare(projectDir);
2707
+ const app = createOptiCmsApp(APP.name, APP.version, undefined, envFiles, projectDir);
2592
2708
  app.command(commands);
2593
2709
  try {
2594
2710
  await app.parse(process.argv.slice(2));
2595
2711
  }
2596
2712
  catch {
2597
2713
  //We're ignoring error here, as yargs will already generate the "nice output" for it
2598
- //console.log('Caught error')
2714
+ //console.log ('Caught error')
2599
2715
  }
2600
2716
  }
2601
2717
  main();