@remkoj/optimizely-cms-cli 6.0.0-pre8 → 6.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,34 +1,52 @@
1
1
  import { globSync, glob, globIterate } from 'glob';
2
2
  import { config } from 'dotenv';
3
3
  import { expand } from 'dotenv-expand';
4
- import { readPartialEnvConfig, getCmsIntegrationApiConfigFromEnvironment, createClient, OptiCmsVersion, IntegrationApi, ContentRoots } from '@remkoj/optimizely-cms-api';
5
- import yargs from 'yargs';
6
- import chalk from 'chalk';
7
4
  import path from 'node:path';
8
5
  import fs from 'node:fs';
6
+ import { readPartialEnvConfig, createClient, OptiCmsVersion, ContentRoots } from '@remkoj/optimizely-cms-api';
7
+ import yargs from 'yargs';
8
+ import chalk from 'chalk';
9
9
  import figures from 'figures';
10
10
  import Table from 'cli-table3';
11
+ import slugify from '@sindresorhus/slugify';
12
+ import { diff } from 'deep-object-diff';
13
+ import fsAsync from 'node:fs/promises';
11
14
  import { input, select, confirm } from '@inquirer/prompts';
12
- import fs$1 from 'node:fs/promises';
13
15
  import createDeepMerge from '@fastify/deepmerge';
14
16
  import { Ajv } from 'ajv';
15
17
  import addFormats from 'ajv-formats';
18
+ import { DocumentGenerator, PropertyCollisionTracker } from '@remkoj/optimizely-graph-functions/generate';
16
19
  import deepEqual from 'fast-deep-equal';
17
20
 
21
+ function getProjectDir() {
22
+ let testPath = process.cwd();
23
+ let hasPackageJson = false;
24
+ do {
25
+ hasPackageJson = fs.statSync(path.join(testPath, 'package.json'), { throwIfNoEntry: false })?.isFile() ?? false;
26
+ if (!hasPackageJson)
27
+ testPath = path.normalize(path.join(testPath, '..'));
28
+ } while (!hasPackageJson && testPath.length > 2);
29
+ if (!hasPackageJson) {
30
+ throw new Error('No package.json found!');
31
+ }
32
+ return testPath;
33
+ }
18
34
  /**
19
35
  * Prepare the application context, by parsing the .env files in the main
20
36
  * application directory.
21
37
  *
22
38
  * @returns A string array with the files processed
23
39
  */
24
- function prepare() {
25
- const globResult = globSync(".env*");
26
- 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`));
40
+ function prepare(pDir) {
41
+ const projectDir = pDir ?? getProjectDir();
42
+ const envFiles = globSync(".env*", { cwd: projectDir })
43
+ .sort((a, b) => b.length - a.length)
44
+ .filter(n => n == ".env" || n == ".env.local" || (process.env.NODE_ENV && n == `.env.${process.env.NODE_ENV}.local`));
27
45
  expand(config({ path: envFiles, debug: false, quiet: true }));
28
46
  return envFiles;
29
47
  }
30
48
 
31
- function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
49
+ function createOptiCmsApp(scriptName, version, epilogue, envFiles, projectDir) {
32
50
  if (envFiles) {
33
51
  process.stdout.write(chalk.bold(`✅ Loaded environment files:`) + "\n");
34
52
  envFiles.forEach(envFile => {
@@ -44,13 +62,14 @@ function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
44
62
  console.error(chalk.redBright(`${chalk.bold("[ERROR]:")} Error processing environment variables: ${e.message}`));
45
63
  process.exit(1);
46
64
  }
65
+ const defaultPath = projectDir ?? process.cwd();
47
66
  return yargs(process.argv)
48
67
  .scriptName(scriptName)
49
68
  .version(version)
50
69
  .usage('$0 <cmd> [args]')
51
- .option("path", { alias: "p", description: "Application root folder", string: true, type: "string", demandOption: false, default: process.cwd() })
70
+ .option("path", { alias: "p", description: "Application root folder", string: true, type: "string", demandOption: false, default: defaultPath })
52
71
  .option("components", { alias: "c", description: "Path to components folder", string: true, type: "string", demandOption: false, default: "./src/components/cms" })
53
- .option("cms_url", { alias: "cu", description: "Optimizely CMS URL", string: true, type: "string", demandOption: isDemanded(config.base), default: config.base, coerce: (val) => new URL(val) })
72
+ .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 })
54
73
  .option("client_id", { alias: "ci", description: "API Client ID", string: true, type: "string", demandOption: isDemanded(config.clientId), default: config.clientId })
55
74
  .option('client_secret', { alias: "cs", description: "API Client Secrent", string: true, type: "string", demandOption: isDemanded(config.clientSecret), default: config.clientSecret })
56
75
  .option('user_id', { alias: "u", description: "Impersonate user id", string: true, type: "string", demandOption: false, default: config.actAs })
@@ -115,13 +134,13 @@ function parseArgs({ client_id, client_secret, cms_url, user_id, verbose, path:
115
134
 
116
135
  function createCmsClient(args) {
117
136
  const cfg = getCmsIntegrationApiOptions(args);
118
- const baseConfig = getCmsIntegrationApiConfigFromEnvironment();
137
+ const baseConfig = readPartialEnvConfig();
119
138
  const client = createClient({
120
139
  ...baseConfig,
121
140
  ...cfg
122
141
  });
123
142
  if (cfg.debug)
124
- process.stdout.write(chalk.gray(`${figures.arrowRight} Connecting to ${cfg.base.href} as ${cfg.actAs ?? cfg.clientId}\n`));
143
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Connecting to ${client.cmsUrl} as ${cfg.actAs ?? cfg.clientId}\n`));
125
144
  return client;
126
145
  }
127
146
  function getCmsIntegrationApiOptions(args) {
@@ -132,149 +151,285 @@ function getCmsIntegrationApiOptions(args) {
132
151
  return parseArgs(args)._config;
133
152
  }
134
153
 
135
- const StylesPushCommand = {
136
- command: "styles:push",
137
- describe: "Push Visual Builder style definitions into the CMS (create/replace)",
138
- builder: (yargs) => {
139
- yargs.option('excludeTemplates', { alias: 'e', description: "Exclude these templates", string: true, type: 'array', demandOption: false, default: [] });
140
- yargs.option("templates", { alias: 't', description: "Select only these templates", string: true, type: 'array', demandOption: false, default: [] });
141
- return yargs;
142
- },
143
- handler: async (args) => {
144
- const { _config: cfg, excludeTemplates, templates, ...opts } = parseArgs(args);
145
- const client = createCmsClient(args);
146
- if (client.runtimeCmsVersion == OptiCmsVersion.CMS12) {
147
- process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
148
- return;
154
+ /**
155
+ * This file contains tools that allow using the project that we're targeting
156
+ */
157
+ /**
158
+ * Get all the file paths for the content type, taking the current project configuration into
159
+ * account.
160
+ *
161
+ * @param contentType
162
+ * @param basePath
163
+ * @returns
164
+ */
165
+ function getContentTypePaths(contentType, basePath, createFolder = false, debug = false) {
166
+ const baseTypeSlug = keyToSlug(contentType.baseType, {
167
+ defaultKey: 'global',
168
+ stripLeadingUnderscore: true
169
+ });
170
+ const typeSlug = keyToSlug(contentType.key);
171
+ const typePath = path.join(basePath, baseTypeSlug, typeSlug);
172
+ if (createFolder) {
173
+ if (!fs.existsSync(typePath)) {
174
+ fs.mkdirSync(typePath, { recursive: true });
175
+ if (debug)
176
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Created folder ${typePath} for Content-Type ${contentType.key}\n`));
149
177
  }
150
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/replace) DisplayStyles into Optimizely CMS\n`));
151
- const styleDefinitionFiles = await glob("./**/*.opti-style.json", {
152
- cwd: opts.components
153
- });
154
- const results = (await Promise.all(styleDefinitionFiles.map(async (styleDefinitionFile) => {
155
- const filePath = path.normalize(path.join(opts.components, styleDefinitionFile));
156
- const styleDefinition = tryReadJsonFile(filePath, cfg.debug);
157
- const styleKey = styleDefinition.key;
158
- if (!styleKey) {
159
- 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`));
160
- return undefined;
161
- }
162
- if (excludeTemplates.includes(styleKey))
163
- return undefined; // Skip excluded styles
164
- if (templates.length > 0 && !templates.includes(styleKey))
165
- return undefined; // Only include defined styles, if any
166
- if (cfg.debug)
167
- process.stdout.write(chalk.gray(`${figures.arrowRight} Pushing: ${styleKey}\n`));
168
- const newTemplate = await client.displayTemplatesPut({ path: { key: styleKey }, body: styleDefinition });
169
- return newTemplate;
170
- }))).filter(isNotNullOrUndefined);
171
- const styles = new Table({
172
- head: [
173
- chalk.yellow(chalk.bold("Name")),
174
- chalk.yellow(chalk.bold("Key")),
175
- chalk.yellow(chalk.bold("Default")),
176
- chalk.yellow(chalk.bold("Target"))
177
- ],
178
- colWidths: [31, 20, 9, 20],
179
- colAligns: ["left", "left", "center", "left"]
180
- });
181
- results.forEach(tpl => {
182
- styles.push([
183
- tpl.displayName,
184
- tpl.key,
185
- tpl.isDefault ? figures.tick : figures.cross,
186
- tpl.contentType ? `${tpl.contentType} (C)` : tpl.baseType ? `${tpl.baseType} (B)` : `${tpl.nodeType} (N)`
187
- ]);
188
- });
189
- process.stdout.write(styles.toString() + "\n");
190
- process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
178
+ // Check folders
179
+ if (!fs.statSync(typePath).isDirectory())
180
+ throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
191
181
  }
192
- };
193
- function tryReadJsonFile(filePath, debug = false) {
194
- try {
195
- if (debug)
196
- process.stdout.write(chalk.gray(`${figures.arrowRight} Reading style definition from ${filePath}\n`));
197
- return JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' }));
182
+ const typeFile = path.join(typePath, `${typeSlug}.opti-type.json`);
183
+ const fragmentFile = path.join(typePath, `${typeSlug}.${baseTypeSlug}.graphql`);
184
+ const propertyFragmentFile = path.join(typePath, `${typeSlug}.property.graphql`);
185
+ const queryFile = path.join(typePath, `${typeSlug}.query.graphql`);
186
+ const componentFile = path.join(typePath, `index.tsx`);
187
+ return {
188
+ type: contentType.key,
189
+ path: typePath,
190
+ typePath,
191
+ typeFile,
192
+ fragmentFile,
193
+ componentFile,
194
+ propertyFragmentFile,
195
+ queryFile
196
+ };
197
+ }
198
+ function getStyleFilePathsSync(displayTemplate, contentBaseType = '_Component', basePath = './src/components/cms', createFolder = false) {
199
+ const displayTemplateType = getDisplayTemplateType(displayTemplate);
200
+ let target;
201
+ let groupPath;
202
+ switch (displayTemplateType) {
203
+ case 'base':
204
+ groupPath = path.join(keyToSlug(displayTemplate.baseType, { stripLeadingUnderscore: true }), 'styles');
205
+ target = displayTemplate.baseType;
206
+ break;
207
+ case 'node':
208
+ groupPath = path.join('nodes', keyToSlug(displayTemplate.nodeType));
209
+ target = displayTemplate.nodeType;
210
+ break;
211
+ case 'component':
212
+ const baseType = contentBaseType;
213
+ target = displayTemplate.contentType;
214
+ groupPath = path.join(keyToSlug(baseType, { stripLeadingUnderscore: true }), keyToSlug(displayTemplate.contentType, { stripLeadingGroup: true }));
215
+ break;
216
+ default:
217
+ throw new Error(`Unsupported DisplayTemplate target for ${displayTemplate.key}`);
198
218
  }
199
- catch (e) {
200
- process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} Error while reading ${filePath}\n`));
219
+ const displayTemplateKeySlug = keyToSlug(displayTemplate.key ?? 'displayTemplate', { stripLeadingGroup: true });
220
+ const groupFolder = path.join(basePath, groupPath);
221
+ const styleFolder = displayTemplateType === 'component' ? groupFolder : path.join(groupFolder, displayTemplateKeySlug);
222
+ if (createFolder && !fs.existsSync(styleFolder))
223
+ fs.mkdirSync(styleFolder, { recursive: true });
224
+ return {
225
+ styleFile: path.join(styleFolder, displayTemplateKeySlug + '.opti-style.json'),
226
+ helperFile: path.join(groupFolder, 'displayTemplates.ts'),
227
+ identifier: `${displayTemplateType}/${target}`,
228
+ helperFolder: groupFolder,
229
+ styleFolder
230
+ };
231
+ }
232
+ async function getStyleFilePaths(definition, opts) {
233
+ let defintionBaseType = undefined;
234
+ if (definition.contentType && !opts.contentBaseType && opts.client) {
235
+ const contentType = await opts.client.contentTypesGet({ path: { key: definition.contentType } }).catch(() => undefined);
236
+ defintionBaseType = contentType?.baseType;
201
237
  }
202
- return undefined;
238
+ else {
239
+ defintionBaseType = opts.contentBaseType;
240
+ }
241
+ return getStyleFilePathsSync(definition, defintionBaseType, opts.basePath, opts.createFolder);
242
+ }
243
+ /**
244
+ * Simple logic to create path slugs for storing ContentType related files on
245
+ * disk.
246
+ *
247
+ * @param typeKey
248
+ * @param stripLeadingUnderscore
249
+ * @returns
250
+ */
251
+ function keyToSlug(typeKey, options = {}) {
252
+ // Destruct the configuration into the parts we need
253
+ const { stripLeadingGroup = true, defaultKey = 'unknown', stripLeadingUnderscore = false, ...slugifyBaseConfig } = options;
254
+ const slugifyConfig = {
255
+ ...slugifyBaseConfig,
256
+ preserveLeadingUnderscore: !stripLeadingUnderscore
257
+ };
258
+ // First make sure we have a valid input type
259
+ if (!(typeof typeKey === 'string' || typeKey === undefined || typeKey === null))
260
+ throw new Error(`Invalid typeKey provided, expected an optional string, received a value of type ${typeof typeKey}`);
261
+ // Then, take the prefix out, if any and required
262
+ const toSlugify = stripLeadingGroup && typeKey?.indexOf(":") >= 0 ? typeKey.split(":", 2).at(1) : typeKey;
263
+ // Finally slugify the result
264
+ return slugify(toSlugify ?? defaultKey, slugifyConfig);
265
+ }
266
+ function getDisplayTemplateType(displayTemplate) {
267
+ const templateType = displayTemplate.baseType ? 'base' : displayTemplate.nodeType ? 'node' : displayTemplate.contentType ? 'component' : 'unknown';
268
+ return templateType;
269
+ }
270
+
271
+ /**
272
+ * Test if the value must be included in a set, by defining allowed values and blocked values. A value
273
+ * is considered be eligible to be included when it's both allowed an not disallowed.
274
+ *
275
+ * @param value The value to test
276
+ * @param allow The list of allowed values, when not provided, or and empty array, all
277
+ * values will be allowed.
278
+ * @param disallow The list of explicitly disallowed values, yielding an "all allowed but
279
+ * these" operation
280
+ * @param compareSlugified When set to `true`, and after test with `includes()` on the allow and
281
+ * disallow did not yield a match, will try to match by converting the
282
+ * values to string (using the `.toString()` method), then slugify them with
283
+ * the `keyToSlug` method and then comparing the outcomes.
284
+ * @param slugifyOptions The options to provide to the `keyToSlug` method, this will be ignored
285
+ * unless you set the `compareSlugified` parameter to `true`
286
+ * @returns If the value should be included in the result, based upon the `allow` and
287
+ * `disallow` configuration.
288
+ *
289
+ * @see {@link keyToSlug}
290
+ */
291
+ function shouldInclude(value, allow, disallow, compareSlugified = false, slugifyOptions) {
292
+ // Do not include null or undefined values
293
+ if (value === null || value === undefined)
294
+ return false;
295
+ // Item is allowed when either allow is not set, an empty array or has the value
296
+ const isAllowed = !Array.isArray(allow) || allow.length === 0 || allow.includes(value) || (compareSlugified && allow.some(av => keyToSlug(av.toString(), slugifyOptions) === keyToSlug(value.toString(), slugifyOptions)));
297
+ // Item is disallowed when and the array is set and includes the value
298
+ const isDisallowed = Array.isArray(disallow) && (disallow.includes(value) || (compareSlugified && disallow.some(av => keyToSlug(av.toString(), slugifyOptions) === keyToSlug(value.toString(), slugifyOptions))));
299
+ // Return the outcome
300
+ return isAllowed && !isDisallowed;
301
+ }
302
+ /**
303
+ * Test if the provided content type is a contract
304
+ *
305
+ * @param contentType The ContentType to test
306
+ * @returns `true` if the ContentType is a contract, `false` otherwise
307
+ * @see {@link IntegrationApi.ContentType}
308
+ */
309
+ function isContract(contentType) {
310
+ if (typeof contentType !== 'object' || contentType === null)
311
+ return false;
312
+ return contentType.isContract || contentType.source === 'globalcontract';
313
+ }
314
+ /**
315
+ * Test if the provided content type comes from the Graph content source (e.g. external content)
316
+ *
317
+ * @param contentType The ContentType to test
318
+ * @returns `true` if the ContentType is a graph type, `false` otherwise
319
+ * @see {@link IntegrationApi.ContentType}
320
+ */
321
+ function isGraphType(contentType) {
322
+ if (typeof contentType !== 'object' || contentType === null)
323
+ return false;
324
+ return (contentType.key && typeof (contentType.key) === 'string' && contentType.key.startsWith('graph:')) || contentType.source === 'graph';
325
+ }
326
+ /**
327
+ * Test if the provided content type is a folder, which is a structural element for editors, but
328
+ * does not represent anything significant when working with ContentType
329
+ *
330
+ * @param contentType The ContentType to test
331
+ * @returns `true` if the ContentType is a folder, `false` otherwise
332
+ * @see {@link IntegrationApi.ContentType}
333
+ */
334
+ function isFolder(contentType) {
335
+ if (typeof contentType !== 'object' || contentType === null)
336
+ return false;
337
+ return contentType.baseType === '_folder';
338
+ }
339
+ /**
340
+ * Test if the provided content type is a system type
341
+ *
342
+ * @param contentType The ContentType to test
343
+ * @returns `true` if the ContentType is a folder, `false` otherwise
344
+ * @see {@link IntegrationApi.ContentType}
345
+ */
346
+ function isSystemType(contentType) {
347
+ if (typeof contentType !== 'object' || contentType === null)
348
+ return false;
349
+ return contentType.source === 'system';
203
350
  }
204
- function isNotNullOrUndefined(i) {
205
- return i ? true : false;
351
+ function isNonEmptyArray(toTest) {
352
+ return Array.isArray(toTest) && toTest.length > 0;
353
+ }
354
+ function isEmptyArray(toTest) {
355
+ return toTest === null || !Array.isArray(toTest) || toTest.length === 0;
356
+ }
357
+ function isDefined(toTest) {
358
+ return toTest !== null && toTest !== undefined;
206
359
  }
207
360
 
208
- const contentTypesBuilder = yargs => {
209
- yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
210
- yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
211
- yargs.option("baseTypes", { alias: 'b', description: "Select only these base types", string: true, type: 'array', demandOption: false, default: [] });
212
- yargs.option("types", { alias: 't', description: "Select only these types", string: true, type: 'array', demandOption: false, default: [] });
213
- yargs.option('all', { alias: 'a', description: "Include non-supported base types", boolean: true, type: 'boolean', demandOption: false, default: false });
361
+ const ContentTypesArgsDefaults = {
362
+ excludeBaseTypes: ['folder', 'media', 'image', 'video'],
363
+ excludeTypes: [],
364
+ baseTypes: [],
365
+ types: [],
366
+ all: false
367
+ };
368
+ const contentTypesBuilder = (yargs, defaults = ContentTypesArgsDefaults) => {
369
+ yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: defaults.excludeTypes });
370
+ yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: defaults.excludeBaseTypes });
371
+ yargs.option("baseTypes", { alias: 'b', description: "Select only these base types", string: true, type: 'array', demandOption: false, default: defaults.baseTypes });
372
+ yargs.option("types", { alias: 't', description: "Select only these types", string: true, type: 'array', demandOption: false, default: defaults.types });
373
+ yargs.option('all', { alias: 'a', description: "Include non-supported base types", boolean: true, type: 'boolean', demandOption: false, default: defaults.all });
214
374
  return yargs;
215
375
  };
216
- function getEnumOptions(enumObject) {
217
- return Object.keys(enumObject).filter((item) => {
218
- return isNaN(Number(item));
219
- });
220
- }
221
- async function getContentTypes(client, args, pageSize = 5, allowSystem = false, customFilter) {
376
+ async function getContentTypes(client, args, pageSize = 25, allowSystem = false, customFilter) {
222
377
  const { _config: cfg, excludeBaseTypes, excludeTypes, baseTypes, types, all } = parseArgs(args);
223
- const validBaseTypes = getEnumOptions(IntegrationApi.ContentBaseType).map(x => x.toLowerCase());
224
378
  const allContentTypes = [];
225
379
  const filteredContentTypes = [];
226
380
  for await (const contentType of getAllContentTypes(client, cfg.debug, pageSize)) {
227
- const normalizedContentType = { ...contentType, baseType: contentType.baseType ?? 'default' };
228
- // Build the "All" data set
229
- if (all)
230
- allContentTypes.push(normalizedContentType);
231
- else if (validBaseTypes.includes(normalizedContentType.baseType.toLowerCase()))
232
- allContentTypes.push(normalizedContentType);
233
- else if (cfg.debug) {
234
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${normalizedContentType.key} as it has an unsupported base type: ${normalizedContentType.baseType}\n`));
381
+ // Skip contracts by default as they're abstract classes and cannot be used to directly store data.
382
+ if (!all && isContract(contentType)) {
383
+ if (cfg.debug)
384
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key || contentType.displayName || "unnamed type"} as it is a contract and cannot be instantiated directly, use --all to include\n`));
235
385
  continue;
236
386
  }
237
- else {
387
+ // Skip content types mapped against Graph data, these should be used with their source type in Graph,
388
+ // not the reference in CMS
389
+ if (!all && isGraphType(contentType)) {
390
+ if (cfg.debug)
391
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it is a reference to external data in Optimizely Graph, use --all to include\n`));
392
+ continue;
393
+ }
394
+ // Skip folder types as these are non-data carrying types in the instance.
395
+ if (!all && isFolder(contentType)) {
396
+ if (cfg.debug)
397
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it is a folder, use --all to include\n`));
238
398
  continue;
239
399
  }
400
+ // Build the unfiltered array
401
+ allContentTypes.push(contentType);
240
402
  // Skip based upon base type filters
241
- if (!shouldInclude(normalizedContentType.baseType, baseTypes, excludeBaseTypes)) {
403
+ if (!shouldInclude(contentType.baseType, baseTypes, excludeBaseTypes, true)) {
242
404
  if (cfg.debug)
243
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${normalizedContentType.key} as it has a restricted base type: ${normalizedContentType.baseType}\n`));
405
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it has a restricted base type: ${contentType.baseType}\n`));
244
406
  continue;
245
407
  }
246
408
  // Skip based upon type filters
247
- if (!shouldInclude(normalizedContentType.key, types, excludeTypes)) {
409
+ if (!shouldInclude(contentType.key, types, excludeTypes, true)) {
248
410
  if (cfg.debug)
249
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${normalizedContentType.key} as it has a restricted type: ${normalizedContentType.key}\n`));
411
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it is a restricted type (${types.join(', ')})(${excludeTypes.join(', ')})\n`));
250
412
  continue;
251
413
  }
252
414
  // Skip based upon system filter
253
- if (normalizedContentType.source == 'system' && !allowSystem) {
415
+ if (!allowSystem && isSystemType(contentType)) {
254
416
  if (cfg.debug)
255
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${normalizedContentType.key} due to it being a system type\n`));
417
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${contentType.key} due to it being a system type\n`));
256
418
  continue;
257
419
  }
258
420
  // Skip based upon custom filter
259
- if (customFilter && !await customFilter(normalizedContentType)) {
421
+ if (customFilter && !await customFilter(contentType)) {
260
422
  if (cfg.debug)
261
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${normalizedContentType.key} due to the custom filter\n`));
423
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${contentType.key} due to the custom filter\n`));
262
424
  continue;
263
425
  }
264
426
  // Add to list
265
- filteredContentTypes.push(normalizedContentType);
427
+ filteredContentTypes.push(contentType);
266
428
  }
267
429
  if (cfg.debug)
268
430
  process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${allContentTypes.length} to ${filteredContentTypes.length} items\n`));
269
431
  return { all: allContentTypes, contentTypes: filteredContentTypes };
270
432
  }
271
- function shouldInclude(value, allow, disallow) {
272
- // Item is allowed when either allow is not set, an empty array or has the value
273
- const isAllowed = !Array.isArray(allow) || allow.length === 0 || allow.includes(value);
274
- // Item is disallowed when and the array is set and includes the value
275
- const isDisallowed = Array.isArray(disallow) && disallow.includes(value);
276
- return isAllowed && !isDisallowed;
277
- }
278
433
  /**
279
434
  * Retrieve all content types as an Async Generator, allowing processing of entries whilest they are being loaded from the CMS instance.
280
435
  *
@@ -322,50 +477,60 @@ const stylesBuilder = yargs => {
322
477
  return newArgs;
323
478
  };
324
479
  async function getStyles(client, args, pageSize = 25) {
325
- if (client.runtimeCmsVersion == OptiCmsVersion.CMS12)
326
- return { all: [], styles: [] };
327
- const { _config: cfg, excludeBaseTypes, excludeTypes, excludeNodeTypes, excludeTemplates, baseTypes, types, nodes, templates, templateTypes } = parseArgs(args);
480
+ const { _config: cfg, excludeBaseTypes: disallowBaseTypes, excludeTypes: disallowTypes, excludeNodeTypes: disallowNodeTypes, excludeTemplates: disallowTemplates, baseTypes: allowBaseTypes, types: allowTypes, nodes: allowNodeTypes, templates: allowTemplates, templateTypes: baseAllowTemplateTypes } = parseArgs(args);
328
481
  process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Style-Definitions from Optimizely CMS\n`));
482
+ const allowTemplateTypes = [];
483
+ if (!Array.isArray(baseAllowTemplateTypes) || baseAllowTemplateTypes.length === 0) {
484
+ if (isNonEmptyArray(allowBaseTypes)) {
485
+ console.log('BT', allowBaseTypes);
486
+ if (cfg.debug)
487
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Base picking filter is active, adjusting template type filter to pick templates targeting a base type\n`));
488
+ allowTemplateTypes.push('base');
489
+ }
490
+ if (isNonEmptyArray(allowNodeTypes)) {
491
+ if (cfg.debug)
492
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Node type filter active, adjusting template type filter\n`));
493
+ allowTemplateTypes.push('node');
494
+ }
495
+ if (isNonEmptyArray(allowTypes)) {
496
+ if (cfg.debug)
497
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Component type filter active, adjusting template type filter\n`));
498
+ allowTemplateTypes.push('component');
499
+ }
500
+ }
501
+ else {
502
+ allowTemplateTypes.push(...baseAllowTemplateTypes);
503
+ }
329
504
  const allDisplayTemplates = [];
330
505
  const filteredDisplayTemplates = [];
331
506
  for await (const displayTemplate of getAllStyles(client, cfg.debug, pageSize)) {
332
507
  allDisplayTemplates.push(displayTemplate);
333
- const templateType = displayTemplate.baseType ? 'base' : displayTemplate.nodeType ? 'node' : displayTemplate.contentType ? 'component' : 'unknown';
334
- if (isExcluded(displayTemplate.key, excludeTemplates, templates)) {
508
+ const templateType = getDisplayTemplateType(displayTemplate);
509
+ if (!shouldInclude(displayTemplate.key, allowTemplates, disallowTemplates)) {
335
510
  if (cfg.debug)
336
511
  process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style defintion key filtering active\n`));
337
512
  continue;
338
513
  }
339
- if (isExcluded(templateType, [], templateTypes)) {
514
+ if (!shouldInclude(templateType, allowTemplateTypes)) {
340
515
  if (cfg.debug)
341
516
  process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style type filtering is active\n`));
342
517
  continue;
343
518
  }
344
- if (displayTemplate.baseType && isExcluded(displayTemplate.baseType, excludeBaseTypes, baseTypes)) {
519
+ if (templateType === 'base' && !shouldInclude(displayTemplate.baseType, allowBaseTypes, disallowBaseTypes)) {
345
520
  if (cfg.debug)
346
521
  process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is defined at base type level and base type filtering is active\n`));
347
522
  continue;
348
523
  }
349
- if (displayTemplate.contentType && isExcluded(displayTemplate.contentType, excludeTypes, types)) {
524
+ if (templateType === 'component' && !shouldInclude(displayTemplate.contentType, allowTypes, disallowTypes)) {
350
525
  if (cfg.debug)
351
526
  process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is defined at component type level and component type filtering is active\n`));
352
527
  continue;
353
528
  }
354
- if (templateType != 'component' && types.length > 0) {
355
- if (cfg.debug)
356
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is targeting the ${templateType} level and component type selection is active\n`));
357
- continue;
358
- }
359
- if (displayTemplate.nodeType && isExcluded(displayTemplate.nodeType, excludeNodeTypes, nodes)) {
529
+ if (templateType === 'node' && !shouldInclude(displayTemplate.nodeType, allowNodeTypes, disallowNodeTypes)) {
360
530
  if (cfg.debug)
361
531
  process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is defined at node type level and node type filtering is active\n`));
362
532
  continue;
363
533
  }
364
- if (templateType != 'node' && nodes.length > 0) {
365
- if (cfg.debug)
366
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is targeting the ${templateType} level and node type selection is active\n`));
367
- continue;
368
- }
369
534
  filteredDisplayTemplates.push(displayTemplate);
370
535
  }
371
536
  if (cfg.debug)
@@ -376,8 +541,6 @@ async function getStyles(client, args, pageSize = 25) {
376
541
  };
377
542
  }
378
543
  async function* getAllStyles(client, debug = false, pageSize = 5) {
379
- if (client.runtimeCmsVersion == OptiCmsVersion.CMS12)
380
- return;
381
544
  let requestPageSize = pageSize;
382
545
  let requestPageIndex = 0;
383
546
  let totalItemCount = 0;
@@ -405,35 +568,268 @@ async function* getAllStyles(client, debug = false, pageSize = 5) {
405
568
  }
406
569
  } while (requestPageIndex < totalPages);
407
570
  }
408
- function isExcluded(value, exclusions, inclusions) {
409
- if (value == undefined || value == null)
410
- return false;
411
- return exclusions.includes(value) || (inclusions.length > 0 && !inclusions.includes(value));
412
- }
413
- async function getStyleFilePath(definition, opts) {
414
- if (definition.nodeType)
415
- return `nodes/${definition.nodeType}/${definition.key}/${definition.key}.opti-style.json`;
416
- if (definition.baseType)
417
- return `${definition.baseType}/styles/${definition.key}/${definition.key}.opti-style.json`;
418
- if (definition.contentType) {
419
- if (opts?.contentBaseType)
420
- return `${opts?.contentBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
421
- if (!opts?.client)
422
- throw new Error("Neither the contentBaseType, nor the ApiClient has been provided for a definition for a specific ContentType - unable to generate the path");
423
- const pageSize = 50;
424
- let resultsPage = await opts.client.contentTypesList({ query: { pageIndex: 0, pageSize } });
425
- const contentTypes = resultsPage.items ?? [];
426
- let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
427
- while (pagesRemaining > 0 && contentTypes.length < resultsPage.totalItemCount) {
428
- resultsPage = await opts.client.contentTypesList({ query: { pageIndex: resultsPage.pageIndex + 1, pageSize: resultsPage.pageSize } });
429
- contentTypes.push(...resultsPage.items);
430
- pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
431
- }
432
- const fetchedBaseType = contentTypes.filter(x => x.key == definition.contentType).map(x => x.baseType).at(0);
433
- if (fetchedBaseType)
434
- return `${fetchedBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
435
- }
436
- throw new Error(`Unable to resolve the target for the DisplayTemplate: ${definition.key}`);
571
+ class TypeFilesList extends Map {
572
+ getDisplayTemplateByKey(displayTemplateKey) {
573
+ for (const groupKey of this.keys()) {
574
+ const templates = this.get(groupKey)?.templates || [];
575
+ const displayTemplate = templates.find(x => x.key === displayTemplateKey);
576
+ if (displayTemplate)
577
+ return displayTemplate.data;
578
+ }
579
+ return undefined;
580
+ }
581
+ getDisplayTemplatePathsByKey(displayTemplateKey) {
582
+ for (const identifier of this.keys()) {
583
+ const groupInfo = this.get(identifier);
584
+ const templates = groupInfo?.templates || [];
585
+ const helperFile = groupInfo?.filePath;
586
+ const helperFolder = groupInfo?.fileFolder;
587
+ const info = templates.find(x => x.key === displayTemplateKey);
588
+ if (info)
589
+ return {
590
+ styleFile: info.file,
591
+ styleFolder: info.folder,
592
+ identifier,
593
+ helperFile,
594
+ helperFolder
595
+ };
596
+ }
597
+ return undefined;
598
+ }
599
+ }
600
+ async function toTypeFilesList(displayTemplates, client, basePath) {
601
+ if (isNonEmptyArray(displayTemplates)) {
602
+ // Get all content types we need to get the type file lists
603
+ const targetContentTypes = (await Promise.allSettled(displayTemplates.map(async (displayTemplate) => {
604
+ if (!displayTemplate.contentType)
605
+ return undefined;
606
+ return client.contentTypesGet({ path: { key: displayTemplate.contentType } });
607
+ }))).map(x => x.status == 'fulfilled' ? x.value : undefined).filter(isDefined);
608
+ // Simple helper to get the base type from the list
609
+ function getBaseTypeOf(contentTypeKey) {
610
+ if (!contentTypeKey)
611
+ return undefined;
612
+ return targetContentTypes.find(x => x.key === contentTypeKey)?.baseType;
613
+ }
614
+ // Now reduce the list into the Map we need
615
+ return displayTemplates.reduce((aggregator, displayTemplate) => {
616
+ const contentTypeBaseType = getBaseTypeOf(displayTemplate.contentType);
617
+ const { identifier, helperFile, helperFolder, styleFile, styleFolder } = getStyleFilePathsSync(displayTemplate, contentTypeBaseType, basePath, true);
618
+ const info = aggregator.get(identifier) ?? { filePath: helperFile, fileFolder: helperFolder, templates: [] };
619
+ info.templates.push({ key: displayTemplate.key, file: styleFile, data: displayTemplate, folder: styleFolder });
620
+ aggregator.set(identifier, info);
621
+ return aggregator;
622
+ }, new TypeFilesList());
623
+ }
624
+ return new TypeFilesList();
625
+ }
626
+
627
+ function normalizeMergePatch(value, omitKeys, requiredKeys = [], requiredSource) {
628
+ if (value === undefined || value === null)
629
+ return null;
630
+ if (Array.isArray(value))
631
+ return value.map(item => normalizeMergePatch(item, omitKeys));
632
+ if (typeof value !== 'object')
633
+ return value;
634
+ const output = Object.entries(value).reduce((acc, [key, entry]) => {
635
+ if (!omitKeys.includes(key))
636
+ acc[key] = normalizeMergePatch(entry, omitKeys);
637
+ return acc;
638
+ }, {});
639
+ const withRequiredKeys = requiredKeys.reduce((acc, key) => {
640
+ if (requiredSource && !Object.keys(acc).includes(key))
641
+ acc[key] = requiredSource[key];
642
+ return acc;
643
+ }, output);
644
+ return withRequiredKeys;
645
+ }
646
+ /**
647
+ * Computes a JSON Merge Patch (RFC 7396) that transforms `currentValue` into `newValue`.
648
+ *
649
+ * The resulting patch can be sent directly as the request body of an `application/merge-patch+json`
650
+ * request. Changed and added properties are included with their new value; removed properties are
651
+ * included with a `null` value (as required by RFC 7396); unchanged properties are omitted entirely.
652
+ *
653
+ * @see https://datatracker.ietf.org/doc/html/rfc7396
654
+ *
655
+ * @example Basic usage
656
+ * ```ts
657
+ * const current = { name: 'Alice', age: 30, role: 'user' }
658
+ * const updated = { name: 'Alice', age: 31 }
659
+ *
660
+ * generatePatch(current, updated)
661
+ * // => { age: 31, role: null }
662
+ * // `age` is replaced, `role` is removed (null), `name` is unchanged (omitted)
663
+ * ```
664
+ *
665
+ * @example Excluding keys from the patch
666
+ * ```ts
667
+ * const current = { id: '123', name: 'Alice', version: 1 }
668
+ * const updated = { id: '123', name: 'Bob', version: 2 }
669
+ *
670
+ * generatePatch(current, updated, { readonlyFields: ['id', 'version'] })
671
+ * // => { name: 'Bob' }
672
+ * // `id` and `version` are excluded even though they are present in the diff
673
+ * ```
674
+ *
675
+ * @example Always include required fields
676
+ * ```ts
677
+ * const current = { id: '123', name: 'Alice', version: 1 }
678
+ * const updated = { id: '123', name: 'Alice', version: 2 }
679
+ *
680
+ * generatePatch(current, updated, { requiredFields: ['id'] })
681
+ * // => { id: '123', version: 2 }
682
+ * // `id` is included even though it did not change
683
+ * ```
684
+ *
685
+ * @example Nested objects
686
+ * ```ts
687
+ * const current = { address: { city: 'Amsterdam', zip: '1000AA' } }
688
+ * const updated = { address: { city: 'Utrecht' } }
689
+ *
690
+ * generatePatch(current, updated)
691
+ * // => { address: { city: 'Utrecht', zip: null } }
692
+ * ```
693
+ *
694
+ * @param currentValue - The current state of the resource.
695
+ * @param newValue - The desired state of the resource.
696
+ * @param options - Optional configuration.
697
+ * @param options.readonlyFields - Keys that should never appear in the generated patch,
698
+ * regardless of whether they changed.
699
+ * @param options.requiredFields - Keys that should always appear in the generated patch,
700
+ * using values from `newValue`.
701
+ * @returns A {@link MergePatch} object ready to be serialised as an `application/merge-patch+json` body.
702
+ */
703
+ function generatePatch(currentValue, newValue, options = {}) {
704
+ const requiredKeys = options.requiredFields ?? [];
705
+ const omitKeys = (options.readonlyFields ?? []).filter(key => !requiredKeys.includes(key));
706
+ const patch = diff(currentValue, newValue);
707
+ return normalizeMergePatch(patch, omitKeys, requiredKeys, newValue);
708
+ }
709
+ /**
710
+ * Extracts all field paths from a {@link MergePatch} as dot-separated strings.
711
+ * Nested objects are flattened recursively; leaf values (including `null`) produce a path entry.
712
+ *
713
+ * @example
714
+ * ```ts
715
+ * getPatchFields({ a: { b: 0 }, c: 'value' })
716
+ * // => ['a.b', 'c']
717
+ * ```
718
+ *
719
+ * @param patch - The merge patch to extract fields from.
720
+ * @param prefix - Internal prefix used during recursion; omit when calling directly.
721
+ * @returns An array of dot-separated field path strings.
722
+ */
723
+ function getPatchFields(patch, prefix = '') {
724
+ return Object.entries(patch).flatMap(([key, value]) => {
725
+ const path = prefix ? `${prefix}.${key}` : key;
726
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
727
+ ? getPatchFields(value, path)
728
+ : [path];
729
+ });
730
+ }
731
+
732
+ const StylesPushCommand = {
733
+ command: "styles:push",
734
+ describe: "Push Visual Builder style definitions into the CMS (create/replace)",
735
+ builder: (yargs) => {
736
+ yargs.option('excludeTemplates', { alias: 'e', description: "Exclude these templates", string: true, type: 'array', demandOption: false, default: [] });
737
+ yargs.option("templates", { alias: 't', description: "Select only these templates", string: true, type: 'array', demandOption: false, default: [] });
738
+ return yargs;
739
+ },
740
+ handler: async (args) => {
741
+ const { _config: cfg, excludeTemplates, templates, ...opts } = parseArgs(args);
742
+ const client = createCmsClient(args);
743
+ const { styles: displayTemplates } = await getStyles(client, {
744
+ all: false,
745
+ baseTypes: [],
746
+ excludeBaseTypes: [],
747
+ excludeNodeTypes: [],
748
+ excludeTemplates: [],
749
+ excludeTypes: [],
750
+ nodes: [],
751
+ templates: [],
752
+ templateTypes: [],
753
+ types: [],
754
+ ...args
755
+ }, 50);
756
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/replace) DisplayStyles into Optimizely CMS\n`));
757
+ const styleDefinitionFiles = await glob("./**/*.opti-style.json", {
758
+ cwd: opts.components
759
+ });
760
+ const results = (await Promise.allSettled(styleDefinitionFiles.map(async (styleDefinitionFile) => {
761
+ const filePath = path.normalize(path.join(opts.components, styleDefinitionFile));
762
+ const styleDefinition = tryReadJsonFile(filePath, cfg.debug);
763
+ const styleKey = styleDefinition.key;
764
+ if (!styleKey) {
765
+ 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`));
766
+ return undefined;
767
+ }
768
+ if (excludeTemplates.includes(styleKey))
769
+ return undefined; // Skip excluded styles
770
+ if (templates.length > 0 && !templates.includes(styleKey))
771
+ return undefined; // Only include defined styles, if any
772
+ if (cfg.debug)
773
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Pushing: ${styleKey}\n`));
774
+ // Try to fetch the current template
775
+ const currentTemplate = displayTemplates.find(dt => dt.key === styleKey);
776
+ // Create / Replace the current template
777
+ const newTemplate = await (currentTemplate ? (async () => {
778
+ const patch = generatePatch(currentTemplate, styleDefinition, {
779
+ readonlyFields: ['key', 'created', 'lastModified', 'createdBy', 'lastModifiedBy']
780
+ });
781
+ if (!path || Object.entries(patch).length === 0)
782
+ return currentTemplate;
783
+ // @ts-expect-error There's a mis-match between the logic in the CMS and the contents of the
784
+ // OpenAPI Spec file.
785
+ return client.displayTemplatesPatch({ path: { key: styleKey }, body: patch });
786
+ })() :
787
+ client.displayTemplatesCreate({ body: styleDefinition }));
788
+ const missedFields = generatePatch(newTemplate, currentTemplate, {
789
+ readonlyFields: ['key', 'created', 'lastModified', 'createdBy', 'lastModifiedBy']
790
+ });
791
+ if (missedFields && Object.keys(missedFields).length > 0)
792
+ throw new Error(`The Display template ${styleKey} failed to update properties: ${getPatchFields(missedFields).join('; ')}`);
793
+ return newTemplate;
794
+ })));
795
+ const styles = new Table({
796
+ head: [
797
+ chalk.yellow(chalk.bold("Name")),
798
+ chalk.yellow(chalk.bold("Key")),
799
+ chalk.yellow(chalk.bold("Default")),
800
+ chalk.yellow(chalk.bold("Target"))
801
+ ],
802
+ colWidths: [31, 20, 9, 20],
803
+ colAligns: ["left", "left", "center", "left"]
804
+ });
805
+ results.forEach(result => {
806
+ if (result.status === 'fulfilled') {
807
+ const tpl = result.value;
808
+ styles.push([
809
+ tpl.displayName,
810
+ tpl.key,
811
+ tpl.isDefault ? figures.tick : figures.cross,
812
+ tpl.contentType ? `${tpl.contentType} (C)` : tpl.baseType ? `${tpl.baseType} (B)` : `${tpl.nodeType} (N)`
813
+ ]);
814
+ }
815
+ else {
816
+ process.stderr.write(`Error processing DisplayTemplate: ${result.reason}\n`);
817
+ }
818
+ });
819
+ process.stdout.write(styles.toString() + "\n");
820
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
821
+ }
822
+ };
823
+ function tryReadJsonFile(filePath, debug = false) {
824
+ try {
825
+ if (debug)
826
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Reading style definition from ${filePath}\n`));
827
+ return JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' }));
828
+ }
829
+ catch (e) {
830
+ process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} Error while reading ${filePath}\n`));
831
+ }
832
+ return undefined;
437
833
  }
438
834
 
439
835
  const StylesListCommand = {
@@ -469,158 +865,147 @@ const StylesListCommand = {
469
865
  }
470
866
  };
471
867
 
868
+ function ucFirst(current) {
869
+ if (typeof current != 'string')
870
+ throw new Error("Only strings can be transformed");
871
+ if (current == "")
872
+ return current;
873
+ return current[0]?.toUpperCase() + current.substring(1);
874
+ }
875
+
472
876
  const StylesPullCommand = {
473
877
  command: "styles:pull",
474
878
  describe: "Create Visual Builder style definitions from the CMS",
475
879
  builder: (yargs) => {
476
880
  const newYargs = stylesBuilder(yargs);
477
881
  newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
478
- newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true });
882
+ newYargs.option("definitions", { alias: 'u', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true });
479
883
  return newYargs;
480
884
  },
481
885
  handler: async (args) => {
482
- const { _config: cfg, components: basePath, force, definitions } = parseArgs(args);
886
+ const { _config: cfg, components: basePath, force} = parseArgs(args);
483
887
  const client = createCmsClient(args);
484
- if (client.runtimeCmsVersion == OptiCmsVersion.CMS12) {
485
- process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
486
- return;
487
- }
488
888
  const { styles: filteredResults } = await getStyles(client, args);
489
- //#region Create & Write opti-style.json files
490
889
  process.stdout.write(chalk.gray(`${figures.arrowRight} Start creating .opti-style.json files\n`));
491
- const typeFiles = {};
492
- const updatedTemplates = (await Promise.all(filteredResults.map(async (displayTemplate) => {
493
- let itemPath = undefined;
494
- let targetType;
495
- let typesPath;
496
- if (displayTemplate.nodeType) {
497
- itemPath = path.join(basePath, 'nodes', displayTemplate.nodeType, displayTemplate.key);
498
- typesPath = path.join(basePath, 'nodes', displayTemplate.nodeType);
499
- targetType = 'node/' + displayTemplate.nodeType;
500
- }
501
- else if (displayTemplate.baseType) {
502
- itemPath = path.join(basePath, displayTemplate.baseType, 'styles', displayTemplate.key);
503
- typesPath = path.join(basePath, displayTemplate.baseType, 'styles');
504
- targetType = 'base/' + displayTemplate.baseType;
505
- }
506
- else if (displayTemplate.contentType) {
507
- const contentType = await client.contentTypesGet({ path: { key: displayTemplate.contentType ?? '-' } });
508
- itemPath = path.join(basePath, contentType.baseType, contentType.key);
509
- typesPath = path.join(basePath, contentType.baseType, contentType.key);
510
- targetType = 'content/' + displayTemplate.contentType;
511
- }
512
- if (!fs.existsSync(itemPath))
513
- fs.mkdirSync(itemPath, { recursive: true });
514
- // Write Style JSON
515
- const filePath = path.join(itemPath, `${displayTemplate.key}.opti-style.json`);
516
- const outputTemplate = { ...displayTemplate };
517
- if (outputTemplate.createdBy)
518
- delete outputTemplate.createdBy;
519
- if (outputTemplate.lastModifiedBy)
520
- delete outputTemplate.lastModifiedBy;
521
- if (outputTemplate.created)
522
- delete outputTemplate.created;
523
- if (outputTemplate.lastModified)
524
- delete outputTemplate.lastModified;
525
- if (fs.existsSync(filePath)) {
526
- if (!force) {
527
- if (cfg.debug)
528
- process.stdout.write(chalk.gray(`${figures.cross} Skipping style file for ${displayTemplate.key} - File already exists\n`));
529
- }
530
- else {
531
- if (cfg.debug) {
532
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting style file for ${displayTemplate.key}\n`));
533
- }
534
- fs.writeFileSync(filePath, JSON.stringify(outputTemplate, undefined, 2));
535
- }
890
+ const styleFiles = await toTypeFilesList(filteredResults, client, basePath);
891
+ const updatedTemplates = [];
892
+ for (const groupIdentifier of styleFiles.keys()) {
893
+ const displayTemplateGroup = styleFiles.get(groupIdentifier);
894
+ for (const { file: filePath, data: displayTemplate } of (displayTemplateGroup?.templates || [])) {
895
+ // Write JSON to disk
896
+ var updatedJson = await createDisplayTemplateFile(displayTemplate, filePath, force, cfg.debug);
897
+ if (updatedJson)
898
+ updatedTemplates.push(displayTemplate.key);
536
899
  }
537
- else {
538
- if (cfg.debug)
539
- process.stdout.write(chalk.gray(`${figures.arrowRight} Creating style file for ${displayTemplate.key} in ${filePath}\n`));
540
- fs.writeFileSync(filePath, JSON.stringify(outputTemplate, undefined, 2));
541
- }
542
- if (!typeFiles[targetType]) {
543
- typeFiles[targetType] = {
544
- filePath: path.join(typesPath, 'displayTemplates.ts'),
545
- templates: []
546
- };
900
+ // Write template to disk
901
+ void await createDisplayTemplateHelper(displayTemplateGroup, groupIdentifier, force, cfg.debug);
902
+ }
903
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + ` Created/updated style definitions for ${updatedTemplates.join(', ')}`)) + "\n");
904
+ }
905
+ };
906
+ async function createDisplayTemplateFile(displayTemplate, filePath, force = false, debug = false) {
907
+ let updated = false;
908
+ // Build local style data
909
+ const outputTemplate = { ...displayTemplate };
910
+ if (outputTemplate.createdBy)
911
+ delete outputTemplate.createdBy;
912
+ if (outputTemplate.lastModifiedBy)
913
+ delete outputTemplate.lastModifiedBy;
914
+ if (outputTemplate.created)
915
+ delete outputTemplate.created;
916
+ if (outputTemplate.lastModified)
917
+ delete outputTemplate.lastModified;
918
+ // Write file to disk
919
+ if (fs.existsSync(filePath)) {
920
+ if (!force) {
921
+ if (debug)
922
+ process.stdout.write(chalk.gray(`${figures.cross} Skipping style file for ${displayTemplate.key} - File already exists\n`));
923
+ }
924
+ else {
925
+ if (debug) {
926
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting style file for ${displayTemplate.key}\n`));
547
927
  }
548
- typeFiles[targetType].templates.push({ file: filePath, data: displayTemplate });
549
- return displayTemplate.key;
550
- }))).filter(x => x);
551
- //#endregion
552
- //#region Create needed definition files
553
- if (definitions) {
554
- process.stdout.write(chalk.gray(`${figures.arrowRight} Start creating displayTemplates.ts files\n`));
555
- for (const targetId of Object.getOwnPropertyNames(typeFiles)) {
556
- const { filePath: typeFilePath, templates } = typeFiles[targetId];
557
- if (fs.existsSync(typeFilePath)) {
558
- if (!force) {
559
- if (cfg.debug)
560
- process.stdout.write(chalk.gray(`${figures.cross} Skipped writing definition file for ${targetId} - it already exists\n`));
561
- continue;
562
- }
563
- if (cfg.debug)
564
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting definition file for ${targetId} - ${typeFilePath}\n`));
565
- }
566
- else if (cfg.debug)
567
- process.stdout.write(chalk.gray(`${figures.arrowRight} Creating definition file for ${targetId} - ${typeFilePath}\n`));
568
- // Write Style definition
569
- const imports = [
570
- 'import type { LayoutProps } from "@remkoj/optimizely-cms-react"',
571
- 'import type { ReactNode, JSX } from "react"'
572
- ];
573
- const typeContents = [];
574
- const props = [];
575
- let typeId = targetId.split('/', 2)[1];
576
- templates.forEach(({ file: displayTemplateFile, data: displayTemplate }) => {
577
- const importPath = path.relative(path.dirname(typeFilePath), displayTemplateFile).replaceAll('\\', '/');
578
- imports.push(`import type ${displayTemplate.key}Styles from "./${importPath}"`);
579
- typeContents.push(`export type ${displayTemplate.key}Props = LayoutProps<typeof ${displayTemplate.key}Styles>`);
580
- typeContents.push(`export type ${displayTemplate.key}ComponentProps<DT extends Record<string, any> = Record<string, any>> = {
581
- data: DT
582
- layoutProps: ${displayTemplate.key}Props | undefined
583
- } & JSX.IntrinsicElements['div']`);
584
- typeContents.push(`export type ${displayTemplate.key}Component<DT extends Record<string, any> = Record<string, any>> = (props: ${displayTemplate.key}ComponentProps<DT>) => ReactNode`);
585
- typeContents.push('');
586
- props.push(`${displayTemplate.key}Props`);
587
- if (!typeId)
588
- typeId = displayTemplate.nodeType ?? displayTemplate.baseType ?? displayTemplate.contentType;
589
- });
590
- if (typeId) {
591
- typeId = ucFirst$1(typeId);
592
- typeContents.push('');
593
- typeContents.push(`export type ${typeId}LayoutProps = ${props.join(' | ')}
594
- export type ${typeId}ComponentProps<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = {
595
- data: DT
596
- layoutProps: LP | undefined
597
- } & JSX.IntrinsicElements['div']
598
-
599
- export type ${typeId}Component<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = (props: ${typeId}ComponentProps<DT,LP>) => ReactNode
600
-
601
- export function isDefaultProps(props?: ${typeId}LayoutProps | null) : props is ${templates.filter(t => t.data.isDefault).at(0)?.data?.key}Props
928
+ fs.writeFileSync(filePath, JSON.stringify(outputTemplate, undefined, 2));
929
+ updated = true;
930
+ }
931
+ }
932
+ else {
933
+ if (debug)
934
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating style file for ${displayTemplate.key} in ${filePath}\n`));
935
+ fs.writeFileSync(filePath, JSON.stringify(outputTemplate, undefined, 2));
936
+ updated = true;
937
+ }
938
+ return updated;
939
+ }
940
+ async function createDisplayTemplateHelper(typeFile, typeFileId, force = false, debug = false) {
941
+ const prefix = '//not-modified - Remove this line when making change to prevent it from being updated by the CLI tools';
942
+ const { filePath: typeFilePath, templates } = typeFile;
943
+ const shouldWrite = await fsAsync.readFile(typeFilePath, { encoding: "utf-8" }).then(data => data.startsWith('//not-modified')).catch((e) => {
944
+ if (e?.code === 'ENOENT')
945
+ return true;
946
+ if (debug)
947
+ process.stdout.write(chalk.redBright(chalk.bold(`${figures.cross} Unexpected error while reading display template file\n`)));
948
+ return false;
949
+ });
950
+ if (!shouldWrite) {
951
+ if (!force) {
952
+ if (debug)
953
+ process.stdout.write(chalk.gray(`${figures.cross} Skipped writing definition file for ${typeFileId} - it already exists and has been modified\n`));
954
+ return false;
955
+ }
956
+ else {
957
+ if (debug)
958
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Forcefully overwriting definition file for ${typeFileId} - ${typeFilePath}\n`));
959
+ }
960
+ }
961
+ else if (debug)
962
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating or updating definition file for ${typeFileId} - ${typeFilePath}\n`));
963
+ // Write Style definition
964
+ const imports = [
965
+ 'import type { LayoutProps, LayoutPropsSettingKeys, LayoutPropsSettingValues, CmsComponentProps } from "@remkoj/optimizely-cms-react"',
966
+ 'import type { JSX, ComponentType } from "react"'
967
+ ];
968
+ const typeContents = [];
969
+ const props = [];
970
+ let typeId = typeFileId.split('/', 2)[1];
971
+ templates.forEach(({ file: displayTemplateFile, data: displayTemplate }) => {
972
+ const importPath = path.relative(path.dirname(typeFilePath), displayTemplateFile).replaceAll('\\', '/');
973
+ imports.push(`import type ${displayTemplate.key}Styles from "./${importPath}"`);
974
+ typeContents.push(`export type ${displayTemplate.key}Props = LayoutProps<typeof ${displayTemplate.key}Styles>`);
975
+ typeContents.push(`export type ${displayTemplate.key}Keys = LayoutPropsSettingKeys<${displayTemplate.key}Props>`);
976
+ typeContents.push(`export type ${displayTemplate.key}Options<K extends ${displayTemplate.key}Keys> = LayoutPropsSettingValues<${displayTemplate.key}Props, K>`);
977
+ typeContents.push(`export type ${displayTemplate.key}ComponentProps<DT extends Record<string, unknown> = Record<string, unknown>> = Omit<CmsComponentProps<DT, ${displayTemplate.key}Props>,'children'> & JSX.IntrinsicElements['div']`);
978
+ typeContents.push(`export type ${displayTemplate.key}Component<DT extends Record<string, unknown> = Record<string, unknown>> = ComponentType<${displayTemplate.key}ComponentProps<DT>>`);
979
+ typeContents.push('');
980
+ props.push(`${displayTemplate.key}Props`);
981
+ if (!typeId)
982
+ typeId = displayTemplate.nodeType ?? displayTemplate.baseType ?? displayTemplate.contentType;
983
+ });
984
+ if (typeId) {
985
+ typeId = ucFirst(typeId);
986
+ typeContents.push(`export type ${typeId}LayoutProps = ${props.join(' | ')}
987
+ export type ${typeId}LayoutKeys = LayoutPropsSettingKeys<${typeId}LayoutProps>
988
+ export type ${typeId}LayoutOptions<K extends ${typeId}LayoutKeys> = LayoutPropsSettingValues<${typeId}LayoutProps,K>
989
+ export type ${typeId}ComponentProps<DT extends Record<string, unknown> = Record<string, unknown>> = Omit<CmsComponentProps<DT, ${typeId}LayoutProps>,'children'> & JSX.IntrinsicElements['div']
990
+ export type ${typeId}Component<DT extends Record<string, unknown> = Record<string, unknown>> = ComponentType<${typeId}ComponentProps<DT>>`);
991
+ const defaultTemplate = templates.find(t => t.data.isDefault);
992
+ if (defaultTemplate) {
993
+ typeContents.push(`
994
+ export function isDefaultProps(props?: ${typeId}LayoutProps | null) : props is ${defaultTemplate.data?.key}Props
602
995
  {
603
- return props?.template == "${templates.filter(t => t.data.isDefault).at(0)?.data?.key}"
996
+ return props?.template == "${defaultTemplate.data?.key}"
604
997
  }`);
605
- templates.forEach(t => {
606
- typeContents.push(`
998
+ }
999
+ templates.forEach(t => {
1000
+ typeContents.push(`
607
1001
  export function is${t.data.key}Props(props?: ${typeId}LayoutProps | null) : props is ${t.data.key}Props
608
1002
  {
609
1003
  return props?.template == "${t.data.key}"
610
1004
  }`);
611
- });
612
- }
613
- fs.writeFileSync(typeFilePath, imports.join("\n") + "\n\n" + typeContents.join("\n"));
614
- }
615
- }
616
- //#endregion
617
- process.stdout.write(chalk.green(chalk.bold(figures.tick + ` Created/updated style definitions for ${updatedTemplates.join(', ')}`)) + "\n");
1005
+ });
618
1006
  }
619
- };
620
- function ucFirst$1(input) {
621
- if (typeof (input) != 'string' || input.length < 1)
622
- return input;
623
- return input[0].toUpperCase() + input.substring(1);
1007
+ void await fsAsync.writeFile(typeFilePath, prefix + "\n" + imports.join("\n") + "\n\n" + typeContents.join("\n"));
1008
+ return true;
624
1009
  }
625
1010
 
626
1011
  // Node JS and 3rd Party libraries
@@ -633,10 +1018,6 @@ const StylesCreateCommand = {
633
1018
  handler: async (args) => {
634
1019
  const { components: basePath } = parseArgs(args);
635
1020
  const client = createCmsClient(args);
636
- if (client.runtimeCmsVersion == OptiCmsVersion.CMS12) {
637
- process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
638
- return;
639
- }
640
1021
  const allowedBaseTypes = ['section', 'component', 'experience'];
641
1022
  // Prepare
642
1023
  process.stdout.write(chalk.yellowBright(chalk.bold(`Reading current information from Optimizely CMS\n`)));
@@ -685,10 +1066,10 @@ const StylesCreateCommand = {
685
1066
  definition[type] = typeId;
686
1067
  definition['settings'] = {};
687
1068
  const contentBaseType = type == "contentType" ? contentTypes.filter(x => x.key == typeId).map(x => x.baseType).at(0) : undefined;
688
- const styleFilePath = await getStyleFilePath(definition, { contentBaseType, client });
689
- if (!fs.existsSync(path.join(basePath, path.dirname(styleFilePath))))
690
- fs.mkdirSync(path.join(basePath, path.dirname(styleFilePath)), { recursive: true });
691
- if (fs.existsSync(path.join(basePath, styleFilePath))) {
1069
+ const { styleFile: styleFilePath, styleFolder: styleFileFolder } = await getStyleFilePaths(definition, { contentBaseType, client, basePath });
1070
+ if (!fs.existsSync(styleFileFolder))
1071
+ fs.mkdirSync(styleFileFolder, { recursive: true });
1072
+ if (fs.existsSync(styleFilePath)) {
692
1073
  const overwrite = await confirm({ message: "The style definition file already exists, do you want to overwrite it?" });
693
1074
  if (!overwrite) {
694
1075
  process.stdout.write("\n");
@@ -697,10 +1078,10 @@ const StylesCreateCommand = {
697
1078
  }
698
1079
  }
699
1080
  // @ToDo: Add properties
700
- fs.writeFileSync(path.join(basePath, styleFilePath), JSON.stringify(definition, undefined, 4));
701
- process.stdout.write(chalk.yellowBright(chalk.bold(`\nWritten style defintion template to: ${path.normalize(path.join(basePath, styleFilePath))}\n`)));
1081
+ fs.writeFileSync(styleFilePath, JSON.stringify(definition, undefined, 4));
1082
+ process.stdout.write(chalk.yellowBright(chalk.bold(`\nWritten style defintion template to: ${path.normalize(styleFilePath)}\n`)));
702
1083
  if (await confirm({ message: "Do you want to publish this style into Optimizely CMS?" })) {
703
- const response = await client.displayTemplatesPut({ body: definition, path: { key: definition.key } }).catch(_ => undefined);
1084
+ const response = await client.displayTemplatesCreate({ body: definition }).catch(_ => undefined);
704
1085
  if (!response) {
705
1086
  process.stderr.write(chalk.redBright(chalk.bold(`\[ERROR] Unable to create style definition ${definition.displayName} in Optimizely CMS\n`)));
706
1087
  }
@@ -720,6 +1101,91 @@ const StylesCreateCommand = {
720
1101
  }
721
1102
  };
722
1103
 
1104
+ const StylesDeleteCommand = {
1105
+ command: "styles:delete",
1106
+ describe: "Remove Visual Builder style definitions from the CMS",
1107
+ builder: (yargs) => {
1108
+ const newYargs = stylesBuilder(yargs);
1109
+ newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
1110
+ newYargs.option('withStyleFile', { alias: 'w', description: "Delete the .opti-style.json file as weill", boolean: true, type: 'boolean', demandOption: false, default: true });
1111
+ newYargs.option("definitions", { alias: 'u', description: "Update typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true });
1112
+ return newYargs;
1113
+ },
1114
+ handler: async (args) => {
1115
+ const { components: basePath } = parseArgs(args);
1116
+ const client = createCmsClient(args);
1117
+ const { styles, all: allStyles } = await getStyles(client, args, 100);
1118
+ if (styles.findIndex(x => x.isDefault) >= 0)
1119
+ 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`))));
1120
+ if (!args.force) {
1121
+ process.stdout.write(`This will remove the following display templates\n`);
1122
+ for (const style of styles) {
1123
+ process.stdout.write(` ${figures.arrowRight} ${style.displayName || style.key} [Key: ${style.key}; Default: ${style.isDefault ? 'Yes' : 'No'}]\n`);
1124
+ }
1125
+ process.stdout.write(`\nRun with -f to actually preform removal\n`);
1126
+ return;
1127
+ }
1128
+ const keysToDelete = styles.map(displayTemplate => displayTemplate.key);
1129
+ const styleHelpers = await toTypeFilesList(allStyles, client, basePath);
1130
+ for (const displayTemplate of styles) {
1131
+ const { styleFile: styleFilePath, styleFolder: itemPath, identifier: targetType, helperFolder: typesPath } = styleHelpers.getDisplayTemplatePathsByKey(displayTemplate.key);
1132
+ // Remove style file
1133
+ if (args.withStyleFile) {
1134
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing *.opti-style.json file for ${displayTemplate.displayName} [${displayTemplate.key}]\n`));
1135
+ await fsAsync.rm(styleFilePath, { force: false, recursive: false, maxRetries: 1 }).catch((e) => {
1136
+ if (e?.code === 'ENOENT') // The file can't be deleted as it does not exist - that's the desired state
1137
+ return;
1138
+ throw e;
1139
+ });
1140
+ await fsAsync.rmdir(itemPath, { recursive: false, maxRetries: 1 }).catch((e) => {
1141
+ if (e?.code === 'ENOTEMPTY') {
1142
+ process.stdout.write(chalk.yellowBright(`${figures.warning} The folder is not empty, please remove ${itemPath} manually if needed\n`));
1143
+ return;
1144
+ }
1145
+ else if (e?.code === 'ENOENT')
1146
+ return;
1147
+ else
1148
+ throw e;
1149
+ });
1150
+ }
1151
+ // Remove/update typescript helper
1152
+ if (args.definitions && styleHelpers.has(targetType)) {
1153
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing/updating displayTemplates.ts file for ${displayTemplate.displayName} [${displayTemplate.key}]\n`));
1154
+ const remainingTemplates = styleHelpers[targetType].templates.filter(x => !keysToDelete.includes(x.key));
1155
+ if (remainingTemplates.length === 0) {
1156
+ await fsAsync.rm(styleHelpers[targetType].filePath, { force: false, recursive: false, maxRetries: 1 }).catch((e) => {
1157
+ if (e?.code === 'ENOENT') // The file can't be deleted as it does not exist - that's the desired state
1158
+ return;
1159
+ throw e;
1160
+ });
1161
+ await fsAsync.rmdir(typesPath, { recursive: false, maxRetries: 1 }).catch((e) => {
1162
+ if (e?.code === 'ENOTEMPTY') {
1163
+ process.stdout.write(chalk.yellowBright(`${figures.warning} The folder is not empty, please remove ${typesPath} manually if needed\n`));
1164
+ return;
1165
+ }
1166
+ else if (e?.code === 'ENOENT')
1167
+ return;
1168
+ else
1169
+ throw e;
1170
+ });
1171
+ }
1172
+ else {
1173
+ const newEntry = {
1174
+ filePath: styleHelpers[targetType].filePath,
1175
+ templates: remainingTemplates
1176
+ };
1177
+ if (!await createDisplayTemplateHelper(newEntry, targetType, false, false))
1178
+ process.stdout.write(chalk.yellowBright(`${figures.warning} The display template was not updated, please update ${newEntry.filePath} manually`));
1179
+ }
1180
+ }
1181
+ // Actually remove from CMS
1182
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing the display template ${displayTemplate.displayName} [${displayTemplate.key}] from Optimizely CMS\n`));
1183
+ void await client.displayTemplatesDelete({ path: { key: displayTemplate.key } });
1184
+ }
1185
+ process.stdout.write("\n" + chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1186
+ }
1187
+ };
1188
+
723
1189
  const TypesPullCommand = {
724
1190
  command: "types:pull",
725
1191
  describe: "Pull content type definition files into the project",
@@ -729,40 +1195,237 @@ const TypesPullCommand = {
729
1195
  return newArgs;
730
1196
  },
731
1197
  handler: async (args) => {
732
- const { _config: { debug }, components: basePath, force } = parseArgs(args);
1198
+ const { _config: { debug }, components: basePath, path: projectPath, force } = parseArgs(args);
733
1199
  const client = createCmsClient(args);
734
1200
  const { contentTypes } = await getContentTypes(client, args);
735
1201
  const updatedTypes = contentTypes.map(contentType => {
736
- const typePath = path.join(basePath, contentType.baseType, contentType.key);
737
- const typeFile = path.join(typePath, `${contentType.key}.opti-type.json`);
738
- if (!fs.existsSync(typePath))
739
- fs.mkdirSync(typePath, { recursive: true });
1202
+ const { path: path$1, typeFile } = getContentTypePaths(contentType, basePath);
1203
+ if (!fs.existsSync(path$1))
1204
+ fs.mkdirSync(path$1, { recursive: true });
740
1205
  if (fs.existsSync(typeFile) && !force) {
741
1206
  if (debug)
742
1207
  process.stdout.write(chalk.yellow(`${figures.cross} Skipping type definition for ${contentType.displayName} (${contentType.key}) - File already exists\n`));
743
1208
  return contentType.key;
744
1209
  }
745
- const outContentType = { ...contentType };
746
- if (outContentType.source || outContentType.source == "")
747
- delete outContentType.source;
748
- if (outContentType.features)
749
- delete outContentType.features;
750
- if (outContentType.usage)
751
- delete outContentType.usage;
752
- if (outContentType.lastModifiedBy)
753
- delete outContentType.lastModifiedBy;
754
- if (outContentType.lastModified)
755
- delete outContentType.lastModified;
756
- if (outContentType.created)
757
- delete outContentType.created;
758
- if (debug)
759
- process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key})\n`));
760
- fs.writeFileSync(typeFile, JSON.stringify(outContentType, undefined, 2));
761
- return contentType.key;
762
- }).filter(x => x);
763
- process.stdout.write(chalk.green(chalk.bold(`${figures.tick} Created/updated type definitions for ${updatedTypes.join(', ')}\n`)));
1210
+ const outContentType = { ...contentType };
1211
+ if (outContentType.source || outContentType.source == "")
1212
+ delete outContentType.source;
1213
+ if (outContentType.lastModifiedBy || outContentType.lastModifiedBy == "")
1214
+ delete outContentType.lastModifiedBy;
1215
+ if (outContentType.lastModified)
1216
+ delete outContentType.lastModified;
1217
+ if (outContentType.created)
1218
+ delete outContentType.created;
1219
+ for (const propName of Object.getOwnPropertyNames(outContentType.properties ?? {})) {
1220
+ if (!["content", "contentReference"].includes(outContentType.properties[propName].type)) {
1221
+ if (isEmptyArray(outContentType.properties[propName].allowedTypes))
1222
+ delete outContentType.properties[propName].allowedTypes;
1223
+ if (isEmptyArray(outContentType.properties[propName].restrictedTypes))
1224
+ delete outContentType.properties[propName].restrictedTypes;
1225
+ }
1226
+ if (outContentType.properties[propName].type == 'array' && !["content", "contentReference"].includes(outContentType.properties[propName].items?.type)) {
1227
+ if (isEmptyArray(outContentType.properties[propName].items.allowedTypes))
1228
+ delete outContentType.properties[propName].items.allowedTypes;
1229
+ if (isEmptyArray(outContentType.properties[propName].items.restrictedTypes))
1230
+ delete outContentType.properties[propName].items.restrictedTypes;
1231
+ }
1232
+ }
1233
+ if (debug)
1234
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key}) into ${path.relative(projectPath, path$1)}\n`));
1235
+ fs.writeFileSync(typeFile, JSON.stringify(outContentType, undefined, 2));
1236
+ return contentType.key;
1237
+ }).filter(x => x);
1238
+ process.stdout.write(chalk.green(chalk.bold(`${figures.tick} Created/updated type definitions for ${updatedTypes.join(', ')}\n`)));
1239
+ }
1240
+ };
1241
+
1242
+ const deepmerge$1 = createDeepMerge();
1243
+ async function loadSchema(client, schemaName) {
1244
+ const schemas = Array.isArray(schemaName) ? schemaName : [schemaName];
1245
+ process.stdout.write(`${figures.arrowRight} Downloading current JSON Schema\n`);
1246
+ const spec = await client.getOpenApiSpec();
1247
+ const specSchemas = spec.components?.schemas ?? {};
1248
+ process.stdout.write(` ${figures.tick} Complete\n`);
1249
+ const result = [];
1250
+ process.stdout.write(`\n${figures.arrowRight} Constructing schema for ${schemas.join(', ')}\n`);
1251
+ for await (const schema of schemas)
1252
+ if (specSchemas[schema]) {
1253
+ const definitions = {};
1254
+ const processedSchema = processSchema(specSchemas[schema], definitions, spec);
1255
+ const jsonSchema = {
1256
+ //"$schema": "https://json-schema.org/draft-07/schema",
1257
+ "$id": new URL(`schema/${schema}`, client.getSchemaItemBase()).href,
1258
+ type: "object",
1259
+ title: schema,
1260
+ ...processedSchema,
1261
+ definitions
1262
+ };
1263
+ result.push({
1264
+ title: schema,
1265
+ schema: postProcessDefintions(jsonSchema)
1266
+ });
1267
+ process.stdout.write(` ${figures.tick} Constructed schema of ${schema}\n`);
1268
+ }
1269
+ return result;
1270
+ }
1271
+ function getValidator(schemaObject) {
1272
+ const ajv = new Ajv({
1273
+ discriminator: true
1274
+ });
1275
+ addFormats(ajv);
1276
+ return ajv.compile(schemaObject);
1277
+ }
1278
+ function postProcessDefintions(jsonSchema) {
1279
+ if (!jsonSchema.definitions)
1280
+ return jsonSchema;
1281
+ for (const definitionName in jsonSchema.definitions) {
1282
+ if ((definitionName.endsWith("Property") || definitionName.endsWith("ListItem")) && jsonSchema.definitions[definitionName].properties) {
1283
+ const type = jsonSchema.definitions[definitionName].properties?.type;
1284
+ if (isRefSchema(type)) {
1285
+ const typeSchema = resolveRefSchema(type, jsonSchema);
1286
+ if (typeSchema && typeSchema.type === 'string' && Array.isArray(typeSchema.enum)) {
1287
+ let typeValue = definitionName.substring(0, definitionName.length - 8);
1288
+ typeValue = typeValue[0].toLowerCase() + typeValue.substring(1);
1289
+ typeValue = typeValue === 'list' ? 'array' : typeValue;
1290
+ typeValue = typeValue === 'jsonString' ? 'json' : typeValue;
1291
+ if (typeSchema.enum.includes(typeValue)) {
1292
+ const newTypeDef = deepmerge$1({}, typeSchema);
1293
+ newTypeDef.enum = newTypeDef.enum.filter((x) => x === typeValue);
1294
+ jsonSchema.definitions[definitionName].properties.type = newTypeDef;
1295
+ }
1296
+ }
1297
+ }
1298
+ }
1299
+ }
1300
+ return jsonSchema;
1301
+ }
1302
+ /**
1303
+ * Schema normalization to tranform the schema from a .Net generated OpenAPI Schema
1304
+ * to a AJV compatible JSON Schema
1305
+ *
1306
+ * @see https://ajv.js.org/
1307
+ * @param schema
1308
+ * @param defs
1309
+ * @param spec
1310
+ * @returns
1311
+ */
1312
+ function processSchema(schema, defs, spec, mergeAllOf = true) {
1313
+ if (Array.isArray(schema))
1314
+ return schema.map(s => processSchema(s, defs, spec, mergeAllOf));
1315
+ if (typeof schema !== 'object' || schema === null)
1316
+ return schema;
1317
+ const props = Object.getOwnPropertyNames(schema);
1318
+ if (props.length === 1 && props[0] === '$ref') {
1319
+ const ref = schema['$ref'];
1320
+ if (isLocalRef(ref)) {
1321
+ const refName = getLocalRefName(ref);
1322
+ if (!defs[refName]) {
1323
+ const refItem = resolveLocalRef(ref, spec);
1324
+ defs[refName] = processSchema(refItem, defs, spec, mergeAllOf);
1325
+ }
1326
+ const newRef = `#/definitions/${refName}`;
1327
+ return { "$ref": newRef };
1328
+ }
1329
+ else {
1330
+ return schema;
1331
+ }
1332
+ }
1333
+ else if (mergeAllOf && props.includes('allOf') && Array.isArray(schema['allOf'])) {
1334
+ // process all of
1335
+ const newObject = props.reduce((generated, propName) => {
1336
+ if (propName != 'allOf')
1337
+ generated[propName] = schema[propName];
1338
+ return generated;
1339
+ }, {});
1340
+ const allOfSchemas = schema['allOf'].map((subschema) => {
1341
+ const resolvedSchema = isRefSchema(subschema) ? resolveRefSchema(subschema, spec) : subschema;
1342
+ const resolved = processSchema(resolvedSchema, defs, spec, mergeAllOf);
1343
+ return resolved;
1344
+ });
1345
+ const merged = allOfSchemas.reduce((previous, current) => deepmerge$1(previous, current), newObject);
1346
+ if (newObject['description'])
1347
+ merged['description'] = newObject['description'];
1348
+ if (newObject['title'])
1349
+ merged['title'] = newObject['title'];
1350
+ return merged;
1351
+ }
1352
+ else {
1353
+ const newSchema = {};
1354
+ for (const key of props) {
1355
+ if (key === 'pattern' && typeof schema[key] === 'string') {
1356
+ newSchema[key] = safeCreateUnicodeRegExp(schema[key]);
1357
+ /*} else if (key === 'readOnly' && schema[key] === true) {
1358
+ return undefined*/
1359
+ }
1360
+ else if (['additionalProperties', 'required', 'properties', 'discriminator'].includes(key) && typeof schema[key] !== 'object') ;
1361
+ else if (key === 'discriminator' && !Array.isArray(schema['oneOf'])) ;
1362
+ else if (key === 'discriminator' && !schema['type']) {
1363
+ newSchema.type = 'object';
1364
+ }
1365
+ else {
1366
+ const keyVal = processSchema(schema[key], defs, spec, mergeAllOf);
1367
+ if (keyVal !== undefined)
1368
+ newSchema[key] = keyVal;
1369
+ }
1370
+ }
1371
+ if (newSchema['oneOf'] && Array.isArray(newSchema['oneOf']) && newSchema['nullable']) {
1372
+ delete newSchema['nullable'];
1373
+ }
1374
+ return newSchema;
764
1375
  }
765
- };
1376
+ }
1377
+ function isRefSchema(schema) {
1378
+ if (typeof schema != 'object' || schema == null)
1379
+ return false;
1380
+ return Object.getOwnPropertyNames(schema).length === 1 && typeof schema['$ref'] === 'string';
1381
+ }
1382
+ function resolveRefSchema(schema, spec) {
1383
+ if (!isRefSchema(schema))
1384
+ return undefined;
1385
+ return resolveLocalRef(schema['$ref'], spec);
1386
+ }
1387
+ function isLocalRef(ref) {
1388
+ return typeof ref === 'string' && ref.startsWith('#');
1389
+ }
1390
+ function getLocalRefName(ref) {
1391
+ if (!isLocalRef(ref))
1392
+ return undefined;
1393
+ const refPath = ref.substring(2).split('/');
1394
+ return refPath.at(refPath.length - 1);
1395
+ }
1396
+ function resolveLocalRef(ref, spec) {
1397
+ const refPath = ref.substring(2).split('/');
1398
+ return refPath.reduce((integrator, current) => {
1399
+ return integrator ? integrator[current] : undefined;
1400
+ }, spec);
1401
+ }
1402
+ const replaceEscapedChars = [
1403
+ [new RegExp('\\\\ ', 'g'), ' '],
1404
+ [new RegExp('\\\\!', 'g'), '!'],
1405
+ [new RegExp('\\\\"', 'g'), '"'],
1406
+ [new RegExp('\\\\#', 'g'), '#'],
1407
+ [new RegExp('\\\\%', 'g'), '%'],
1408
+ [new RegExp('\\\\&', 'g'), '&'],
1409
+ [new RegExp("\\\\'", 'g'), "'"],
1410
+ [new RegExp('\\\\,', 'g'), ','],
1411
+ [new RegExp('\\\\-', 'g'), '-'],
1412
+ [new RegExp('\\\\:', 'g'), ':'],
1413
+ [new RegExp('\\\\;', 'g'), ';'],
1414
+ [new RegExp('\\\\<', 'g'), '<'],
1415
+ [new RegExp('\\\\=', 'g'), '='],
1416
+ [new RegExp('\\\\>', 'g'), '>'],
1417
+ [new RegExp('\\\\@', 'g'), '@'],
1418
+ [new RegExp('\\\\_', 'g'), '_'],
1419
+ [new RegExp('\\\\`', 'g'), '`'],
1420
+ [new RegExp('\\\\~', 'g'), '~'],
1421
+ [new RegExp('\\\\[zZ]', 'g'), '$'],
1422
+ ];
1423
+ function safeCreateUnicodeRegExp(pattern) {
1424
+ for (const unEscape of replaceEscapedChars) {
1425
+ pattern = pattern.replace(unEscape[0], unEscape[1]);
1426
+ }
1427
+ return pattern;
1428
+ }
766
1429
 
767
1430
  const TypesPushCommand = {
768
1431
  command: "types:push",
@@ -793,9 +1456,18 @@ const TypesPushCommand = {
793
1456
  (!baseTypes || baseTypes.length == 0 || baseTypes.includes(data.definition.baseType)) &&
794
1457
  (!types || types.length == 0 || types.includes(data.definition.key));
795
1458
  });
1459
+ // Create validator
1460
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Loading OpenAPI Specification for validation\n`));
1461
+ const typeSchema = (await loadSchema(client, 'ContentType')).at(0)?.schema;
1462
+ const typeValidator = await getValidator(typeSchema);
796
1463
  // Output selected types
797
- const results = await Promise.all(typeDefinitions.map(type => {
1464
+ const results = await Promise.all(typeDefinitions.map(async (type) => {
1465
+ if (!type.definition.key) {
1466
+ process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Content Type has no key in ${type.file}\n`));
1467
+ return;
1468
+ }
798
1469
  process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Pushing ${type.definition.displayName} from ${type.file}\n`));
1470
+ // Filter unwanted fields from the ContentType definition
799
1471
  const outType = { ...type.definition };
800
1472
  if (outType.source)
801
1473
  delete outType.source;
@@ -805,13 +1477,31 @@ const TypesPushCommand = {
805
1477
  delete outType.lastModified;
806
1478
  if (outType.lastModifiedBy || outType.lastModifiedBy == "")
807
1479
  delete outType.lastModifiedBy;
808
- if (outType.features)
809
- delete outType.features;
810
- if (outType.usage)
811
- delete outType.usage;
812
- return client.contentTypesPut({ path: { key: outType.key }, body: outType, query: { ignoreDataLossWarnings: force } })
813
- .then(ct => { return { key: type.definition.key, type: ct, file: type.file }; })
814
- .catch(e => { return { key: type.definition.key, type: type.definition, file: type.file, error: e }; });
1480
+ //if (outType.features) delete outType.features
1481
+ //if (outType.usage) delete outType.usage
1482
+ const validationResult = typeValidator(outType);
1483
+ if (validationResult || force) {
1484
+ if (!validationResult)
1485
+ process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Ignoring validation errors in ${type.file}\n`));
1486
+ const currentContentType = (await client.contentTypesGet({ path: { key: type.definition.key } }).catch(() => null));
1487
+ function buildError(e) {
1488
+ return { key: type.definition.key, type: type.definition, file: type.file, error: e };
1489
+ }
1490
+ function buildData(ct) {
1491
+ return { key: type.definition.key, type: ct, file: type.file };
1492
+ }
1493
+ return (currentContentType ?
1494
+ client.contentTypesPatch({ path: { key: type.definition.key }, body: outType }) :
1495
+ client.contentTypesCreate({ body: outType })).then(buildData).catch(buildError);
1496
+ }
1497
+ else {
1498
+ return {
1499
+ key: type.definition.key,
1500
+ type: type.definition,
1501
+ file: type.file,
1502
+ error: typeValidator.errors.map(x => x.message).join(', ')
1503
+ };
1504
+ }
815
1505
  }));
816
1506
  const overview = new Table({
817
1507
  head: [
@@ -843,241 +1533,10 @@ const builder = yargs => {
843
1533
  return updatedArgs;
844
1534
  };
845
1535
  function createTypeFolders(contentTypes, basePath, debug = false) {
846
- const folders = contentTypes.map(contentType => {
847
- const baseType = contentType.baseType ?? 'default';
848
- // Create the type folder
849
- const typePath = path.join(basePath, baseType, contentType.key);
850
- if (!fs.existsSync(typePath)) {
851
- fs.mkdirSync(typePath, { recursive: true });
852
- if (debug)
853
- process.stdout.write(chalk.gray(`${figures.arrowRight} Created folder ${typePath} for Content-Type ${contentType.key}\n`));
854
- }
855
- // Check folders
856
- if (!fs.statSync(typePath).isDirectory())
857
- throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
858
- return {
859
- type: contentType.key,
860
- path: typePath,
861
- };
862
- });
863
- return folders;
1536
+ return contentTypes.map(contentType => getContentTypePaths(contentType, basePath, true, debug));
864
1537
  }
865
1538
  function getTypeFolder(list, type) {
866
- return list.filter(x => x.type == type).at(0)?.path;
867
- }
868
-
869
- /**
870
- * Keep track of all generated properties
871
- */
872
- let generatedProps = [];
873
- const NextJsQueriesCommand = {
874
- command: "nextjs:fragments",
875
- describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
876
- builder,
877
- handler: async (args, opts) => {
878
- generatedProps = [];
879
- // Prepare
880
- const { loadedContentTypes, createdTypeFolders } = opts || {};
881
- const { components: basePath, _config: { debug }, force } = parseArgs(args);
882
- const client = createCmsClient(args);
883
- const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
884
- // Start process
885
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments for ${contentTypes.map(x => x.key).join(', ')}\n`));
886
- const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
887
- const updatedTypes = contentTypes.map(contentType => {
888
- const typePath = getTypeFolder(typeFolders, contentType.key);
889
- return createGraphFragments(contentType, typePath, basePath, force, debug, allContentTypes, client.runtimeCmsVersion == OptiCmsVersion.CMS12);
890
- }).filter(x => x).flat();
891
- // Report outcome
892
- if (updatedTypes.length > 0)
893
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
894
- else
895
- process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
896
- if (!opts)
897
- process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
898
- generatedProps = [];
899
- }
900
- };
901
- function createGraphFragments(contentType, typePath, basePath, force, debug, contentTypes, forCms12 = false) {
902
- const returnValue = [];
903
- const baseType = contentType.baseType ?? 'default';
904
- const baseQueryFile = path.join(typePath, `${contentType.key}.${baseType}.graphql`);
905
- if (fs.existsSync(baseQueryFile)) {
906
- if (force) {
907
- if (debug)
908
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
909
- }
910
- else {
911
- if (debug)
912
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
913
- return undefined;
914
- }
915
- }
916
- else if (debug) {
917
- process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
918
- }
919
- const { fragment, propertyTypes } = createInitialFragment(contentType, false, undefined, forCms12);
920
- fs.writeFileSync(baseQueryFile, fragment);
921
- returnValue.push(contentType.key);
922
- let dependencies = Array.isArray(propertyTypes) ? [...propertyTypes] : [];
923
- while (Array.isArray(dependencies) && dependencies.length > 0) {
924
- let newDependencies = [];
925
- dependencies.forEach(dep => {
926
- const propContentType = contentTypes.filter(x => x.key == dep[0])[0];
927
- if (!propContentType) {
928
- console.warn(`🟠 The content type ${dep[0]} has been referenced, but is not found in the Optimizely CMS instance`);
929
- return;
930
- }
931
- const fullTypeName = forCms12 ? contentType.key + propContentType.key : propContentType.key;
932
- const propertyFragmentFile = path.join(basePath, propContentType.baseType, propContentType.key, `${fullTypeName}.property.graphql`);
933
- const propertyFragmentDir = path.dirname(propertyFragmentFile);
934
- if (!fs.existsSync(propertyFragmentDir))
935
- fs.mkdirSync(propertyFragmentDir, { recursive: true });
936
- if (!fs.existsSync(propertyFragmentFile) || force) {
937
- if (debug)
938
- process.stdout.write(chalk.gray(`${figures.arrowRight} Writing ${propContentType.displayName} (${propContentType.key}) property fragment\n`));
939
- const propContentTypeInfo = createInitialFragment(propContentType, true, contentType, forCms12);
940
- fs.writeFileSync(propertyFragmentFile, propContentTypeInfo.fragment);
941
- returnValue.push(propContentType.key);
942
- if (Array.isArray(propContentTypeInfo.propertyTypes))
943
- newDependencies.push(...propContentTypeInfo.propertyTypes);
944
- }
945
- });
946
- dependencies = newDependencies;
947
- }
948
- return returnValue.length > 0 ? returnValue : undefined;
949
- }
950
- function createInitialFragment(contentType, forProperty = false, forBaseType, forCms12 = false) {
951
- const propertyTypes = [];
952
- const fragmentFields = [];
953
- const typeProps = contentType.properties ?? {};
954
- Object.getOwnPropertyNames(typeProps).forEach(propKey => {
955
- // Exclude system properties, which are not present in Optimizely Graph
956
- if (['experience', 'section'].includes(contentType.baseType) && ['AdditionalData', 'UnstructuredData', 'Layout'].includes(propKey))
957
- return;
958
- // Exclude CMS 12 System Properties
959
- if (forCms12 && ['Categories'].includes(propKey))
960
- return;
961
- const propType = typeProps[propKey].type;
962
- const isConflict = generatedProps.some(x => x.propName == propKey && x.propType != propType);
963
- const propName = isConflict ? `${contentType.key}${propKey}: ${propKey}` : propKey;
964
- // Write the property
965
- switch (propType) {
966
- case IntegrationApi.PropertyDataType.ARRAY:
967
- {
968
- const typeData = typeProps[propKey];
969
- switch (typeData.items.type) {
970
- case IntegrationApi.PropertyDataType.INTEGER:
971
- if (typeData.format == 'categorization')
972
- fragmentFields.push(`${propName} { Id, Name, Description }`);
973
- else
974
- fragmentFields.push(propName);
975
- break;
976
- case IntegrationApi.PropertyDataType.STRING:
977
- fragmentFields.push(propName);
978
- break;
979
- case IntegrationApi.PropertyDataType.CONTENT:
980
- if (contentType.baseType == 'page' || contentType.baseType == 'experience')
981
- fragmentFields.push(`${propName} { ...${forCms12 ? 'PageIContentListItem' : 'BlockData'} }`);
982
- else
983
- fragmentFields.push(`${propName} { ...IContentListItem }`);
984
- break;
985
- case IntegrationApi.PropertyDataType.COMPONENT:
986
- const componentType = typeData.items.contentType;
987
- switch (componentType) {
988
- case 'link':
989
- fragmentFields.push(`${propName} { ...LinkItemData }`);
990
- break;
991
- default:
992
- {
993
- const componentFragmentName = forCms12 ? contentType.key + componentType : componentType + 'Property';
994
- fragmentFields.push(`${propName} { ...${componentFragmentName}Data }`);
995
- propertyTypes.push([componentType, true]);
996
- break;
997
- }
998
- }
999
- break;
1000
- case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
1001
- fragmentFields.push(`${propName} { ...ReferenceData }`);
1002
- break;
1003
- default:
1004
- fragmentFields.push(`${propName} { __typename }`);
1005
- break;
1006
- }
1007
- break;
1008
- }
1009
- case IntegrationApi.PropertyDataType.STRING: {
1010
- const propDetails = typeProps[propKey];
1011
- switch (propDetails.format ?? "") {
1012
- case 'html':
1013
- fragmentFields.push(forCms12 ? `${propName} { Structure, Html }` : `${propName} { json, html }`);
1014
- break;
1015
- case 'shortString':
1016
- case 'selectOne':
1017
- case '':
1018
- fragmentFields.push(propName);
1019
- break;
1020
- default:
1021
- if (!isConflict)
1022
- console.warn(chalk.redBright(`❗ Unsupported string format "${propDetails.format}" for ${contentType.key}.${propKey}; add it manually to the fragment if you need to access this field`));
1023
- break;
1024
- }
1025
- break;
1026
- }
1027
- case IntegrationApi.PropertyDataType.URL:
1028
- fragmentFields.push(forCms12 ? propName : `${propName} { ...LinkData }`);
1029
- break;
1030
- case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
1031
- fragmentFields.push(`${propName} { ...ReferenceData }`);
1032
- break;
1033
- case IntegrationApi.PropertyDataType.COMPONENT: {
1034
- const componentType = typeProps[propKey].contentType;
1035
- if (forCms12 && componentType == "link") {
1036
- fragmentFields.push(`${propName} { ...LinkItemData }`);
1037
- }
1038
- else {
1039
- const componentFragmentName = forCms12 ? contentType.key + componentType : componentType + 'Property';
1040
- fragmentFields.push(`${propName} { ...${componentFragmentName}Data }`);
1041
- propertyTypes.push([componentType, true]);
1042
- }
1043
- break;
1044
- }
1045
- case IntegrationApi.PropertyDataType.BINARY:
1046
- fragmentFields.push(`${propName} { ...BinaryData }`);
1047
- break;
1048
- default:
1049
- fragmentFields.push(propName);
1050
- break;
1051
- }
1052
- generatedProps.push({
1053
- propType: typeProps[propKey].type,
1054
- propName: propKey
1055
- });
1056
- });
1057
- if (contentType.baseType == "experience")
1058
- fragmentFields.push('...ExperienceData');
1059
- if (fragmentFields.length == 0) {
1060
- if (forCms12)
1061
- fragmentFields.push('empty: _metadata: ContentLink { key: GuidValue }');
1062
- else
1063
- fragmentFields.push('empty: _metadata { key }');
1064
- }
1065
- const fragmentTarget = forProperty ? (forCms12 ? (forBaseType?.key ?? '') + contentType.key : contentType.key + 'Property') : contentType.key;
1066
- const tpl = `fragment ${fragmentTarget}Data on ${fragmentTarget} {
1067
- ${fragmentFields.join("\n ")}
1068
- }`;
1069
- return {
1070
- fragment: tpl,
1071
- propertyTypes: propertyTypes.length == 0 ? null : propertyTypes
1072
- };
1073
- }
1074
-
1075
- function ucFirst(current) {
1076
- if (typeof current != 'string')
1077
- throw new Error("Only strings can be transformed");
1078
- if (current == "")
1079
- return current;
1080
- return current[0]?.toUpperCase() + current.substring(1);
1539
+ return list.filter(x => x.type == type).at(0);
1081
1540
  }
1082
1541
 
1083
1542
  const NextJsComponentsCommand = {
@@ -1105,8 +1564,8 @@ const NextJsComponentsCommand = {
1105
1564
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1106
1565
  }
1107
1566
  };
1108
- function createComponent(contentType, typePath, force, debug = false) {
1109
- const componentFile = path.join(typePath, 'index.tsx');
1567
+ function createComponent(contentType, typePathInfo, force, debug = false) {
1568
+ const { componentFile, path: typePath } = typePathInfo;
1110
1569
  if (fs.existsSync(componentFile)) {
1111
1570
  if (!force) {
1112
1571
  if (debug)
@@ -1121,8 +1580,9 @@ function createComponent(contentType, typePath, force, debug = false) {
1121
1580
  // Get type information & short-hands
1122
1581
  const displayTemplate = getDisplayTemplateInfo$1(contentType, typePath);
1123
1582
  const baseDisplayTemplate = getBaseTypeTemplateInfo(contentType, typePath);
1124
- const varName = `${contentType.key}${ucFirst(contentType.baseType ?? 'part')}`;
1125
- const tplFn = Templates[contentType.baseType] ?? Templates['default'];
1583
+ const normalizedBaseType = normalizeBaseType(contentType.baseType ?? 'part');
1584
+ const varName = `${contentType.key}${ucFirst(normalizedBaseType)}`;
1585
+ const tplFn = Templates[normalizedBaseType] ?? Templates['default'];
1126
1586
  if (!tplFn) {
1127
1587
  if (debug)
1128
1588
  process.stdout.write(chalk.redBright(`${figures.cross} Skipping ${contentType.displayName} (${contentType.key}) component - no template for ${contentType.baseType} found\n`));
@@ -1146,6 +1606,11 @@ function getBaseTypeTemplateInfo(contentType, typePath) {
1146
1606
  }
1147
1607
  return undefined;
1148
1608
  }
1609
+ function normalizeBaseType(baseType) {
1610
+ if (baseType.startsWith('_'))
1611
+ return baseType.substring(1);
1612
+ return baseType;
1613
+ }
1149
1614
  function getDisplayTemplateInfo$1(contentType, typePath) {
1150
1615
  const displayTemplatesFile = path.join(typePath, 'displayTemplates.ts');
1151
1616
  if (fs.existsSync(displayTemplatesFile)) {
@@ -1159,51 +1624,90 @@ function getDisplayTemplateInfo$1(contentType, typePath) {
1159
1624
  }
1160
1625
  const Templates = {
1161
1626
  // Default Template for all components without specifics
1162
- default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { type CmsComponent } from "@remkoj/optimizely-cms-react";
1627
+ default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
1163
1628
  import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
1164
1629
  import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
1165
1630
  import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
1166
1631
 
1167
1632
  /**
1168
1633
  * ${contentType.displayName}
1634
+ * ---
1169
1635
  * ${contentType.description}
1170
1636
  */
1171
- export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, children }) => {
1637
+ export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps }) => {
1172
1638
  const componentName = '${contentType.displayName}'
1173
1639
  const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1174
- return <div className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4">
1640
+ return <CmsEditable className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4" {...editProps}>
1175
1641
  <div className="font-bold italic">{ componentName }</div>
1176
1642
  <div>{ componentInfo }</div>
1177
1643
  { 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> }
1178
- { children && <div className="mt-4 mx-4 flex flex-col">{ children }</div>}
1179
- </div>
1644
+ </CmsEditable>
1180
1645
  }
1181
1646
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1182
1647
  ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1183
1648
 
1649
+ export default ${varName}`,
1650
+ // Default Template for all section types
1651
+ section: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { OptimizelyComposition, isNode, CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
1652
+ import { get${contentType.key}DataDocument, type get${contentType.key}DataQuery, SectionCompositionDataFragmentDoc } from "@/gql/graphql";
1653
+ import { getFragmentData } from "@/gql/fragment-masking";${displayTemplate ? `
1654
+ import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
1655
+ import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
1656
+
1657
+ /**
1658
+ * ${contentType.displayName}
1659
+ * ---
1660
+ * ${contentType.description}
1661
+ */
1662
+ export const ${varName} : CmsComponent<get${contentType.key}DataQuery${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps, children, ctx }) => {
1663
+ // If we're rendering stand-alone we'll get a composition back, with ourself included If we're
1664
+ // rendering as part of an experience, we'll get the data directly. So handle both cases.
1665
+ if (!data?._metadata && children) return children;
1666
+
1667
+ const componentName = '${contentType.displayName}'
1668
+ const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1669
+ const composition = getFragmentData(SectionCompositionDataFragmentDoc, data)?.composition;
1670
+ return <CmsEditable className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4" {...editProps}>
1671
+ <div className="font-bold italic">{ componentName }</div>
1672
+ <div>{ componentInfo }</div>
1673
+ { 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> }
1674
+ ${(displayTemplate || baseDisplayTemplate) ? '<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(layoutProps, undefined, 4) }</pre>' : '{/* This component doesn\'t have layout options */}'}
1675
+ {children && <div>{ children }</div>}
1676
+ {composition && isNode(composition) && <OptimizelyComposition node={composition} ctx={ctx} />}
1677
+ </CmsEditable>
1678
+ }
1679
+ ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1680
+ ${varName}.getDataQuery = () => get${contentType.key}DataDocument
1681
+
1184
1682
  export default ${varName}`,
1185
1683
  // Template for all page component types
1186
1684
  page: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
1187
- import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
1685
+ import { get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'${displayTemplate ? `
1188
1686
  import { ${displayTemplate} } from "./displayTemplates";` : ''}
1189
1687
  import { getSdk } from "@/gql"
1190
1688
 
1191
1689
  /**
1192
1690
  * ${contentType.displayName}
1691
+ * ---
1193
1692
  * ${contentType.description}
1693
+ *
1694
+ * This component uses the content query that is auto-generated with the Optimizely CMS Preset for GraphQL Codegen, if you need
1695
+ * to override this query create a GraphQL file (for example: \`get${contentType.key}Data.query.graphql\`) in the same folder as
1696
+ * this file. This file must include a GraphQL query with the name \`get${contentType.key}Data\`.
1697
+ *
1698
+ * [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
1194
1699
  */
1195
- export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, children }) => {
1700
+ export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''} }) => {
1196
1701
  const componentName = '${contentType.displayName}'
1197
1702
  const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1198
1703
  return <div className="mx-auto px-2 container">
1199
1704
  <div className="font-bold italic">{ componentName }</div>
1200
1705
  <div>{ componentInfo }</div>
1201
1706
  { 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> }
1202
- { children && <div className="flex flex-col mt-4 mx-4">{ children }</div>}
1203
1707
  </div>
1204
1708
  }
1205
1709
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1206
- ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1710
+ ${varName}.getDataQuery = () => get${contentType.key}DataDocument
1207
1711
  ${varName}.getMetaData = async (contentLink, locale, client) => {
1208
1712
  const sdk = getSdk(client);
1209
1713
  // Add your metadata logic here
@@ -1213,23 +1717,36 @@ ${varName}.getMetaData = async (contentLink, locale, client) => {
1213
1717
  export default ${varName}`,
1214
1718
  // Template for all experience component types
1215
1719
  experience: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
1216
- import { ${contentType.key}DataFragmentDoc, type ExperienceDataFragment, type ${contentType.key}DataFragment } from "@/gql/graphql";
1720
+ import { ExperienceDataFragmentDoc, get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'
1721
+ import { getFragmentData } from '@/gql/fragment-masking'
1217
1722
  import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
1218
1723
  import { ${displayTemplate} } from "./displayTemplates";` : ''}
1219
1724
  import { getSdk } from "@/gql/client"
1220
1725
 
1221
1726
  /**
1222
1727
  * ${contentType.displayName}
1728
+ * ---
1223
1729
  * ${contentType.description}
1730
+ *
1731
+ * This component uses the content query that is auto-generated with the Optimizely CMS Preset for GraphQL Codegen, if you need
1732
+ * to override this query create the file \`${contentType.key}.query.graphql\` in the same folder as this file. This file then
1733
+ * must include a GraphQL query with the name \`get${contentType.key} Data\`.
1734
+ *
1735
+ * [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
1224
1736
  */
1225
- export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, ctx }) => {
1226
- const composition = (data as ExperienceDataFragment)?.composition
1227
- return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData" ctx={ctx}>
1228
- { composition && isNode(composition) && <OptimizelyComposition node={composition} ctx={ctx} /> }
1229
- </CmsEditable>
1737
+ export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, ctx }) => {
1738
+ if (ctx) ctx.editableContentIsExperience = true
1739
+ const composition = getFragmentData(ExperienceDataFragmentDoc, data).composition
1740
+ const componentName = '${contentType.displayName}'
1741
+ const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
1742
+ return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData" ctx={ctx}>
1743
+ <div className="font-bold italic">{ componentName }</div>
1744
+ <div>{ componentInfo }</div>
1745
+ { composition && isNode(composition) && <OptimizelyComposition node={composition} ctx={ctx} /> }
1746
+ </CmsEditable>
1230
1747
  }
1231
1748
  ${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
1232
- ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
1749
+ ${varName}.getDataQuery = () => get${contentType.key}DataDocument
1233
1750
  ${varName}.getMetaData = async (contentLink, locale, client) => {
1234
1751
  const sdk = getSdk(client);
1235
1752
  // Add your metadata logic here
@@ -1247,18 +1764,19 @@ const NextJsVisualBuilderCommand = {
1247
1764
  // Prepare
1248
1765
  const { components: basePath, _config: { debug }, force } = parseArgs(args);
1249
1766
  // Create the fall-back node and style defintions
1250
- await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node'], definitions: true });
1767
+ await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'], definitions: true });
1251
1768
  createGenericNode(basePath, force, debug);
1252
1769
  // Get all styles
1253
1770
  const { styles } = await getStyles(createCmsClient(args), { ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'] });
1254
1771
  // Process node styles
1255
1772
  styles.filter(x => typeof (x.nodeType) == 'string' && x.nodeType.length > 0).map(styleDefintion => {
1256
- const templatePath = path.join(basePath, 'nodes', styleDefintion.nodeType, styleDefintion.key);
1773
+ const templatePath = path.join(basePath, 'nodes', keyToSlug(styleDefintion.nodeType), keyToSlug(styleDefintion.key));
1257
1774
  createSpecificNode(styleDefintion, templatePath, force, debug);
1258
1775
  });
1259
1776
  // Process base styles
1260
1777
  styles.filter(x => typeof (x.baseType) == 'string' && x.baseType.length > 0).map(styleDefinition => {
1261
- const templatePath = path.join(basePath, styleDefinition.baseType, 'styles', styleDefinition.key);
1778
+ const baseType = (styleDefinition.baseType ?? '').startsWith('_') ? (styleDefinition.baseType ?? '').substring(1) : styleDefinition.baseType;
1779
+ const templatePath = path.join(basePath, baseType, 'styles', keyToSlug(styleDefinition.key));
1262
1780
  createSpecificNode(styleDefinition, templatePath, force, debug);
1263
1781
  });
1264
1782
  }
@@ -1268,23 +1786,40 @@ function createSpecificNode(template, templatePath, force = false, debug = false
1268
1786
  if (fs.existsSync(nodeFile)) {
1269
1787
  if (!force) {
1270
1788
  if (debug)
1271
- process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${template.displayName} (${template.key}) component - file already exists\n`));
1789
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${template.displayName} (${template.key}) node - file already exists\n`));
1272
1790
  return undefined;
1273
1791
  }
1274
1792
  if (debug)
1275
- process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${template.displayName} (${template.key}) component\n`));
1793
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${template.displayName} (${template.key}) node\n`));
1276
1794
  }
1277
1795
  else if (debug)
1278
- process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${template.displayName} (${template.key}) component\n`));
1796
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${template.displayName} (${template.key}) node\n`));
1279
1797
  if (!fs.existsSync(templatePath))
1280
1798
  fs.mkdirSync(templatePath, { recursive: true });
1799
+ const baseType = template.nodeType ?? template.baseType ?? 'unknown';
1281
1800
  const displayTemplateName = getDisplayTemplateInfo(template, templatePath);
1282
- const component = `import { extractSettings, type CmsLayoutComponent } from "@remkoj/optimizely-cms-react/rsc";${displayTemplateName ? `
1283
- import { ${displayTemplateName} } from "../displayTemplates";` : ''}
1801
+ const imports = ['import { extractSettings, type CmsLayoutComponent } from "@remkoj/optimizely-cms-react/rsc";'];
1802
+ const componentProps = [`className="vb:${baseType} vb:${baseType}:${template.key}"`];
1803
+ const componentArgs = ['layoutProps', 'children'];
1804
+ let componentType = 'div';
1805
+ if (displayTemplateName)
1806
+ imports.push(`import { ${displayTemplateName} } from "../displayTemplates";`);
1807
+ if (baseType.toLowerCase() == 'section') {
1808
+ imports.push('import { CmsEditable } from "@remkoj/optimizely-cms-react/rsc";');
1809
+ componentType = 'CmsEditable';
1810
+ componentProps.push('{ ...editProps }');
1811
+ componentArgs.push('editProps');
1812
+ }
1813
+ let style = '';
1814
+ if (baseType == "row")
1815
+ style = ` style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "stretch" }}`;
1816
+ if (baseType == "column")
1817
+ style = ` style={{ display: "flex", flexDirection: "column", flexGrow: 1 }}`;
1818
+ const component = `${imports.join("\n")}
1284
1819
 
1285
- export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ contentLink, layoutProps, children }) => {
1820
+ export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ ${componentArgs.join(', ')} }) => {
1286
1821
  const layout = extractSettings(layoutProps);
1287
- return (<div className="vb:${template.nodeType} vb:${template.nodeType}:${template.key}">{ children }</div>);
1822
+ return (<${componentType} ${componentProps.join(' ')}${style}>{ children }</${componentType}>);
1288
1823
  }
1289
1824
 
1290
1825
  export default ${template.key};`;
@@ -1304,13 +1839,19 @@ function createGenericNode(basePath, force, debug) {
1304
1839
  else if (debug)
1305
1840
  process.stdout.write(chalk.gray(`${figures.arrowRight} Creating generic node component\n`));
1306
1841
  const nodeContent = `import { CmsEditable, type CmsLayoutComponent } from '@remkoj/optimizely-cms-react/rsc'
1842
+ import { type CSSProperties } from 'react';
1307
1843
 
1308
- export const VisualBuilderNode : CmsLayoutComponent = ({ contentLink, layoutProps, children, ctx }) =>
1844
+ export const VisualBuilderNode : CmsLayoutComponent = ({ editProps, layoutProps, children }) =>
1309
1845
  {
1310
- let className = \`vb:\${layoutProps?.layoutType}\`
1311
- if (layoutProps && layoutProps.layoutType == "section")
1312
- return <CmsEditable as="div" className={ className } cmsId={ contentLink.key } ctx={ctx}>{ children }</CmsEditable>
1313
- return <div className={ className }>{ children }</div>
1846
+ let className = \`vb:\${layoutProps?.layoutType}\`;
1847
+ let style : CSSProperties | undefined = undefined;
1848
+ if (layoutProps?.layoutType == "row")
1849
+ style = {display: "flex", flexDirection: "row"}
1850
+ if (layoutProps?.layoutType == "column")
1851
+ style = {display: "flex", flexDirection: "column"}
1852
+ if (layoutProps && layoutProps.layoutType == "section")
1853
+ return <CmsEditable as="div" className={ className } {...editProps}>{ children }</CmsEditable>
1854
+ return <div className={ className } style={ style }>{ children }</div>
1314
1855
  }
1315
1856
 
1316
1857
  export default VisualBuilderNode`;
@@ -1330,6 +1871,7 @@ function getDisplayTemplateInfo(template, typePath) {
1330
1871
 
1331
1872
  const ROOT_FACTORY_KEY = ".";
1332
1873
  const FACTORY_FILE_NAME = "index.ts";
1874
+ const reservedNames = ["loading.tsx", "loading.jsx", "suspense.tsx", "suspense.jsx"];
1333
1875
  const NextJsFactoryCommand = {
1334
1876
  command: "nextjs:factory",
1335
1877
  describe: "Create the ComponentFactory for a Next.JS / Optimizely Graph structure",
@@ -1338,108 +1880,102 @@ const NextJsFactoryCommand = {
1338
1880
  const { components: basePath, force, _config: { debug } } = parseArgs(args);
1339
1881
  if (debug)
1340
1882
  process.stdout.write(chalk.gray(`${figures.arrowRight} Start generating component factories\n`));
1883
+ // Get & filter all component files
1884
+ const clientComponents = [];
1341
1885
  const components = globSync(["./**/*.jsx", "./**/*.tsx"], {
1342
1886
  cwd: basePath
1343
1887
  }).map(p => p.split(path.sep)).filter(p => {
1888
+ // Get the filename
1889
+ const fileName = p.at(p.length - 1);
1890
+ if (!fileName)
1891
+ return false;
1344
1892
  // Consider components in a file starting with "_" as a partial
1345
- if (p.at(p.length - 1)?.startsWith('_') == true)
1893
+ if (fileName.startsWith('_') == true)
1346
1894
  return false;
1347
1895
  // Consider components in a folder named "partials" as a partial
1348
- if (p.at(p.length - 2)?.toLowerCase() == 'partials')
1896
+ if (p.some(folder => folder === 'partials'))
1349
1897
  return false;
1350
1898
  // Skip the special loader component
1351
- if (p.at(p.length - 1) == "loading.tsx" || p.at(p.length - 1) == "loading.jsx")
1899
+ if (reservedNames.includes(fileName))
1352
1900
  return false;
1353
1901
  // Check if the file has a default export
1354
1902
  const fileBuffer = fs.readFileSync(path.join(basePath, p.join(path.sep)));
1355
1903
  const hasDefaultExport = fileBuffer.includes('export default');
1904
+ if (fileBuffer.includes('use client'))
1905
+ clientComponents.push(p.join(path.sep));
1356
1906
  if (!hasDefaultExport) {
1357
1907
  process.stdout.write(chalk.redBright(`${figures.warning} No default export in ${p.join(path.sep)} - ignoring file\n`));
1358
1908
  return false;
1359
1909
  }
1360
1910
  return true;
1361
1911
  });
1912
+ // Report what we have found
1362
1913
  if (debug)
1363
1914
  process.stdout.write(chalk.gray(`${figures.arrowRight} Identified ${components.length} components in ${basePath}\n`));
1915
+ // Build factory / component structure
1364
1916
  const componentFactoryDefintions = new Map();
1365
1917
  components.forEach(component => {
1918
+ const componentDir = path.dirname(path.join(...component));
1919
+ path.basename(path.join(...component));
1920
+ // Determine component target
1921
+ const componentKey = getComponentKey(component, basePath);
1922
+ let componentVariant = (component.length > 1 ? component.at(component.length - 1) ?? 'default' : 'default').replace('index', 'default');
1923
+ componentVariant = path.basename(componentVariant, path.extname(componentVariant));
1924
+ // Get factory information
1366
1925
  const factorySegments = component.length > 2 ? component.slice(0, -2) : [ROOT_FACTORY_KEY];
1367
- const factoryKey = factorySegments.join(path.sep);
1926
+ const factoryKey = path.posix.join(...factorySegments);
1368
1927
  const factoryFile = path.join(factoryKey, FACTORY_FILE_NAME);
1369
- // Add component to factory
1928
+ // Check dynamic & suspense
1929
+ const isClientComponent = clientComponents.includes(component.join(path.sep));
1930
+ const useDynamic = [
1931
+ path.join(basePath, componentDir, 'loading.tsx'),
1932
+ path.join(basePath, componentDir, 'loading.jsx')
1933
+ ].some(fs.existsSync);
1934
+ const useSuspense = [
1935
+ path.join(basePath, componentDir, 'suspense.tsx'),
1936
+ path.join(basePath, componentDir, 'suspense.jsx')
1937
+ ].some(fs.existsSync);
1938
+ // Prepare data
1939
+ const componentImport = component.length == 1 ?
1940
+ `.${path.posix.sep}${path.basename(component[0], path.extname(component[0]))}` :
1941
+ `.${path.posix.sep}${path.posix.relative(factoryKey, componentDir)}${componentVariant !== 'default' ? path.posix.sep + componentVariant : ''}`;
1942
+ const loaderImport = useDynamic ? componentImport + path.posix.sep + 'loading' : undefined;
1943
+ const suspenseImport = useSuspense ? componentImport + path.posix.sep + 'suspense' : undefined;
1944
+ const componentVariablesBase = componentKey + (componentVariant !== 'default' ? processName(componentVariant) : '');
1945
+ // Add to the factory
1370
1946
  const factory = componentFactoryDefintions.get(factoryKey) || { file: factoryFile, entries: [], subfactories: [] };
1371
- 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'));
1372
- if (useSuspense && debug)
1373
- process.stdout.write(chalk.gray(`${figures.arrowRight} Components in ${component.slice(0, -1).join(path.sep)} will use suspense\n`));
1374
- const componentSegments = component.slice(-2).map(p => {
1375
- const entry = p.substring(0, p.length - path.extname(p).length);
1376
- return entry.toLowerCase() == "index" ? null : entry;
1377
- }).filter(x => x);
1378
- const componentImport = "./" + componentSegments.join('/');
1379
- const loaderImport = useSuspense ? "./" + component.slice(-2).map((p, i, a) => {
1380
- const entry = p.substring(0, p.length - path.extname(p).length);
1381
- if (i == a.length - 1)
1382
- return 'loading';
1383
- return entry.toLowerCase() == "index" ? null : entry;
1384
- }).join('/') : undefined;
1385
1947
  factory.entries.push({
1948
+ key: componentKey,
1949
+ variant: componentVariant,
1386
1950
  import: componentImport,
1387
- variable: [...componentSegments.map(processName), 'Component'].join(''),
1388
- key: componentSegments.join('/') == "node" ? "Node" : componentSegments.join('/'),
1951
+ variable: componentVariablesBase + 'Component',
1389
1952
  loaderImport,
1390
- loaderVariable: useSuspense ? [...componentSegments.map(processName), 'Loader'].join('') : undefined,
1953
+ loaderVariable: useDynamic ? componentVariablesBase + 'Loader' : undefined,
1954
+ suspenseImport,
1955
+ suspenseVariable: useSuspense ? componentVariablesBase + 'Placeholder' : undefined,
1956
+ isClient: isClientComponent
1391
1957
  });
1392
1958
  componentFactoryDefintions.set(factoryKey, factory);
1393
- // Register with all appropriate parents
1959
+ // Add/update parent factories
1394
1960
  const parentSegements = factoryKey == ROOT_FACTORY_KEY ? factorySegments.slice(0, -1) : [ROOT_FACTORY_KEY, ...factorySegments.slice(0, -1)];
1395
- let currentFactory = {
1396
- key: factoryKey,
1397
- import: './' + factorySegments.slice(-1).join('/'),
1398
- prefix: processName(factorySegments.slice(-1).at(0)),
1399
- variable: factorySegments.map(processName).join("") + "Factory"
1400
- };
1401
- for (let i = parentSegements.length; i > 0; i--) {
1402
- const parentFactoryKey = parentSegements.slice(0, i).filter(x => x != ROOT_FACTORY_KEY).join(path.sep) || ROOT_FACTORY_KEY;
1961
+ parentSegements.forEach((_, idx, data) => {
1962
+ const parentFactoryKey = path.posix.join(...data.slice(0, idx + 1)) || ROOT_FACTORY_KEY;
1963
+ const childFactoryKey = factorySegments.slice(idx, idx + 1).at(0);
1403
1964
  const parentFactory = componentFactoryDefintions.get(parentFactoryKey) ?? { file: path.join(parentFactoryKey, FACTORY_FILE_NAME), entries: [], subfactories: [] };
1404
- if (!parentFactory.subfactories.some(x => x.key == currentFactory.key))
1405
- parentFactory.subfactories.push(currentFactory);
1406
- componentFactoryDefintions.set(parentFactoryKey, parentFactory);
1407
- if (i > 1) {
1408
- currentFactory = {
1409
- key: parentFactoryKey,
1410
- import: './' + parentFactoryKey.split(path.sep).slice(-1).at(0),
1411
- prefix: processName(parentFactoryKey.split(path.sep).slice(-1).at(0)),
1412
- variable: parentFactoryKey.split(path.sep).map(processName).join("") + "Factory"
1413
- };
1965
+ if (!parentFactory.subfactories.some(x => x.key === childFactoryKey)) {
1966
+ parentFactory.subfactories.push({
1967
+ key: childFactoryKey,
1968
+ import: './' + childFactoryKey,
1969
+ variable: processName(childFactoryKey) + 'Factory'
1970
+ });
1971
+ componentFactoryDefintions.set(parentFactoryKey, parentFactory);
1414
1972
  }
1415
- }
1416
- });
1417
- const mainFactory = componentFactoryDefintions.get(ROOT_FACTORY_KEY);
1418
- if (mainFactory) {
1419
- if (debug)
1420
- process.stdout.write(chalk.gray(`${figures.arrowRight} Updating prefixes within RootFactory\n`));
1421
- mainFactory.subfactories = mainFactory.subfactories.map(subFactory => {
1422
- if (typeof (subFactory.prefix == 'string'))
1423
- switch (subFactory.prefix) {
1424
- case "Video":
1425
- case "Image":
1426
- subFactory.prefix = ["Media", subFactory.prefix, "Component"];
1427
- break;
1428
- case "Experience":
1429
- subFactory.prefix = [subFactory.prefix, "Page"];
1430
- break;
1431
- case "Element":
1432
- subFactory.prefix = ["Component"];
1433
- break;
1434
- case "Media":
1435
- subFactory.prefix = [subFactory.prefix, "Component"];
1436
- break;
1437
- }
1438
- return subFactory;
1439
1973
  });
1440
- }
1974
+ });
1975
+ // Report factory file count
1441
1976
  if (debug)
1442
1977
  process.stdout.write(chalk.gray(`${figures.arrowRight} Finished preparing ${componentFactoryDefintions.size} factories, start writing\n`));
1978
+ // Iterate over the factories and create them
1443
1979
  let updateCounter = 0;
1444
1980
  for (const key of componentFactoryDefintions.keys()) {
1445
1981
  const factory = componentFactoryDefintions.get(key);
@@ -1463,6 +1999,36 @@ const NextJsFactoryCommand = {
1463
1999
  process.stdout.write("\n");
1464
2000
  }
1465
2001
  };
2002
+ /**
2003
+ * Get the component key that must be used for the variable in the factory
2004
+ * this looks first at the component-type file.
2005
+ *
2006
+ * @param component The file path broken into parts
2007
+ * @returns The key to use
2008
+ */
2009
+ function getComponentKey(component, componentsRootDir) {
2010
+ // Prepare context
2011
+ //const componentFile = path.basename(path.join(...component));
2012
+ const componentDir = path.dirname(path.join(...component));
2013
+ // Calculate the fallback value
2014
+ const baseName = processName(component.length == 1 ? ucFirst(path.basename(component[0], path.extname(component[0]))) : component.at(component.length - 2));
2015
+ // Try to read the key from the opti-type.json file in the same folder
2016
+ const definitions = globSync("*.opti-type.json", { cwd: path.join(componentsRootDir, componentDir) });
2017
+ //console.log(definitions, componentDir);
2018
+ if (definitions.length == 1) {
2019
+ try {
2020
+ const data = JSON.parse(fs.readFileSync(path.join(componentsRootDir, componentDir, definitions[0])).toString('utf8'));
2021
+ //console.log('Updating to', data.key)
2022
+ if (data.key)
2023
+ return data.key;
2024
+ }
2025
+ catch {
2026
+ // Ignored on purpose
2027
+ }
2028
+ }
2029
+ // Return the fallback value
2030
+ return baseName;
2031
+ }
1466
2032
  function shouldWriteFactory(factoryFile, force = false, debug = false) {
1467
2033
  if (!fs.existsSync(factoryFile)) {
1468
2034
  process.stdout.write(chalk.green(`${figures.tick} Creating new factory file: ${factoryFile}\n`));
@@ -1488,59 +2054,64 @@ function processName(input) {
1488
2054
  return nameSegements.map(ucFirst).join('');
1489
2055
  }
1490
2056
  function generateFactory(factoryInfo, factoryKey) {
1491
- const needsPrefixFunction = factoryInfo.subfactories.length > 0;
2057
+ // Get the factory name
1492
2058
  const factoryName = factoryKey.split(path.sep).map(processName).join("") + "Factory";
1493
- const components = factoryInfo.entries;
1494
- const subFactories = factoryInfo.subfactories;
1495
- const factoryContent = `// Auto generated dictionary
2059
+ // Get the components and sub-factories, sorted by key to minimize changes between runs
2060
+ const components = [...factoryInfo.entries].sort((a, b) => { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
2061
+ const subFactories = [...factoryInfo.subfactories].sort((a, b) => { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
2062
+ // Check if there's at least one component that uses next/dynamic
2063
+ const hasDynamic = factoryInfo.entries.some(x => x.loaderImport);
2064
+ const hasClientComponents = factoryInfo.entries.some(x => x.isClient);
2065
+ // The intro for the factory
2066
+ const factoryIntro = `// Auto generated dictionary
1496
2067
  // @not-modified => When this line is removed, the "force" parameter of the CLI tool is required to overwrite this file
1497
- import { type ComponentTypeDictionary } from "@remkoj/optimizely-cms-react";
1498
- ${[...components, ...subFactories].map(x => {
1499
- let importLine = `import ${x.variable} from "${x.import}";`;
1500
- if (x.loaderImport && x.loaderVariable) {
1501
- importLine += `\nimport ${x.loaderVariable} from "${x.loaderImport}";`;
1502
- }
1503
- return importLine;
1504
- }).join("\n")}
1505
-
1506
- ${needsPrefixFunction ? `// Prefix entries - if needed
1507
- ${subFactories.map(subFactory => Array.isArray(subFactory.prefix) ?
1508
- subFactory.prefix.map(z => `prefixDictionaryEntries(${subFactory.variable}, "${z}");`).join("\n") :
1509
- `prefixDictionaryEntries(${subFactory.variable}, "${subFactory.prefix}");`).join("\n")}
1510
-
1511
- ` : ''}// Build dictionary
1512
- export const ${factoryName} : ComponentTypeDictionary = [
1513
- ${[...components.map(x => {
1514
- if (x.loaderVariable) {
1515
- return `{
1516
- type: "${x.key}",
1517
- component: ${x.variable},
1518
- useSuspense: true,
1519
- loader: ${x.loaderVariable}
1520
- }`;
1521
- }
1522
- else {
1523
- return `{
1524
- type: "${x.key}",
1525
- component: ${x.variable}
1526
- }`;
1527
- }
1528
- }), ...subFactories.map(x => `...${x.variable}`)].join(",\n ")}
1529
- ];
1530
-
1531
- // Export dictionary
1532
- export default ${factoryName};
1533
- ${needsPrefixFunction ? `
1534
- // Helper functions
1535
- function prefixDictionaryEntries(list: ComponentTypeDictionary, prefix: string) : ComponentTypeDictionary
1536
- {
1537
- list.forEach((component, idx, dictionary) => {
1538
- dictionary[idx].type = typeof component.type == 'string' ? prefix + "/" + component.type : [ prefix, ...component.type ]
1539
- });
1540
- return list;
1541
- }
1542
- ` : ''}`;
1543
- return factoryContent;
2068
+ import { type ComponentTypeDictionary } from '@remkoj/optimizely-cms-react';${hasDynamic || hasClientComponents ? `
2069
+ import dynamic from 'next/dynamic';` : ''}`;
2070
+ // The outry for the factory
2071
+ const factoryOutro = `// Export dictionary
2072
+ export default ${factoryName};`;
2073
+ // The imports of the factory
2074
+ const factoryImports = [...components, ...subFactories].map(x => {
2075
+ const imports = [];
2076
+ if (x.loaderImport)
2077
+ imports.push(`import ${x.loaderVariable} from '${x.loaderImport}';`);
2078
+ else if (!x.isClient)
2079
+ imports.push(`import ${x.variable} from '${x.import}';`);
2080
+ if (x.suspenseImport)
2081
+ imports.push(`import ${x.suspenseVariable} from '${x.suspenseImport}';`);
2082
+ return imports.length > 0 ? imports.join('\n') : undefined;
2083
+ }).filter(x => x).join('\n');
2084
+ // The dynamic imports of the factory
2085
+ const dynamicImports = hasDynamic ? `// Lazy load components that have a loading file, this only affects client components
2086
+ // See https://nextjs.org/docs/app/guides/lazy-loading#importing-server-components
2087
+ // for more details on how this affects server components in Next.js
2088
+ ` + components.filter(x => x.loaderImport).map(x => {
2089
+ return `const ${x.variable} = dynamic(() => import('${x.import}'), {
2090
+ ssr: true,
2091
+ loading: ${x.loaderVariable}
2092
+ });`;
2093
+ }).join('\n') : undefined;
2094
+ // The client imports of the factory
2095
+ const clientComponents = hasClientComponents ? `// Lazy load client components even without loader specified
2096
+ ` + components.filter(x => x.isClient && !x.loaderImport).map(x => {
2097
+ return `const ${x.variable} = dynamic(() => import('${x.import}'), { ssr: true });`;
2098
+ }).join('\n') : undefined;
2099
+ // The actual entries for the factory
2100
+ const factoryEntries = [...components.map(x => {
2101
+ return ` {
2102
+ type: '${x.key}',${x.variant && x.variant !== 'default' ? `
2103
+ variant: '${x.variant}',` : ''}
2104
+ component: ${x.variable}${x.suspenseVariable ? `,
2105
+ useSuspense: true,
2106
+ loader: ${x.suspenseVariable}` : ''}${x.isClient ? `,
2107
+ isClient: true` : ''}
2108
+ }`;
2109
+ }), ...subFactories.map(x => ` ...${x.variable}`)];
2110
+ // The body of the factory
2111
+ const factoryBody = `// Build dictionary
2112
+ export const ${factoryName} : ComponentTypeDictionary = [${factoryEntries.length > 0 ? '\n' + factoryEntries.join(',\n') + '\n' : ''}];`;
2113
+ // Combine everything into one string
2114
+ return [factoryIntro, factoryImports, dynamicImports, clientComponents, factoryBody, factoryOutro].filter(x => (x?.length || 0) > 0).join('\n\n') + '\n';
1544
2115
  }
1545
2116
 
1546
2117
  const NextJsCreateCommand = {
@@ -1558,7 +2129,7 @@ const NextJsCreateCommand = {
1558
2129
  await Promise.all([
1559
2130
  TypesPullCommand.handler(args),
1560
2131
  StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: [], definitions: true }),
1561
- NextJsQueriesCommand.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
2132
+ //createFragments.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
1562
2133
  ]);
1563
2134
  process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared types, styles and fragments") + "\n");
1564
2135
  // Then create the components
@@ -1572,183 +2143,214 @@ const NextJsCreateCommand = {
1572
2143
  process.stdout.write(chalk.green(chalk.bold(figures.tick + " Scaffolded a complete Next.JS / Optimizely Graph structure")) + "\n");
1573
2144
  }
1574
2145
  };
1575
-
1576
- const deepmerge$1 = createDeepMerge();
1577
- async function loadSchema(client, schemaName) {
1578
- const schemas = Array.isArray(schemaName) ? schemaName : [schemaName];
1579
- process.stdout.write(`${figures.arrowRight} Downloading current JSON Schema\n`);
1580
- const spec = await client.getOpenApiSpec();
1581
- const specSchemas = spec.components?.schemas ?? {};
1582
- process.stdout.write(` ${figures.tick} Complete\n`);
1583
- const result = [];
1584
- process.stdout.write(`\n${figures.arrowRight} Constructing schema for ${schemas.join(', ')}\n`);
1585
- for await (const schema of schemas)
1586
- if (specSchemas[schema]) {
1587
- const definitions = {};
1588
- const processedSchema = processSchema(specSchemas[schema], definitions, spec);
1589
- const jsonSchema = {
1590
- //"$schema": "https://json-schema.org/draft-07/schema",
1591
- "$id": `https://api.cms.optimizely.com/schema/${schema}`,
1592
- type: "object",
1593
- title: schema,
1594
- ...processedSchema,
1595
- definitions
1596
- };
1597
- result.push({
1598
- title: schema,
1599
- schema: postProcessDefintions(jsonSchema)
1600
- });
1601
- process.stdout.write(` ${figures.tick} Constructed schema of ${schema}\n`);
2146
+
2147
+ const NextJsFragmentsCommand = {
2148
+ command: "nextjs:fragments",
2149
+ describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
2150
+ builder,
2151
+ handler: async (args, opts) => {
2152
+ // Prepare
2153
+ const { loadedContentTypes, createdTypeFolders } = opts || {};
2154
+ const { components: basePath, _config: { debug }, force, path: appPath } = parseArgs(args);
2155
+ const client = createCmsClient(args);
2156
+ // Get content types
2157
+ const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
2158
+ const allContentTypesMap = new Map();
2159
+ allContentTypes.forEach(x => {
2160
+ if (x.key)
2161
+ allContentTypesMap.set(x.key, x);
2162
+ });
2163
+ const generator = new DocumentGenerator(allContentTypesMap);
2164
+ // Start process
2165
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments\n`));
2166
+ const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
2167
+ const tracker = new PropertyCollisionTracker(appPath);
2168
+ const dependencies = [];
2169
+ const updatedTypes = contentTypes.map(contentType => {
2170
+ const { written, propertyTypes } = createComponentFragments(contentType, getTypeFolder(typeFolders, contentType.key), force, debug, tracker, generator);
2171
+ dependencies.push(...propertyTypes);
2172
+ return written ? contentType.key : undefined;
2173
+ }).filter(x => x);
2174
+ // Report outcome
2175
+ if (updatedTypes.length > 0)
2176
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
2177
+ else
2178
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
2179
+ // Start property generation process
2180
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL property fragments\n`));
2181
+ const generatedProps = createPropertyFragments(dependencies, allContentTypes, (contentType) => {
2182
+ if (!contentType.key)
2183
+ return undefined;
2184
+ let tf = getTypeFolder(typeFolders, contentType.key);
2185
+ if (!tf) {
2186
+ tf = getContentTypePaths(contentType, basePath, true, debug);
2187
+ typeFolders.push(tf);
2188
+ }
2189
+ return tf;
2190
+ }, force, debug, tracker, generator);
2191
+ // Report outcome
2192
+ if (generatedProps.length > 0)
2193
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
2194
+ else
2195
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL property fragments created/updated\n`));
2196
+ if (!opts)
2197
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
2198
+ }
2199
+ };
2200
+ function createComponentFragments(contentType, typePath, force, debug, propertyTracker = new Map(), generator) {
2201
+ const baseQueryFile = typePath.fragmentFile;
2202
+ let writeFragment = true;
2203
+ if (fs.existsSync(baseQueryFile)) {
2204
+ if (force) {
2205
+ if (debug)
2206
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
1602
2207
  }
1603
- return result;
1604
- }
1605
- function postProcessDefintions(jsonSchema) {
1606
- if (!jsonSchema.definitions)
1607
- return jsonSchema;
1608
- for (const definitionName in jsonSchema.definitions) {
1609
- if ((definitionName.endsWith("Property") || definitionName.endsWith("ListItem")) && jsonSchema.definitions[definitionName].properties) {
1610
- const type = jsonSchema.definitions[definitionName].properties?.type;
1611
- if (isRefSchema(type)) {
1612
- const typeSchema = resolveRefSchema(type, jsonSchema);
1613
- if (typeSchema && typeSchema.type === 'string' && Array.isArray(typeSchema.enum)) {
1614
- let typeValue = definitionName.substring(0, definitionName.length - 8);
1615
- typeValue = typeValue[0].toLowerCase() + typeValue.substring(1);
1616
- typeValue = typeValue === 'list' ? 'array' : typeValue;
1617
- typeValue = typeValue === 'jsonString' ? 'json' : typeValue;
1618
- if (typeSchema.enum.includes(typeValue)) {
1619
- const newTypeDef = deepmerge$1({}, typeSchema);
1620
- newTypeDef.enum = newTypeDef.enum.filter((x) => x === typeValue);
1621
- jsonSchema.definitions[definitionName].properties.type = newTypeDef;
1622
- }
1623
- }
1624
- }
2208
+ else {
2209
+ if (debug)
2210
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
2211
+ writeFragment = false;
1625
2212
  }
1626
2213
  }
1627
- return jsonSchema;
2214
+ else if (debug) {
2215
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
2216
+ }
2217
+ let written = false;
2218
+ if (writeFragment) {
2219
+ const fragment = generator.buildFragment(contentType, undefined, false, propertyTracker);
2220
+ fs.writeFileSync(baseQueryFile, fragment);
2221
+ written = true;
2222
+ }
2223
+ return { written, propertyTypes: DocumentGenerator.getReferencedPropertyComponents(contentType) };
1628
2224
  }
1629
- /**
1630
- * Schema normalization to tranform the schema from a .Net generated OpenAPI Schema
1631
- * to a AJV compatible JSON Schema
1632
- *
1633
- * @see https://ajv.js.org/
1634
- * @param schema
1635
- * @param defs
1636
- * @param spec
1637
- * @returns
1638
- */
1639
- function processSchema(schema, defs, spec, mergeAllOf = true) {
1640
- if (Array.isArray(schema))
1641
- return schema.map(s => processSchema(s, defs, spec, mergeAllOf));
1642
- if (typeof schema !== 'object' || schema === null)
1643
- return schema;
1644
- const props = Object.getOwnPropertyNames(schema);
1645
- if (props.length === 1 && props[0] === '$ref') {
1646
- const ref = schema['$ref'];
1647
- if (isLocalRef(ref)) {
1648
- const refName = getLocalRefName(ref);
1649
- if (!defs[refName]) {
1650
- const refItem = resolveLocalRef(ref, spec);
1651
- defs[refName] = processSchema(refItem, defs, spec, mergeAllOf);
1652
- }
1653
- const newRef = `#/definitions/${refName}`;
1654
- return { "$ref": newRef };
2225
+ function createPropertyFragments(propertyTypesList, allContentTypes, selectTypeFolder, force = false, debug = false, propertyTracker = new Map(), generator) {
2226
+ const returnValue = [];
2227
+ for (const propertyContentTypeKey of propertyTypesList.filter((v, i, a) => a.indexOf(v, i + 1) <= i)) {
2228
+ // Load the ContentType definition
2229
+ const contentType = allContentTypes.filter(x => x.key == propertyContentTypeKey).at(0);
2230
+ if (!contentType) {
2231
+ console.warn(`🟠 The content type ${propertyContentTypeKey} has been referenced, but is not found in the Optimizely CMS instance`);
2232
+ continue;
1655
2233
  }
1656
- else {
1657
- return schema;
2234
+ // Load the paths for this ContentType
2235
+ const depFolder = selectTypeFolder(contentType);
2236
+ if (!depFolder) {
2237
+ console.warn(`🟠 The content type ${propertyContentTypeKey} cannot be stored in the project`);
2238
+ continue;
1658
2239
  }
1659
- }
1660
- else if (mergeAllOf && props.includes('allOf') && Array.isArray(schema['allOf'])) {
1661
- // process all of
1662
- const newObject = props.reduce((generated, propName) => {
1663
- if (propName != 'allOf')
1664
- generated[propName] = schema[propName];
1665
- return generated;
1666
- }, {});
1667
- const allOfSchemas = schema['allOf'].map((subschema) => {
1668
- const resolvedSchema = isRefSchema(subschema) ? resolveRefSchema(subschema, spec) : subschema;
1669
- const resolved = processSchema(resolvedSchema, defs, spec, mergeAllOf);
1670
- return resolved;
1671
- });
1672
- const merged = allOfSchemas.reduce((previous, current) => deepmerge$1(previous, current), newObject);
1673
- if (newObject['description'])
1674
- merged['description'] = newObject['description'];
1675
- if (newObject['title'])
1676
- merged['title'] = newObject['title'];
1677
- return merged;
1678
- }
1679
- else {
1680
- const newSchema = {};
1681
- for (const key of props) {
1682
- if (key === 'pattern' && typeof schema[key] === 'string') {
1683
- newSchema[key] = safeCreateUnicodeRegExp(schema[key]);
1684
- /*} else if (key === 'readOnly' && schema[key] === true) {
1685
- return undefined*/
1686
- }
1687
- else if (['additionalProperties', 'required', 'properties', 'discriminator'].includes(key) && typeof schema[key] !== 'object') ;
1688
- else if (key === 'discriminator' && !Array.isArray(schema['oneOf'])) ;
1689
- else if (key === 'discriminator' && !schema['type']) {
1690
- newSchema.type = 'object';
2240
+ const propertyFragmentFile = depFolder.propertyFragmentFile;
2241
+ let mustWrite = true;
2242
+ if (fs.existsSync(propertyFragmentFile)) {
2243
+ if (force) {
2244
+ if (debug)
2245
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) property fragment\n`));
1691
2246
  }
1692
2247
  else {
1693
- const keyVal = processSchema(schema[key], defs, spec, mergeAllOf);
1694
- if (keyVal !== undefined)
1695
- newSchema[key] = keyVal;
2248
+ if (debug)
2249
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) property fragment - file already exists\n`));
2250
+ mustWrite = false;
1696
2251
  }
1697
2252
  }
1698
- return newSchema;
2253
+ else if (debug) {
2254
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) property fragment\n`));
2255
+ }
2256
+ if (mustWrite) {
2257
+ const fragment = generator.buildFragment(contentType, undefined, true, propertyTracker);
2258
+ fs.writeFileSync(propertyFragmentFile, fragment);
2259
+ returnValue.push(contentType.key);
2260
+ }
2261
+ // Recurse down for properties that we're not yet rendering
2262
+ const referencedPropertyTypes = DocumentGenerator.getReferencedPropertyComponents(contentType).filter(x => !propertyTypesList.includes(x));
2263
+ if (referencedPropertyTypes.length > 0) {
2264
+ if (debug)
2265
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Component property ${propertyContentTypeKey} uses components a property, recursing down\n`));
2266
+ const additionalProperties = createPropertyFragments(referencedPropertyTypes, allContentTypes, selectTypeFolder, force, debug, propertyTracker, generator);
2267
+ returnValue.push(...additionalProperties);
2268
+ }
1699
2269
  }
2270
+ return returnValue;
1700
2271
  }
1701
- function isRefSchema(schema) {
1702
- if (typeof schema != 'object' || schema == null)
1703
- return false;
1704
- return Object.getOwnPropertyNames(schema).length === 1 && typeof schema['$ref'] === 'string';
1705
- }
1706
- function resolveRefSchema(schema, spec) {
1707
- if (!isRefSchema(schema))
1708
- return undefined;
1709
- return resolveLocalRef(schema['$ref'], spec);
1710
- }
1711
- function isLocalRef(ref) {
1712
- return typeof ref === 'string' && ref.startsWith('#');
1713
- }
1714
- function getLocalRefName(ref) {
1715
- if (!isLocalRef(ref))
1716
- return undefined;
1717
- const refPath = ref.substring(2).split('/');
1718
- return refPath.at(refPath.length - 1);
1719
- }
1720
- function resolveLocalRef(ref, spec) {
1721
- const refPath = ref.substring(2).split('/');
1722
- return refPath.reduce((integrator, current) => {
1723
- return integrator ? integrator[current] : undefined;
1724
- }, spec);
1725
- }
1726
- const replaceEscapedChars = [
1727
- [new RegExp('\\\\ ', 'g'), ' '],
1728
- [new RegExp('\\\\!', 'g'), '!'],
1729
- [new RegExp('\\\\"', 'g'), '"'],
1730
- [new RegExp('\\\\#', 'g'), '#'],
1731
- [new RegExp('\\\\%', 'g'), '%'],
1732
- [new RegExp('\\\\&', 'g'), '&'],
1733
- [new RegExp("\\\\'", 'g'), "'"],
1734
- [new RegExp('\\\\,', 'g'), ','],
1735
- [new RegExp('\\\\-', 'g'), '-'],
1736
- [new RegExp('\\\\:', 'g'), ':'],
1737
- [new RegExp('\\\\;', 'g'), ';'],
1738
- [new RegExp('\\\\<', 'g'), '<'],
1739
- [new RegExp('\\\\=', 'g'), '='],
1740
- [new RegExp('\\\\>', 'g'), '>'],
1741
- [new RegExp('\\\\@', 'g'), '@'],
1742
- [new RegExp('\\\\_', 'g'), '_'],
1743
- [new RegExp('\\\\`', 'g'), '`'],
1744
- [new RegExp('\\\\~', 'g'), '~'],
1745
- [new RegExp('\\\\[zZ]', 'g'), '$'],
1746
- ];
1747
- function safeCreateUnicodeRegExp(pattern) {
1748
- for (const unEscape of replaceEscapedChars) {
1749
- pattern = pattern.replace(unEscape[0], unEscape[1]);
2272
+
2273
+ const NextJsQueriesCommand = {
2274
+ command: "nextjs:queries",
2275
+ describe: "Create the GrapQL Queries to use two queries to load content",
2276
+ builder: yargs => {
2277
+ const updatedArgs = contentTypesBuilder(yargs, { ...ContentTypesArgsDefaults, baseTypes: ['page', 'experience'] });
2278
+ updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
2279
+ return updatedArgs;
2280
+ },
2281
+ handler: async (args, opts) => {
2282
+ // Prepare
2283
+ const { loadedContentTypes, createdTypeFolders } = opts || {};
2284
+ const { components: basePath, _config: { debug }, force, path: appPath } = parseArgs(args);
2285
+ const client = createCmsClient(args);
2286
+ const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
2287
+ const allContentTypesMap = new Map();
2288
+ allContentTypes.forEach(x => {
2289
+ if (x.key)
2290
+ allContentTypesMap.set(x.key, x);
2291
+ });
2292
+ const generator = new DocumentGenerator(allContentTypesMap);
2293
+ const propertyTracker = new PropertyCollisionTracker(appPath);
2294
+ // Start process
2295
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL queries\n`));
2296
+ const dependencies = [];
2297
+ const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
2298
+ const updatedTypes = contentTypes.map(contentType => {
2299
+ const typePath = getTypeFolder(typeFolders, contentType.key);
2300
+ const { written, propertyTypes } = createGraphQueries(contentType, typePath, force, debug, propertyTracker, generator);
2301
+ dependencies.push(...propertyTypes);
2302
+ return written ? contentType.key : undefined;
2303
+ }).filter(x => x).flat();
2304
+ // Report outcome
2305
+ if (updatedTypes.length > 0)
2306
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL queries for ${updatedTypes.join(', ')}\n`));
2307
+ else
2308
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL queries created/updated\n`));
2309
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL property fragments\n`));
2310
+ const generatedProps = createPropertyFragments(dependencies, allContentTypes, (contentType) => {
2311
+ if (!contentType.key)
2312
+ return undefined;
2313
+ let tf = getTypeFolder(typeFolders, contentType.key);
2314
+ if (!tf) {
2315
+ tf = getContentTypePaths(contentType, basePath, true, debug);
2316
+ typeFolders.push(tf);
2317
+ }
2318
+ return tf;
2319
+ }, force, debug, propertyTracker, generator);
2320
+ // Report outcome
2321
+ if (generatedProps.length > 0)
2322
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
2323
+ else
2324
+ process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL property fragments created/updated\n`));
2325
+ if (!opts)
2326
+ process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
1750
2327
  }
1751
- return pattern;
2328
+ };
2329
+ function createGraphQueries(contentType, typePath, force, debug, tracker, generator) {
2330
+ const baseQueryFile = typePath.queryFile;
2331
+ let mustWrite = true;
2332
+ if (fs.existsSync(baseQueryFile)) {
2333
+ if (force) {
2334
+ if (debug)
2335
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
2336
+ }
2337
+ else {
2338
+ if (debug)
2339
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
2340
+ mustWrite = false;
2341
+ }
2342
+ }
2343
+ else if (debug) {
2344
+ process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
2345
+ }
2346
+ if (mustWrite) {
2347
+ const fragment = generator.buildGetQuery(contentType, undefined, tracker);
2348
+ fs.writeFileSync(baseQueryFile, fragment);
2349
+ }
2350
+ return {
2351
+ written: mustWrite,
2352
+ propertyTypes: DocumentGenerator.getReferencedPropertyComponents(contentType)
2353
+ };
1752
2354
  }
1753
2355
 
1754
2356
  const SchemaDownloadCommand = {
@@ -1769,12 +2371,12 @@ const SchemaDownloadCommand = {
1769
2371
  const fullSchemaDir = path.normalize(path.join(projectPath, schemaDir));
1770
2372
  const relativeSchemaDir = path.relative(projectPath, fullSchemaDir);
1771
2373
  process.stdout.write(`\n${figures.arrowRight} Validating schema folder ${relativeSchemaDir}\n`);
1772
- const schemaDirInfo = await fs$1.stat(fullSchemaDir).catch((e) => { if (e.code == 'ENOENT')
2374
+ const schemaDirInfo = await fsAsync.stat(fullSchemaDir).catch((e) => { if (e.code == 'ENOENT')
1773
2375
  return undefined;
1774
2376
  else
1775
2377
  throw e; });
1776
2378
  if (!schemaDirInfo) {
1777
- await fs$1.mkdir(fullSchemaDir, { recursive: true });
2379
+ await fsAsync.mkdir(fullSchemaDir, { recursive: true });
1778
2380
  }
1779
2381
  else if (!schemaDirInfo.isDirectory()) {
1780
2382
  process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] The schema directory exists, but is not a directory (${relativeSchemaDir})\n`)));
@@ -1787,7 +2389,7 @@ const SchemaDownloadCommand = {
1787
2389
  const schemaFile = path.normalize(path.join(fullSchemaDir, `${schema.title.toLowerCase()}.schema.json`));
1788
2390
  const relativePath = path.relative(projectPath, schemaFile);
1789
2391
  try {
1790
- await fs$1.writeFile(schemaFile, JSON.stringify(schema.schema, undefined, 2), {
2392
+ await fsAsync.writeFile(schemaFile, JSON.stringify(schema.schema, undefined, 2), {
1791
2393
  encoding: "utf-8",
1792
2394
  flag: force ? 'w' : 'wx'
1793
2395
  });
@@ -1941,13 +2543,6 @@ function groupErrorsByKeyword(errors) {
1941
2543
  toMerge.enum = [toMerge.enum.reduce((prev, current) => prev ? deepmerge(prev, current) : current, undefined)];
1942
2544
  return toMerge;
1943
2545
  }
1944
- function getValidator(schemaObject) {
1945
- const ajv = new Ajv({
1946
- discriminator: true
1947
- });
1948
- addFormats(ajv);
1949
- return ajv.compile(schemaObject);
1950
- }
1951
2546
 
1952
2547
  const SchemaVsCodeCommand = {
1953
2548
  command: "schema:vscode",
@@ -2034,7 +2629,46 @@ function writeFileAsync(path, data) {
2034
2629
  });
2035
2630
  }
2036
2631
 
2037
- var version = "6.0.0-pre8";
2632
+ const MigrateCommand = {
2633
+ command: "project:migrate",
2634
+ describe: "Automate the directory naming convention update",
2635
+ builder,
2636
+ async handler(args, opts) {
2637
+ const { components: componentsPath} = parseArgs(args);
2638
+ const files = await glob(['*/**/*'], { cwd: componentsPath });
2639
+ const operations = [];
2640
+ for (const file of files) {
2641
+ const fullPath = path.join(componentsPath, file);
2642
+ const pathInfo = await fsAsync.stat(fullPath);
2643
+ if (pathInfo.isDirectory()) { // We only need to rename directories
2644
+ const dirName = path.basename(fullPath);
2645
+ const dirParent = path.dirname(fullPath);
2646
+ const newDirName = keyToSlug(dirName);
2647
+ // We're ignoring casing as changing that will confuse Git
2648
+ if (dirName.toLowerCase() !== newDirName.toLowerCase()) {
2649
+ process.stdout.write(`${figures.arrowRight} Updating ${file}\n`);
2650
+ const newFullPath = path.join(dirParent, newDirName);
2651
+ operations.push({ from: fullPath, to: newFullPath });
2652
+ }
2653
+ }
2654
+ if (pathInfo.isFile() && (fullPath.endsWith('.opti-style.json') || fullPath.endsWith('.opti-type.json'))) {
2655
+ const fileName = path.basename(fullPath);
2656
+ const newFileName = keyToSlug(fileName, { preserveCharacters: ['.'] });
2657
+ if (fileName.toLowerCase() !== newFileName.toLowerCase()) {
2658
+ const filePath = path.dirname(fullPath);
2659
+ const newFullPath = path.join(filePath, newFileName);
2660
+ operations.unshift({ from: fullPath, to: newFullPath });
2661
+ }
2662
+ }
2663
+ }
2664
+ for (const operation of operations) {
2665
+ void await fsAsync.rename(operation.from, operation.to);
2666
+ }
2667
+ NextJsFactoryCommand.handler(args);
2668
+ }
2669
+ };
2670
+
2671
+ var version = "6.0.0-rc.1";
2038
2672
  var name = "opti-cms";
2039
2673
  var APP = {
2040
2674
  version: version,
@@ -2064,10 +2698,10 @@ const CmsVersionCommand = {
2064
2698
  chalk.yellow(chalk.bold("Component")),
2065
2699
  chalk.yellow(chalk.bold("Version"))
2066
2700
  ],
2067
- colWidths: [20, 60],
2701
+ colWidths: [30, 60],
2068
2702
  colAligns: ["left", "left"]
2069
2703
  });
2070
- info.push(["Instance", client.cmsUrl.host]);
2704
+ info.push(["Base URL", versionInfo.baseUrl ?? client.cmsUrl.href]);
2071
2705
  info.push(["Client API", client.apiVersion]);
2072
2706
  info.push(["Service API", versionInfo.apiVersion]);
2073
2707
  info.push(["CMS Build", versionInfo.cmsVersion]);
@@ -2192,7 +2826,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
2192
2826
  process.stderr.write(chalk.redBright(` ${figures.cross}The ContentItem ${key} is a reserved item, skipping deletion\n`));
2193
2827
  return removedCount;
2194
2828
  }
2195
- const deleteResult = await client.contentDelete({ path: { key }, query: { permanent: true } }).then(r => r.key).catch((e) => {
2829
+ const deleteResult = await client.contentDelete({ path: { key }, headers: { "cms-permanent-delete": true } }).then(r => r.key).catch((e) => {
2196
2830
  if (e.status == 404)
2197
2831
  return key;
2198
2832
  throw e;
@@ -2218,8 +2852,7 @@ async function resetSystemTypes(client) {
2218
2852
  body: {
2219
2853
  key: systemType,
2220
2854
  properties: {}
2221
- },
2222
- query: { ignoreDataLossWarnings: true }
2855
+ }
2223
2856
  }).catch((e) => e.status == 404 ? undefined : null);
2224
2857
  if (newType)
2225
2858
  resetTypes++;
@@ -2387,6 +3020,7 @@ const commands = [
2387
3020
  NextJsComponentsCommand,
2388
3021
  NextJsCreateCommand,
2389
3022
  NextJsFactoryCommand,
3023
+ NextJsFragmentsCommand,
2390
3024
  NextJsQueriesCommand,
2391
3025
  NextJsVisualBuilderCommand,
2392
3026
  SchemaDownloadCommand,
@@ -2397,20 +3031,23 @@ const commands = [
2397
3031
  StylesListCommand,
2398
3032
  StylesPullCommand,
2399
3033
  StylesPushCommand,
3034
+ StylesDeleteCommand,
2400
3035
  TypesPullCommand,
2401
- TypesPushCommand
3036
+ TypesPushCommand,
3037
+ MigrateCommand
2402
3038
  ];
2403
3039
 
2404
3040
  async function main() {
2405
- const envFiles = prepare();
2406
- const app = createOptiCmsApp(APP.name, APP.version, undefined, envFiles);
3041
+ const projectDir = getProjectDir();
3042
+ const envFiles = prepare(projectDir);
3043
+ const app = createOptiCmsApp(APP.name, APP.version, undefined, envFiles, projectDir);
2407
3044
  app.command(commands);
2408
3045
  try {
2409
3046
  await app.parse(process.argv.slice(2));
2410
3047
  }
2411
3048
  catch {
2412
3049
  //We're ignoring error here, as yargs will already generate the "nice output" for it
2413
- //console.log('Caught error')
3050
+ //console.log ('Caught error')
2414
3051
  }
2415
3052
  }
2416
3053
  main();