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