@remkoj/optimizely-cms-cli 6.0.0-pre1 → 6.0.0-pre10
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 +61 -17
- package/dist/index.js +1295 -562
- package/dist/index.js.map +1 -1
- package/package.json +29 -17
package/dist/index.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import { globSync, glob } from 'glob';
|
|
1
|
+
import { globSync, glob, globIterate } from 'glob';
|
|
2
2
|
import { config } from 'dotenv';
|
|
3
3
|
import { expand } from 'dotenv-expand';
|
|
4
|
-
import {
|
|
4
|
+
import { readPartialEnvConfig, createClient, OptiCmsVersion, ContentRoots } from '@remkoj/optimizely-cms-api';
|
|
5
5
|
import yargs from 'yargs';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import figures from 'figures';
|
|
10
10
|
import Table from 'cli-table3';
|
|
11
|
+
import fsAsync from 'node:fs/promises';
|
|
11
12
|
import { input, select, confirm } from '@inquirer/prompts';
|
|
13
|
+
import createDeepMerge from '@fastify/deepmerge';
|
|
14
|
+
import { Ajv } from 'ajv';
|
|
15
|
+
import addFormats from 'ajv-formats';
|
|
16
|
+
import GraphQLGen from '@remkoj/optimizely-graph-functions/contenttype-loader';
|
|
17
|
+
import deepEqual from 'fast-deep-equal';
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Prepare the application context, by parsing the .env files in the main
|
|
@@ -17,19 +23,25 @@ import { input, select, confirm } from '@inquirer/prompts';
|
|
|
17
23
|
* @returns A string array with the files processed
|
|
18
24
|
*/
|
|
19
25
|
function prepare() {
|
|
20
|
-
const
|
|
21
|
-
|
|
26
|
+
const globResult = globSync(".env*");
|
|
27
|
+
const envFiles = globResult.sort((a, b) => b.length - a.length).filter(n => n == ".env" || n == ".env.local" || (process.env.NODE_ENV && n == `.env.${process.env.NODE_ENV}.local`));
|
|
28
|
+
expand(config({ path: envFiles, debug: false, quiet: true }));
|
|
22
29
|
return envFiles;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
|
|
33
|
+
if (envFiles) {
|
|
34
|
+
process.stdout.write(chalk.bold(`✅ Loaded environment files:`) + "\n");
|
|
35
|
+
envFiles.forEach(envFile => {
|
|
36
|
+
process.stdout.write(` 👉 ${envFile}\n`);
|
|
37
|
+
});
|
|
38
|
+
process.stdout.write("\n");
|
|
39
|
+
}
|
|
26
40
|
let config;
|
|
27
41
|
try {
|
|
28
|
-
config =
|
|
42
|
+
config = readPartialEnvConfig();
|
|
29
43
|
}
|
|
30
44
|
catch (e) {
|
|
31
|
-
if (envFiles)
|
|
32
|
-
console.error(chalk.gray(`Included environment files: ${envFiles.join(', ')}`));
|
|
33
45
|
console.error(chalk.redBright(`${chalk.bold("[ERROR]:")} Error processing environment variables: ${e.message}`));
|
|
34
46
|
process.exit(1);
|
|
35
47
|
}
|
|
@@ -39,7 +51,7 @@ function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
|
|
|
39
51
|
.usage('$0 <cmd> [args]')
|
|
40
52
|
.option("path", { alias: "p", description: "Application root folder", string: true, type: "string", demandOption: false, default: process.cwd() })
|
|
41
53
|
.option("components", { alias: "c", description: "Path to components folder", string: true, type: "string", demandOption: false, default: "./src/components/cms" })
|
|
42
|
-
.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) })
|
|
54
|
+
.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 })
|
|
43
55
|
.option("client_id", { alias: "ci", description: "API Client ID", string: true, type: "string", demandOption: isDemanded(config.clientId), default: config.clientId })
|
|
44
56
|
.option('client_secret', { alias: "cs", description: "API Client Secrent", string: true, type: "string", demandOption: isDemanded(config.clientSecret), default: config.clientSecret })
|
|
45
57
|
.option('user_id', { alias: "u", description: "Impersonate user id", string: true, type: "string", demandOption: false, default: config.actAs })
|
|
@@ -51,12 +63,10 @@ function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
|
|
|
51
63
|
.epilogue(`Copyright Remko Jantzen - 2023-${(new Date(Date.now())).getFullYear()}`)
|
|
52
64
|
.help()
|
|
53
65
|
.fail((msg, error, args) => {
|
|
54
|
-
if (envFiles)
|
|
55
|
-
console.error(chalk.gray(`Included environment files: ${envFiles.join(', ')}\n`));
|
|
56
66
|
if (msg)
|
|
57
|
-
console.error(msg + "\n");
|
|
67
|
+
console.error(chalk.redBright(msg) + "\n");
|
|
58
68
|
if (error)
|
|
59
|
-
console.error(`[${chalk.bold(error.name ?? 'Error')}]: ${error.message ?? ''}\n
|
|
69
|
+
console.error(chalk.redBright(`[${chalk.bold(error.name ?? 'Error')}]: ${error.message ?? ''}`) + '\n');
|
|
60
70
|
args.showHelp("error");
|
|
61
71
|
});
|
|
62
72
|
}
|
|
@@ -105,16 +115,23 @@ function parseArgs({ client_id, client_secret, cms_url, user_id, verbose, path:
|
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
function createCmsClient(args) {
|
|
108
|
-
const
|
|
109
|
-
const baseConfig =
|
|
118
|
+
const cfg = getCmsIntegrationApiOptions(args);
|
|
119
|
+
const baseConfig = readPartialEnvConfig();
|
|
110
120
|
const client = createClient({
|
|
111
121
|
...baseConfig,
|
|
112
122
|
...cfg
|
|
113
123
|
});
|
|
114
124
|
if (cfg.debug)
|
|
115
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Connecting to ${
|
|
125
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Connecting to ${client.cmsUrl} as ${cfg.actAs ?? cfg.clientId}\n`));
|
|
116
126
|
return client;
|
|
117
127
|
}
|
|
128
|
+
function getCmsIntegrationApiOptions(args) {
|
|
129
|
+
if (typeof args !== 'object' || args === null)
|
|
130
|
+
return undefined;
|
|
131
|
+
if (args.base && (typeof args.base === 'object') && (typeof args.base.href === 'string'))
|
|
132
|
+
return args;
|
|
133
|
+
return parseArgs(args)._config;
|
|
134
|
+
}
|
|
118
135
|
|
|
119
136
|
const StylesPushCommand = {
|
|
120
137
|
command: "styles:push",
|
|
@@ -149,7 +166,12 @@ const StylesPushCommand = {
|
|
|
149
166
|
return undefined; // Only include defined styles, if any
|
|
150
167
|
if (cfg.debug)
|
|
151
168
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Pushing: ${styleKey}\n`));
|
|
152
|
-
|
|
169
|
+
// Try to fetch the current template
|
|
170
|
+
const currentTemplate = await client.displayTemplatesGet({ path: { key: styleKey } }).catch(() => { return undefined; });
|
|
171
|
+
// Create / Replace the current template
|
|
172
|
+
const newTemplate = await (currentTemplate ?
|
|
173
|
+
client.displayTemplatesPatch({ path: { key: styleKey }, body: styleDefinition }) :
|
|
174
|
+
client.displayTemplatesCreate({ body: styleDefinition }));
|
|
153
175
|
return newTemplate;
|
|
154
176
|
}))).filter(isNotNullOrUndefined);
|
|
155
177
|
const styles = new Table({
|
|
@@ -189,80 +211,159 @@ function isNotNullOrUndefined(i) {
|
|
|
189
211
|
return i ? true : false;
|
|
190
212
|
}
|
|
191
213
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
/**
|
|
215
|
+
* This file contains tools that allow using the project that we're targeting
|
|
216
|
+
*/
|
|
217
|
+
/**
|
|
218
|
+
* Get all the file paths for the content type, taking the current project configuration into
|
|
219
|
+
* account.
|
|
220
|
+
*
|
|
221
|
+
* @param contentType
|
|
222
|
+
* @param basePath
|
|
223
|
+
* @returns
|
|
224
|
+
*/
|
|
225
|
+
function getContentTypePaths(contentType, basePath, createFolder = false, debug = false) {
|
|
226
|
+
const baseTypeSlug = typeToSlug(contentType.baseType);
|
|
227
|
+
const typePath = path.join(basePath, baseTypeSlug, contentType.key);
|
|
228
|
+
if (createFolder) {
|
|
229
|
+
if (!fs.existsSync(typePath)) {
|
|
230
|
+
fs.mkdirSync(typePath, { recursive: true });
|
|
231
|
+
if (debug)
|
|
232
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Created folder ${typePath} for Content-Type ${contentType.key}\n`));
|
|
233
|
+
}
|
|
234
|
+
// Check folders
|
|
235
|
+
if (!fs.statSync(typePath).isDirectory())
|
|
236
|
+
throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
|
|
237
|
+
}
|
|
238
|
+
const typeFile = path.join(typePath, `${contentType.key}.opti-type.json`);
|
|
239
|
+
const fragmentFile = path.join(typePath, `${contentType.key}.${baseTypeSlug}.graphql`);
|
|
240
|
+
const propertyFragmentFile = path.join(typePath, `${contentType.key}.property.graphql`);
|
|
241
|
+
const queryFile = path.join(typePath, `get${contentType.key}Data.query.graphql`);
|
|
242
|
+
const componentFile = path.join(typePath, `index.tsx`);
|
|
243
|
+
return {
|
|
244
|
+
type: contentType.key,
|
|
245
|
+
path: typePath,
|
|
246
|
+
typePath,
|
|
247
|
+
typeFile,
|
|
248
|
+
fragmentFile,
|
|
249
|
+
componentFile,
|
|
250
|
+
propertyFragmentFile,
|
|
251
|
+
queryFile
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
*
|
|
256
|
+
* @param typeKey
|
|
257
|
+
* @returns
|
|
258
|
+
*/
|
|
259
|
+
function typeToSlug(typeKey) {
|
|
260
|
+
let typeKeySlug = typeKey.toLowerCase();
|
|
261
|
+
if (typeKeySlug.startsWith('_'))
|
|
262
|
+
typeKeySlug = typeKeySlug.substring(1);
|
|
263
|
+
return typeKeySlug;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const ContentTypesArgsDefaults = {
|
|
267
|
+
excludeBaseTypes: ['folder', 'media', 'image', 'video'],
|
|
268
|
+
excludeTypes: [],
|
|
269
|
+
baseTypes: [],
|
|
270
|
+
types: [],
|
|
271
|
+
all: false
|
|
272
|
+
};
|
|
273
|
+
const contentTypesBuilder = (yargs, defaults = ContentTypesArgsDefaults) => {
|
|
274
|
+
yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: defaults.excludeTypes });
|
|
275
|
+
yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: defaults.excludeBaseTypes });
|
|
276
|
+
yargs.option("baseTypes", { alias: 'b', description: "Select only these base types", string: true, type: 'array', demandOption: false, default: defaults.baseTypes });
|
|
277
|
+
yargs.option("types", { alias: 't', description: "Select only these types", string: true, type: 'array', demandOption: false, default: defaults.types });
|
|
278
|
+
yargs.option('all', { alias: 'a', description: "Include non-supported base types", boolean: true, type: 'boolean', demandOption: false, default: defaults.all });
|
|
198
279
|
return yargs;
|
|
199
280
|
};
|
|
200
|
-
function
|
|
201
|
-
return Object.keys(enumObject).filter((item) => {
|
|
202
|
-
return isNaN(Number(item));
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
async function getContentTypes(client, args, pageSize = 100, allowSystem = false) {
|
|
281
|
+
async function getContentTypes(client, args, pageSize = 25, allowSystem = false, customFilter) {
|
|
206
282
|
const { _config: cfg, excludeBaseTypes, excludeTypes, baseTypes, types, all } = parseArgs(args);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
|
|
213
|
-
while (pagesRemaining > 0 && results.length < resultsPage.totalItemCount) {
|
|
214
|
-
if (cfg.debug)
|
|
215
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${resultsPage.pageIndex + 2} of ${Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize)} (${resultsPage.pageSize} items per page)\n`));
|
|
216
|
-
resultsPage = await client.contentTypesList({ query: { pageIndex: resultsPage.pageIndex + 1, pageSize: resultsPage.pageSize } });
|
|
217
|
-
results.push(...resultsPage.items);
|
|
218
|
-
pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
|
|
219
|
-
}
|
|
220
|
-
if (cfg.debug) {
|
|
221
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
|
|
222
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Content-Types based upon arguments\n`));
|
|
223
|
-
}
|
|
224
|
-
const validBaseTypes = getEnumOptions(IntegrationApi.ContentBaseType).map(x => x.toLowerCase());
|
|
225
|
-
const allContentTypes = all ?
|
|
226
|
-
// If we're returning all content types, including non-supported base types, make sure the base type is always set
|
|
227
|
-
results.map(contentType => {
|
|
228
|
-
return {
|
|
229
|
-
...contentType,
|
|
230
|
-
baseType: contentType.baseType ?? 'default'
|
|
231
|
-
};
|
|
232
|
-
}) :
|
|
233
|
-
// Otherwise filter out any non supported content base type
|
|
234
|
-
results.filter(contentType => {
|
|
235
|
-
const baseType = (contentType.baseType ?? 'default').toLowerCase();
|
|
236
|
-
const isValid = validBaseTypes.includes(baseType);
|
|
237
|
-
if (!isValid && cfg.debug)
|
|
238
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Removing ${contentType.key} as it has an unsupported base type: ${baseType}\n`));
|
|
239
|
-
return isValid;
|
|
240
|
-
});
|
|
241
|
-
const contentTypes = allContentTypes.filter(data => {
|
|
242
|
-
// Remove items based upon filters
|
|
243
|
-
const keepType = (!excludeBaseTypes.includes(data.baseType)) &&
|
|
244
|
-
(!excludeTypes.includes(data.key)) &&
|
|
245
|
-
(baseTypes.length == 0 || baseTypes.includes(data.baseType)) &&
|
|
246
|
-
(types.length == 0 || types.includes(data.key));
|
|
247
|
-
if (!keepType) {
|
|
283
|
+
const allContentTypes = [];
|
|
284
|
+
const filteredContentTypes = [];
|
|
285
|
+
for await (const contentType of getAllContentTypes(client, cfg.debug, pageSize)) {
|
|
286
|
+
// Skip content types mapped against Graph data, these should be used with their source type in Graph, not the reference in CMS
|
|
287
|
+
if (!all && contentType.key.toLowerCase().startsWith('graph:')) {
|
|
248
288
|
if (cfg.debug)
|
|
249
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping
|
|
250
|
-
|
|
289
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it is a reference to external data in Optimizely Graph\n`));
|
|
290
|
+
continue;
|
|
251
291
|
}
|
|
252
|
-
//
|
|
253
|
-
|
|
292
|
+
// Build the unfiltered array
|
|
293
|
+
allContentTypes.push(contentType);
|
|
294
|
+
// Skip based upon base type filters
|
|
295
|
+
if (!shouldInclude(typeToSlug(contentType.baseType), baseTypes.map(x => typeToSlug(x)), excludeBaseTypes.map(x => typeToSlug(x)))) {
|
|
254
296
|
if (cfg.debug)
|
|
255
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping
|
|
256
|
-
|
|
297
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it has a restricted base type: ${contentType.baseType}\n`));
|
|
298
|
+
continue;
|
|
257
299
|
}
|
|
258
|
-
|
|
259
|
-
|
|
300
|
+
// Skip based upon type filters
|
|
301
|
+
if (!shouldInclude(contentType.key, types, excludeTypes)) {
|
|
302
|
+
if (cfg.debug)
|
|
303
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it has a restricted type: ${contentType.key}\n`));
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
// Skip based upon system filter
|
|
307
|
+
if (contentType.source == 'system' && !allowSystem) {
|
|
308
|
+
if (cfg.debug)
|
|
309
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${contentType.key} due to it being a system type\n`));
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
// Skip based upon custom filter
|
|
313
|
+
if (customFilter && !await customFilter(contentType)) {
|
|
314
|
+
if (cfg.debug)
|
|
315
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${contentType.key} due to the custom filter\n`));
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
// Add to list
|
|
319
|
+
filteredContentTypes.push(contentType);
|
|
320
|
+
}
|
|
260
321
|
if (cfg.debug)
|
|
261
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${
|
|
262
|
-
return {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
322
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${allContentTypes.length} to ${filteredContentTypes.length} items\n`));
|
|
323
|
+
return { all: allContentTypes, contentTypes: filteredContentTypes };
|
|
324
|
+
}
|
|
325
|
+
function shouldInclude(value, allow, disallow) {
|
|
326
|
+
// Item is allowed when either allow is not set, an empty array or has the value
|
|
327
|
+
const isAllowed = !Array.isArray(allow) || allow.length === 0 || allow.includes(value);
|
|
328
|
+
// Item is disallowed when and the array is set and includes the value
|
|
329
|
+
const isDisallowed = Array.isArray(disallow) && disallow.includes(value);
|
|
330
|
+
return isAllowed && !isDisallowed;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Retrieve all content types as an Async Generator, allowing processing of entries whilest they are being loaded from the CMS instance.
|
|
334
|
+
*
|
|
335
|
+
* @param client
|
|
336
|
+
* @param args
|
|
337
|
+
* @param pageSize
|
|
338
|
+
*/
|
|
339
|
+
async function* getAllContentTypes(client, debug = false, pageSize = 25) {
|
|
340
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
|
|
341
|
+
let requestPageSize = pageSize;
|
|
342
|
+
let requestPageIndex = 0;
|
|
343
|
+
let totalItemCount = 0;
|
|
344
|
+
let totalPages = 0;
|
|
345
|
+
do {
|
|
346
|
+
const resultsPage = await client.contentTypesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch((_) => {
|
|
347
|
+
return {
|
|
348
|
+
items: [],
|
|
349
|
+
totalItemCount: 0,
|
|
350
|
+
pageIndex: requestPageIndex,
|
|
351
|
+
pageSize: requestPageSize
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
// Calculate fields for next page
|
|
355
|
+
totalItemCount = resultsPage.totalItemCount ?? 0;
|
|
356
|
+
requestPageSize = resultsPage.pageSize;
|
|
357
|
+
requestPageIndex = resultsPage.pageIndex + 1;
|
|
358
|
+
totalPages = Math.ceil(totalItemCount / requestPageSize);
|
|
359
|
+
// Debug output
|
|
360
|
+
if (debug)
|
|
361
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched contentTypes page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page)\n`));
|
|
362
|
+
// Yield items
|
|
363
|
+
for (const contentType of (resultsPage.items ?? [])) {
|
|
364
|
+
yield contentType;
|
|
365
|
+
}
|
|
366
|
+
} while (requestPageIndex < totalPages);
|
|
266
367
|
}
|
|
267
368
|
|
|
268
369
|
const stylesBuilder = yargs => {
|
|
@@ -274,73 +375,90 @@ const stylesBuilder = yargs => {
|
|
|
274
375
|
newArgs.option("templateTypes", { alias: 'tt', description: "Select only these template types", choices: ['node', 'base', 'component'], type: 'array', demandOption: false, default: [] });
|
|
275
376
|
return newArgs;
|
|
276
377
|
};
|
|
277
|
-
async function getStyles(client, args, pageSize =
|
|
378
|
+
async function getStyles(client, args, pageSize = 25) {
|
|
278
379
|
if (client.runtimeCmsVersion == OptiCmsVersion.CMS12)
|
|
279
380
|
return { all: [], styles: [] };
|
|
280
381
|
const { _config: cfg, excludeBaseTypes, excludeTypes, excludeNodeTypes, excludeTemplates, baseTypes, types, nodes, templates, templateTypes } = parseArgs(args);
|
|
281
382
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Style-Definitions from Optimizely CMS\n`));
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (cfg.debug)
|
|
289
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${resultsPage.pageIndex + 2} of ${Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize)} (${resultsPage.pageSize} items per page)\n`));
|
|
290
|
-
resultsPage = await client.displayTemplatesList({ query: { pageIndex: resultsPage.pageIndex + 1, pageSize: resultsPage.pageSize } });
|
|
291
|
-
results.push(...resultsPage.items);
|
|
292
|
-
pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
|
|
293
|
-
}
|
|
294
|
-
if (cfg.debug) {
|
|
295
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Style-Definitions from Optimizely CMS\n`));
|
|
296
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Style-Definitions based upon arguments\n`));
|
|
297
|
-
}
|
|
298
|
-
const styles = results.filter(data => {
|
|
299
|
-
if (isExcluded(data.key, excludeTemplates, templates)) {
|
|
383
|
+
const allDisplayTemplates = [];
|
|
384
|
+
const filteredDisplayTemplates = [];
|
|
385
|
+
for await (const displayTemplate of getAllStyles(client, cfg.debug, pageSize)) {
|
|
386
|
+
allDisplayTemplates.push(displayTemplate);
|
|
387
|
+
const templateType = displayTemplate.baseType ? 'base' : displayTemplate.nodeType ? 'node' : displayTemplate.contentType ? 'component' : 'unknown';
|
|
388
|
+
if (isExcluded(displayTemplate.key, excludeTemplates, templates)) {
|
|
300
389
|
if (cfg.debug)
|
|
301
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${
|
|
302
|
-
|
|
390
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style defintion key filtering active\n`));
|
|
391
|
+
continue;
|
|
303
392
|
}
|
|
304
|
-
const templateType = data.baseType ? 'base' : data.nodeType ? 'node' : data.contentType ? 'component' : 'unknown';
|
|
305
393
|
if (isExcluded(templateType, [], templateTypes)) {
|
|
306
394
|
if (cfg.debug)
|
|
307
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${
|
|
308
|
-
|
|
395
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style type filtering is active\n`));
|
|
396
|
+
continue;
|
|
309
397
|
}
|
|
310
|
-
if (
|
|
398
|
+
if (displayTemplate.baseType && isExcluded(displayTemplate.baseType, excludeBaseTypes, baseTypes)) {
|
|
311
399
|
if (cfg.debug)
|
|
312
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${
|
|
313
|
-
|
|
400
|
+
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`));
|
|
401
|
+
continue;
|
|
314
402
|
}
|
|
315
|
-
if (
|
|
403
|
+
if (displayTemplate.contentType && isExcluded(displayTemplate.contentType, excludeTypes, types)) {
|
|
316
404
|
if (cfg.debug)
|
|
317
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${
|
|
318
|
-
|
|
405
|
+
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`));
|
|
406
|
+
continue;
|
|
319
407
|
}
|
|
320
408
|
if (templateType != 'component' && types.length > 0) {
|
|
321
409
|
if (cfg.debug)
|
|
322
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${
|
|
323
|
-
|
|
410
|
+
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`));
|
|
411
|
+
continue;
|
|
324
412
|
}
|
|
325
|
-
if (
|
|
413
|
+
if (displayTemplate.nodeType && isExcluded(displayTemplate.nodeType, excludeNodeTypes, nodes)) {
|
|
326
414
|
if (cfg.debug)
|
|
327
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${
|
|
328
|
-
|
|
415
|
+
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`));
|
|
416
|
+
continue;
|
|
329
417
|
}
|
|
330
418
|
if (templateType != 'node' && nodes.length > 0) {
|
|
331
419
|
if (cfg.debug)
|
|
332
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${
|
|
333
|
-
|
|
420
|
+
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`));
|
|
421
|
+
continue;
|
|
334
422
|
}
|
|
335
|
-
|
|
336
|
-
}
|
|
423
|
+
filteredDisplayTemplates.push(displayTemplate);
|
|
424
|
+
}
|
|
337
425
|
if (cfg.debug)
|
|
338
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Applied
|
|
426
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Applied style filters, reduced from ${allDisplayTemplates.length} to ${filteredDisplayTemplates.length} items\n`));
|
|
339
427
|
return {
|
|
340
|
-
all:
|
|
341
|
-
styles
|
|
428
|
+
all: allDisplayTemplates,
|
|
429
|
+
styles: filteredDisplayTemplates
|
|
342
430
|
};
|
|
343
431
|
}
|
|
432
|
+
async function* getAllStyles(client, debug = false, pageSize = 5) {
|
|
433
|
+
if (client.runtimeCmsVersion == OptiCmsVersion.CMS12)
|
|
434
|
+
return;
|
|
435
|
+
let requestPageSize = pageSize;
|
|
436
|
+
let requestPageIndex = 0;
|
|
437
|
+
let totalItemCount = 0;
|
|
438
|
+
let totalPages = 0;
|
|
439
|
+
do {
|
|
440
|
+
const resultsPage = await client.displayTemplatesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch((_) => {
|
|
441
|
+
return {
|
|
442
|
+
items: [],
|
|
443
|
+
totalItemCount: 0,
|
|
444
|
+
pageIndex: requestPageIndex,
|
|
445
|
+
pageSize: requestPageSize
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
// Calculate fields for next page
|
|
449
|
+
totalItemCount = resultsPage.totalItemCount ?? 0;
|
|
450
|
+
requestPageSize = resultsPage.pageSize;
|
|
451
|
+
requestPageIndex = resultsPage.pageIndex + 1;
|
|
452
|
+
totalPages = Math.ceil(totalItemCount / requestPageSize);
|
|
453
|
+
// Debug output
|
|
454
|
+
if (debug)
|
|
455
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched displayTemplates page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page)\n`));
|
|
456
|
+
// Yield items
|
|
457
|
+
for (const displayTemplate of (resultsPage.items ?? [])) {
|
|
458
|
+
yield displayTemplate;
|
|
459
|
+
}
|
|
460
|
+
} while (requestPageIndex < totalPages);
|
|
461
|
+
}
|
|
344
462
|
function isExcluded(value, exclusions, inclusions) {
|
|
345
463
|
if (value == undefined || value == null)
|
|
346
464
|
return false;
|
|
@@ -356,18 +474,9 @@ async function getStyleFilePath(definition, opts) {
|
|
|
356
474
|
return `${opts?.contentBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
|
|
357
475
|
if (!opts?.client)
|
|
358
476
|
throw new Error("Neither the contentBaseType, nor the ApiClient has been provided for a definition for a specific ContentType - unable to generate the path");
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
|
|
363
|
-
while (pagesRemaining > 0 && contentTypes.length < resultsPage.totalItemCount) {
|
|
364
|
-
resultsPage = await opts.client.contentTypesList({ query: { pageIndex: resultsPage.pageIndex + 1, pageSize: resultsPage.pageSize } });
|
|
365
|
-
contentTypes.push(...resultsPage.items);
|
|
366
|
-
pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
|
|
367
|
-
}
|
|
368
|
-
const fetchedBaseType = contentTypes.filter(x => x.key == definition.contentType).map(x => x.baseType).at(0);
|
|
369
|
-
if (fetchedBaseType)
|
|
370
|
-
return `${fetchedBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
|
|
477
|
+
const contentType = await opts.client.contentTypesGet({ path: { key: definition.contentType } }).catch(() => undefined);
|
|
478
|
+
if (contentType)
|
|
479
|
+
return `${contentType.baseType}/${definition.contentType}/${definition.key}.opti-style.json`;
|
|
371
480
|
}
|
|
372
481
|
throw new Error(`Unable to resolve the target for the DisplayTemplate: ${definition.key}`);
|
|
373
482
|
}
|
|
@@ -411,7 +520,7 @@ const StylesPullCommand = {
|
|
|
411
520
|
builder: (yargs) => {
|
|
412
521
|
const newYargs = stylesBuilder(yargs);
|
|
413
522
|
newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
414
|
-
newYargs.option("definitions", { alias: '
|
|
523
|
+
newYargs.option("definitions", { alias: 'u', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true });
|
|
415
524
|
return newYargs;
|
|
416
525
|
},
|
|
417
526
|
handler: async (args) => {
|
|
@@ -426,29 +535,9 @@ const StylesPullCommand = {
|
|
|
426
535
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Start creating .opti-style.json files\n`));
|
|
427
536
|
const typeFiles = {};
|
|
428
537
|
const updatedTemplates = (await Promise.all(filteredResults.map(async (displayTemplate) => {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (displayTemplate.nodeType) {
|
|
433
|
-
itemPath = path.join(basePath, 'nodes', displayTemplate.nodeType, displayTemplate.key);
|
|
434
|
-
typesPath = path.join(basePath, 'nodes', displayTemplate.nodeType);
|
|
435
|
-
targetType = 'node/' + displayTemplate.nodeType;
|
|
436
|
-
}
|
|
437
|
-
else if (displayTemplate.baseType) {
|
|
438
|
-
itemPath = path.join(basePath, displayTemplate.baseType, 'styles', displayTemplate.key);
|
|
439
|
-
typesPath = path.join(basePath, displayTemplate.baseType, 'styles');
|
|
440
|
-
targetType = 'base/' + displayTemplate.baseType;
|
|
441
|
-
}
|
|
442
|
-
else if (displayTemplate.contentType) {
|
|
443
|
-
const contentType = await client.contentTypesGet({ path: { key: displayTemplate.contentType ?? '-' } });
|
|
444
|
-
itemPath = path.join(basePath, contentType.baseType, contentType.key);
|
|
445
|
-
typesPath = path.join(basePath, contentType.baseType, contentType.key);
|
|
446
|
-
targetType = 'content/' + displayTemplate.contentType;
|
|
447
|
-
}
|
|
448
|
-
if (!fs.existsSync(itemPath))
|
|
449
|
-
fs.mkdirSync(itemPath, { recursive: true });
|
|
450
|
-
// Write Style JSON
|
|
451
|
-
const filePath = path.join(itemPath, `${displayTemplate.key}.opti-style.json`);
|
|
538
|
+
// Create metadata
|
|
539
|
+
const { targetType, styleFilePath: filePath, helperFilePath: helperPath } = await createTemplateMetadata(client, displayTemplate, basePath, true);
|
|
540
|
+
// Build local style data
|
|
452
541
|
const outputTemplate = { ...displayTemplate };
|
|
453
542
|
if (outputTemplate.createdBy)
|
|
454
543
|
delete outputTemplate.createdBy;
|
|
@@ -458,6 +547,7 @@ const StylesPullCommand = {
|
|
|
458
547
|
delete outputTemplate.created;
|
|
459
548
|
if (outputTemplate.lastModified)
|
|
460
549
|
delete outputTemplate.lastModified;
|
|
550
|
+
// Write file to disk
|
|
461
551
|
if (fs.existsSync(filePath)) {
|
|
462
552
|
if (!force) {
|
|
463
553
|
if (cfg.debug)
|
|
@@ -475,80 +565,20 @@ const StylesPullCommand = {
|
|
|
475
565
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating style file for ${displayTemplate.key} in ${filePath}\n`));
|
|
476
566
|
fs.writeFileSync(filePath, JSON.stringify(outputTemplate, undefined, 2));
|
|
477
567
|
}
|
|
568
|
+
// Ensure we're tracking all files
|
|
478
569
|
if (!typeFiles[targetType]) {
|
|
479
570
|
typeFiles[targetType] = {
|
|
480
|
-
filePath:
|
|
571
|
+
filePath: helperPath,
|
|
481
572
|
templates: []
|
|
482
573
|
};
|
|
483
574
|
}
|
|
484
|
-
typeFiles[targetType].templates.push({ file: filePath, data: displayTemplate });
|
|
575
|
+
typeFiles[targetType].templates.push({ key: displayTemplate.key, file: filePath, data: displayTemplate });
|
|
485
576
|
return displayTemplate.key;
|
|
486
577
|
}))).filter(x => x);
|
|
487
578
|
//#endregion
|
|
488
579
|
//#region Create needed definition files
|
|
489
|
-
if (definitions)
|
|
490
|
-
|
|
491
|
-
for (const targetId of Object.getOwnPropertyNames(typeFiles)) {
|
|
492
|
-
const { filePath: typeFilePath, templates } = typeFiles[targetId];
|
|
493
|
-
if (fs.existsSync(typeFilePath)) {
|
|
494
|
-
if (!force) {
|
|
495
|
-
if (cfg.debug)
|
|
496
|
-
process.stdout.write(chalk.gray(`${figures.cross} Skipped writing definition file for ${targetId} - it already exists\n`));
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
if (cfg.debug)
|
|
500
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting definition file for ${targetId} - ${typeFilePath}\n`));
|
|
501
|
-
}
|
|
502
|
-
else if (cfg.debug)
|
|
503
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating definition file for ${targetId} - ${typeFilePath}\n`));
|
|
504
|
-
// Write Style definition
|
|
505
|
-
const imports = [
|
|
506
|
-
'import type { LayoutProps } from "@remkoj/optimizely-cms-react"',
|
|
507
|
-
'import type { ReactNode } from "react"'
|
|
508
|
-
];
|
|
509
|
-
const typeContents = [];
|
|
510
|
-
const props = [];
|
|
511
|
-
let typeId = targetId.split('/', 2)[1];
|
|
512
|
-
templates.forEach(({ file: displayTemplateFile, data: displayTemplate }) => {
|
|
513
|
-
const importPath = path.relative(path.dirname(typeFilePath), displayTemplateFile).replaceAll('\\', '/');
|
|
514
|
-
imports.push(`import type ${displayTemplate.key}Styles from "./${importPath}"`);
|
|
515
|
-
typeContents.push(`export type ${displayTemplate.key}Props = LayoutProps<typeof ${displayTemplate.key}Styles>`);
|
|
516
|
-
typeContents.push(`export type ${displayTemplate.key}ComponentProps<DT extends Record<string, any> = Record<string, any>> = {
|
|
517
|
-
data: DT
|
|
518
|
-
layoutProps: ${displayTemplate.key}Props | undefined
|
|
519
|
-
} & JSX.IntrinsicElements['div']`);
|
|
520
|
-
typeContents.push(`export type ${displayTemplate.key}Component<DT extends Record<string, any> = Record<string, any>> = (props: ${displayTemplate.key}ComponentProps<DT>) => ReactNode`);
|
|
521
|
-
typeContents.push('');
|
|
522
|
-
props.push(`${displayTemplate.key}Props`);
|
|
523
|
-
if (!typeId)
|
|
524
|
-
typeId = displayTemplate.nodeType ?? displayTemplate.baseType ?? displayTemplate.contentType;
|
|
525
|
-
});
|
|
526
|
-
if (typeId) {
|
|
527
|
-
typeId = ucFirst$1(typeId);
|
|
528
|
-
typeContents.push('');
|
|
529
|
-
typeContents.push(`export type ${typeId}LayoutProps = ${props.join(' | ')}
|
|
530
|
-
export type ${typeId}ComponentProps<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = {
|
|
531
|
-
data: DT
|
|
532
|
-
layoutProps: LP | undefined
|
|
533
|
-
} & JSX.IntrinsicElements['div']
|
|
534
|
-
|
|
535
|
-
export type ${typeId}Component<DT extends Record<string, any> = Record<string, any>, LP extends ${typeId}LayoutProps = ${typeId}LayoutProps> = (props: ${typeId}ComponentProps<DT,LP>) => ReactNode
|
|
536
|
-
|
|
537
|
-
export function isDefaultProps(props?: ${typeId}LayoutProps | null) : props is ${templates.filter(t => t.data.isDefault).at(0)?.data?.key}Props
|
|
538
|
-
{
|
|
539
|
-
return props?.template == "${templates.filter(t => t.data.isDefault).at(0)?.data?.key}"
|
|
540
|
-
}`);
|
|
541
|
-
templates.forEach(t => {
|
|
542
|
-
typeContents.push(`
|
|
543
|
-
export function is${t.data.key}Props(props?: ${typeId}LayoutProps | null) : props is ${t.data.key}Props
|
|
544
|
-
{
|
|
545
|
-
return props?.template == "${t.data.key}"
|
|
546
|
-
}`);
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
fs.writeFileSync(typeFilePath, imports.join("\n") + "\n\n" + typeContents.join("\n"));
|
|
550
|
-
}
|
|
551
|
-
}
|
|
580
|
+
if (definitions)
|
|
581
|
+
await createDisplayTemplateHelpers(typeFiles);
|
|
552
582
|
//#endregion
|
|
553
583
|
process.stdout.write(chalk.green(chalk.bold(figures.tick + ` Created/updated style definitions for ${updatedTemplates.join(', ')}`)) + "\n");
|
|
554
584
|
}
|
|
@@ -558,6 +588,131 @@ function ucFirst$1(input) {
|
|
|
558
588
|
return input;
|
|
559
589
|
return input[0].toUpperCase() + input.substring(1);
|
|
560
590
|
}
|
|
591
|
+
async function createTemplateMetadata(client, displayTemplate, basePath, createPaths = true) {
|
|
592
|
+
let itemPath = undefined;
|
|
593
|
+
let targetType;
|
|
594
|
+
let typesPath;
|
|
595
|
+
const targetPrefix = getTemplateTarget(displayTemplate);
|
|
596
|
+
switch (targetPrefix) {
|
|
597
|
+
case 'node':
|
|
598
|
+
itemPath = path.join(basePath, 'nodes', displayTemplate.nodeType, displayTemplate.key);
|
|
599
|
+
typesPath = path.join(basePath, 'nodes', displayTemplate.nodeType);
|
|
600
|
+
targetType = targetPrefix + '/' + displayTemplate.nodeType;
|
|
601
|
+
break;
|
|
602
|
+
case 'base': {
|
|
603
|
+
const baseTypeSlug = baseTypeToSlug(displayTemplate.baseType);
|
|
604
|
+
itemPath = path.join(basePath, baseTypeSlug, 'styles', displayTemplate.key);
|
|
605
|
+
typesPath = path.join(basePath, baseTypeSlug, 'styles');
|
|
606
|
+
targetType = targetPrefix + '/' + baseTypeSlug;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case 'content': {
|
|
610
|
+
const contentType = await client.contentTypesGet({ path: { key: displayTemplate.contentType ?? '-' } });
|
|
611
|
+
const baseTypeSlug = baseTypeToSlug(contentType.baseType);
|
|
612
|
+
itemPath = path.join(basePath, baseTypeSlug, contentType.key);
|
|
613
|
+
typesPath = path.join(basePath, baseTypeSlug, contentType.key);
|
|
614
|
+
targetType = targetPrefix + '/' + displayTemplate.contentType;
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
default:
|
|
618
|
+
throw new Error("Unsupported display template type");
|
|
619
|
+
}
|
|
620
|
+
if (createPaths)
|
|
621
|
+
void await fsAsync.mkdir(itemPath, { recursive: true }).catch(e => {
|
|
622
|
+
console.log(e);
|
|
623
|
+
});
|
|
624
|
+
const styleFilePath = path.join(itemPath, `${displayTemplate.key}.opti-style.json`);
|
|
625
|
+
const helperFilePath = path.join(typesPath, 'displayTemplates.ts');
|
|
626
|
+
return { key: displayTemplate.key, itemPath, typesPath, targetType, styleFilePath, helperFilePath };
|
|
627
|
+
}
|
|
628
|
+
function baseTypeToSlug(baseType) {
|
|
629
|
+
return baseType.replace(/^\_*/, "").toLowerCase();
|
|
630
|
+
}
|
|
631
|
+
function getTemplateTarget(displayTemplate) {
|
|
632
|
+
if (displayTemplate.baseType && displayTemplate.baseType.length > 0)
|
|
633
|
+
return 'base';
|
|
634
|
+
if (displayTemplate.nodeType && displayTemplate.nodeType.length > 0)
|
|
635
|
+
return 'node';
|
|
636
|
+
if (displayTemplate.contentType && displayTemplate.contentType.length > 0)
|
|
637
|
+
return 'content';
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
async function createDisplayTemplateHelpers(typeFiles, force = false, debug = false) {
|
|
641
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Start creating displayTemplates.ts files\n`));
|
|
642
|
+
for (const targetId in typeFiles) {
|
|
643
|
+
await createDisplayTemplateHelper(typeFiles[targetId], targetId, force, debug);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async function createDisplayTemplateHelper(typeFile, typeFileId, force = false, debug = false) {
|
|
647
|
+
const prefix = '//not-modified - Remove this line when making change to prevent it from being updated by the CLI tools';
|
|
648
|
+
const { filePath: typeFilePath, templates } = typeFile;
|
|
649
|
+
const shouldWrite = await fsAsync.readFile(typeFilePath, { encoding: "utf-8" }).then(data => data.startsWith('//not-modified')).catch((e) => {
|
|
650
|
+
if (e?.code === 'ENOENT')
|
|
651
|
+
return true;
|
|
652
|
+
if (debug)
|
|
653
|
+
process.stdout.write(chalk.redBright(chalk.bold(`${figures.cross} Unexpected error while reading display template file\n`)));
|
|
654
|
+
return false;
|
|
655
|
+
});
|
|
656
|
+
if (!shouldWrite) {
|
|
657
|
+
if (!force) {
|
|
658
|
+
if (debug)
|
|
659
|
+
process.stdout.write(chalk.gray(`${figures.cross} Skipped writing definition file for ${typeFileId} - it already exists and has been modified\n`));
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
if (debug)
|
|
664
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Forcefully overwriting definition file for ${typeFileId} - ${typeFilePath}\n`));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else if (debug)
|
|
668
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating or updating definition file for ${typeFileId} - ${typeFilePath}\n`));
|
|
669
|
+
// Write Style definition
|
|
670
|
+
const imports = [
|
|
671
|
+
'import type { LayoutProps, LayoutPropsSettingKeys, LayoutPropsSettingValues, CmsComponentProps } from "@remkoj/optimizely-cms-react"',
|
|
672
|
+
'import type { JSX, ComponentType } from "react"'
|
|
673
|
+
];
|
|
674
|
+
const typeContents = [];
|
|
675
|
+
const props = [];
|
|
676
|
+
let typeId = typeFileId.split('/', 2)[1];
|
|
677
|
+
templates.forEach(({ file: displayTemplateFile, data: displayTemplate }) => {
|
|
678
|
+
const importPath = path.relative(path.dirname(typeFilePath), displayTemplateFile).replaceAll('\\', '/');
|
|
679
|
+
imports.push(`import type ${displayTemplate.key}Styles from "./${importPath}"`);
|
|
680
|
+
typeContents.push(`export type ${displayTemplate.key}Props = LayoutProps<typeof ${displayTemplate.key}Styles>`);
|
|
681
|
+
typeContents.push(`export type ${displayTemplate.key}Keys = LayoutPropsSettingKeys<${displayTemplate.key}Props>`);
|
|
682
|
+
typeContents.push(`export type ${displayTemplate.key}Options<K extends ${displayTemplate.key}Keys> = LayoutPropsSettingValues<${displayTemplate.key}Props, K>`);
|
|
683
|
+
typeContents.push(`export type ${displayTemplate.key}ComponentProps<DT extends Record<string, any> = Record<string, any>> = Omit<CmsComponentProps<DT, ${displayTemplate.key}Props>,'children'> & JSX.IntrinsicElements['div']`);
|
|
684
|
+
typeContents.push(`export type ${displayTemplate.key}Component<DT extends Record<string, any> = Record<string, any>> = ComponentType<${displayTemplate.key}ComponentProps<DT>>`);
|
|
685
|
+
typeContents.push('');
|
|
686
|
+
props.push(`${displayTemplate.key}Props`);
|
|
687
|
+
if (!typeId)
|
|
688
|
+
typeId = displayTemplate.nodeType ?? displayTemplate.baseType ?? displayTemplate.contentType;
|
|
689
|
+
});
|
|
690
|
+
if (typeId) {
|
|
691
|
+
typeId = ucFirst$1(typeId);
|
|
692
|
+
typeContents.push(`export type ${typeId}LayoutProps = ${props.join(' | ')}
|
|
693
|
+
export type ${typeId}LayoutKeys = LayoutPropsSettingKeys<${typeId}LayoutProps>
|
|
694
|
+
export type ${typeId}LayoutOptions<K extends ${typeId}LayoutKeys> = LayoutPropsSettingValues<${typeId}LayoutProps,K>
|
|
695
|
+
export type ${typeId}ComponentProps<DT extends Record<string, any> = Record<string, any>> = Omit<CmsComponentProps<DT, ${typeId}LayoutProps>,'children'> & JSX.IntrinsicElements['div']
|
|
696
|
+
export type ${typeId}Component<DT extends Record<string, any> = Record<string, any>> = ComponentType<${typeId}ComponentProps<DT>>`);
|
|
697
|
+
const defaultTemplate = templates.find(t => t.data.isDefault);
|
|
698
|
+
if (defaultTemplate) {
|
|
699
|
+
typeContents.push(`
|
|
700
|
+
export function isDefaultProps(props?: ${typeId}LayoutProps | null) : props is ${defaultTemplate.data?.key}Props
|
|
701
|
+
{
|
|
702
|
+
return props?.template == "${defaultTemplate.data?.key}"
|
|
703
|
+
}`);
|
|
704
|
+
}
|
|
705
|
+
templates.forEach(t => {
|
|
706
|
+
typeContents.push(`
|
|
707
|
+
export function is${t.data.key}Props(props?: ${typeId}LayoutProps | null) : props is ${t.data.key}Props
|
|
708
|
+
{
|
|
709
|
+
return props?.template == "${t.data.key}"
|
|
710
|
+
}`);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
void await fsAsync.writeFile(typeFilePath, prefix + "\n" + imports.join("\n") + "\n\n" + typeContents.join("\n"));
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
561
716
|
|
|
562
717
|
// Node JS and 3rd Party libraries
|
|
563
718
|
const StylesCreateCommand = {
|
|
@@ -573,11 +728,11 @@ const StylesCreateCommand = {
|
|
|
573
728
|
process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
|
|
574
729
|
return;
|
|
575
730
|
}
|
|
576
|
-
const allowedBaseTypes = ['section', '
|
|
731
|
+
const allowedBaseTypes = ['section', 'component', 'experience'];
|
|
577
732
|
// Prepare
|
|
578
733
|
process.stdout.write(chalk.yellowBright(chalk.bold(`Reading current information from Optimizely CMS\n`)));
|
|
579
734
|
const [{ contentTypes }, { styles }] = await Promise.all([
|
|
580
|
-
getContentTypes(client, { ...args, excludeBaseTypes: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [], all: false }),
|
|
735
|
+
getContentTypes(client, { ...args, excludeBaseTypes: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [], all: false }, 25, false, (ct) => ct.compositionBehaviors?.includes('elementEnabled') || ct.compositionBehaviors?.includes('sectionEnabled') || ct.baseType == 'experience' || ct.baseType == 'section'),
|
|
581
736
|
getStyles(client, { ...args, excludeBaseTypes: [], excludeNodeTypes: [], excludeTemplates: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [], nodes: [], components: '', templates: [], templateTypes: [], all: false })
|
|
582
737
|
]);
|
|
583
738
|
const styleKeys = styles.map(x => x.key);
|
|
@@ -585,27 +740,34 @@ const StylesCreateCommand = {
|
|
|
585
740
|
process.stdout.write(chalk.yellowBright(chalk.bold(`\nConfigure your style definition\n`)));
|
|
586
741
|
const key = await input({ message: "Identifier:", validate: (val) => { return val.match(/^[a-zA-Z][A-Za-z0-9\-_]*$/) != null && !styleKeys.includes(val); } });
|
|
587
742
|
const displayName = await input({ message: "Display name:" });
|
|
588
|
-
const type = (await select({
|
|
589
|
-
|
|
590
|
-
{ value: "
|
|
591
|
-
{ value: "
|
|
592
|
-
|
|
743
|
+
const type = (await select({
|
|
744
|
+
message: "Style target:", choices: [
|
|
745
|
+
{ value: "baseType", name: "Base type", description: "Target all Content-Types that inherit from the specified base type" },
|
|
746
|
+
{ value: "contentType", name: "Content type", description: "Target a specific Content-Type that supports styling" },
|
|
747
|
+
{ value: "nodeType", name: "Node", description: "Target a specific node type from a Section" }
|
|
748
|
+
]
|
|
749
|
+
}));
|
|
593
750
|
let typeId = "";
|
|
594
751
|
switch (type) {
|
|
595
752
|
case 'contentType':
|
|
596
753
|
typeId = await select({ message: "Content-Type:", choices: contentTypes.map(x => { return { value: x.key, description: x.description, name: x.displayName }; }) });
|
|
597
754
|
break;
|
|
598
755
|
case 'nodeType':
|
|
599
|
-
typeId = await select({
|
|
756
|
+
typeId = await select({
|
|
757
|
+
message: "Node type:", choices: [
|
|
600
758
|
{ value: "row", description: "Row", name: "Row" },
|
|
601
759
|
{ value: "column", description: "Column", name: "Column" }
|
|
602
|
-
]
|
|
760
|
+
]
|
|
761
|
+
});
|
|
603
762
|
break;
|
|
604
763
|
case 'baseType':
|
|
605
|
-
typeId = await select({
|
|
764
|
+
typeId = await select({
|
|
765
|
+
message: "Base type:", choices: [
|
|
766
|
+
{ value: "experience", description: "Target all experience types", name: "Experience" },
|
|
606
767
|
{ value: "section", description: "Target all section types", name: "Section" },
|
|
607
|
-
{ value: "
|
|
608
|
-
]
|
|
768
|
+
{ value: "component", description: "Target all component types", name: "Component" }
|
|
769
|
+
]
|
|
770
|
+
});
|
|
609
771
|
break;
|
|
610
772
|
}
|
|
611
773
|
const isDefault = await confirm({ message: "Should this style be marked as default?" });
|
|
@@ -625,8 +787,18 @@ const StylesCreateCommand = {
|
|
|
625
787
|
process.exit(0);
|
|
626
788
|
}
|
|
627
789
|
}
|
|
790
|
+
// @ToDo: Add properties
|
|
628
791
|
fs.writeFileSync(path.join(basePath, styleFilePath), JSON.stringify(definition, undefined, 4));
|
|
629
792
|
process.stdout.write(chalk.yellowBright(chalk.bold(`\nWritten style defintion template to: ${path.normalize(path.join(basePath, styleFilePath))}\n`)));
|
|
793
|
+
if (await confirm({ message: "Do you want to publish this style into Optimizely CMS?" })) {
|
|
794
|
+
const response = await client.displayTemplatesCreate({ body: definition }).catch(_ => undefined);
|
|
795
|
+
if (!response) {
|
|
796
|
+
process.stderr.write(chalk.redBright(chalk.bold(`\[ERROR] Unable to create style definition ${definition.displayName} in Optimizely CMS\n`)));
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
process.stdout.write(chalk.yellowBright(chalk.bold(`\nCreated style definition ${definition.displayName} in Optimizely CMS\n`)));
|
|
800
|
+
}
|
|
801
|
+
}
|
|
630
802
|
process.stdout.write(chalk.yellowBright(`\n1. Add your properties to the 'settings' list in the file`));
|
|
631
803
|
process.stdout.write(chalk.yellowBright(`\n2. Run `));
|
|
632
804
|
process.stdout.write(chalk.whiteBright(`yarn opti-cms styles:push -t ${definition.key}`));
|
|
@@ -652,8 +824,7 @@ const TypesPullCommand = {
|
|
|
652
824
|
const client = createCmsClient(args);
|
|
653
825
|
const { contentTypes } = await getContentTypes(client, args);
|
|
654
826
|
const updatedTypes = contentTypes.map(contentType => {
|
|
655
|
-
const typePath =
|
|
656
|
-
const typeFile = path.join(typePath, `${contentType.key}.opti-type.json`);
|
|
827
|
+
const { typePath, typeFile } = getContentTypePaths(contentType, basePath);
|
|
657
828
|
if (!fs.existsSync(typePath))
|
|
658
829
|
fs.mkdirSync(typePath, { recursive: true });
|
|
659
830
|
if (fs.existsSync(typeFile) && !force) {
|
|
@@ -664,16 +835,28 @@ const TypesPullCommand = {
|
|
|
664
835
|
const outContentType = { ...contentType };
|
|
665
836
|
if (outContentType.source || outContentType.source == "")
|
|
666
837
|
delete outContentType.source;
|
|
667
|
-
if (outContentType.features)
|
|
668
|
-
|
|
669
|
-
if (outContentType.usage)
|
|
670
|
-
delete outContentType.usage;
|
|
838
|
+
//if (outContentType.features) delete outContentType.features
|
|
839
|
+
//if (outContentType.usage) delete outContentType.usage
|
|
671
840
|
if (outContentType.lastModifiedBy)
|
|
672
841
|
delete outContentType.lastModifiedBy;
|
|
673
842
|
if (outContentType.lastModified)
|
|
674
843
|
delete outContentType.lastModified;
|
|
675
844
|
if (outContentType.created)
|
|
676
845
|
delete outContentType.created;
|
|
846
|
+
for (const propName of Object.getOwnPropertyNames(outContentType.properties ?? {})) {
|
|
847
|
+
if (!["content", "contentReference"].includes(outContentType.properties[propName].type)) {
|
|
848
|
+
if (isEmptyArray(outContentType.properties[propName].allowedTypes))
|
|
849
|
+
delete outContentType.properties[propName].allowedTypes;
|
|
850
|
+
if (isEmptyArray(outContentType.properties[propName].restrictedTypes))
|
|
851
|
+
delete outContentType.properties[propName].restrictedTypes;
|
|
852
|
+
}
|
|
853
|
+
if (outContentType.properties[propName].type == 'array' && !["content", "contentReference"].includes(outContentType.properties[propName].items?.type)) {
|
|
854
|
+
if (isEmptyArray(outContentType.properties[propName].items.allowedTypes))
|
|
855
|
+
delete outContentType.properties[propName].items.allowedTypes;
|
|
856
|
+
if (isEmptyArray(outContentType.properties[propName].items.restrictedTypes))
|
|
857
|
+
delete outContentType.properties[propName].items.restrictedTypes;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
677
860
|
if (debug)
|
|
678
861
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key})\n`));
|
|
679
862
|
fs.writeFileSync(typeFile, JSON.stringify(outContentType, undefined, 2));
|
|
@@ -682,6 +865,197 @@ const TypesPullCommand = {
|
|
|
682
865
|
process.stdout.write(chalk.green(chalk.bold(`${figures.tick} Created/updated type definitions for ${updatedTypes.join(', ')}\n`)));
|
|
683
866
|
}
|
|
684
867
|
};
|
|
868
|
+
function isEmptyArray(toTest) {
|
|
869
|
+
return toTest === null || !Array.isArray(toTest) || toTest.length === 0;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const deepmerge$1 = createDeepMerge();
|
|
873
|
+
async function loadSchema(client, schemaName) {
|
|
874
|
+
const schemas = Array.isArray(schemaName) ? schemaName : [schemaName];
|
|
875
|
+
process.stdout.write(`${figures.arrowRight} Downloading current JSON Schema\n`);
|
|
876
|
+
const spec = await client.getOpenApiSpec();
|
|
877
|
+
const specSchemas = spec.components?.schemas ?? {};
|
|
878
|
+
process.stdout.write(` ${figures.tick} Complete\n`);
|
|
879
|
+
const result = [];
|
|
880
|
+
process.stdout.write(`\n${figures.arrowRight} Constructing schema for ${schemas.join(', ')}\n`);
|
|
881
|
+
for await (const schema of schemas)
|
|
882
|
+
if (specSchemas[schema]) {
|
|
883
|
+
const definitions = {};
|
|
884
|
+
const processedSchema = processSchema(specSchemas[schema], definitions, spec);
|
|
885
|
+
const jsonSchema = {
|
|
886
|
+
//"$schema": "https://json-schema.org/draft-07/schema",
|
|
887
|
+
"$id": new URL(`schema/${schema}`, client.getSchemaItemBase()).href,
|
|
888
|
+
type: "object",
|
|
889
|
+
title: schema,
|
|
890
|
+
...processedSchema,
|
|
891
|
+
definitions
|
|
892
|
+
};
|
|
893
|
+
result.push({
|
|
894
|
+
title: schema,
|
|
895
|
+
schema: postProcessDefintions(jsonSchema)
|
|
896
|
+
});
|
|
897
|
+
process.stdout.write(` ${figures.tick} Constructed schema of ${schema}\n`);
|
|
898
|
+
}
|
|
899
|
+
return result;
|
|
900
|
+
}
|
|
901
|
+
function getValidator(schemaObject) {
|
|
902
|
+
const ajv = new Ajv({
|
|
903
|
+
discriminator: true
|
|
904
|
+
});
|
|
905
|
+
addFormats(ajv);
|
|
906
|
+
return ajv.compile(schemaObject);
|
|
907
|
+
}
|
|
908
|
+
function postProcessDefintions(jsonSchema) {
|
|
909
|
+
if (!jsonSchema.definitions)
|
|
910
|
+
return jsonSchema;
|
|
911
|
+
for (const definitionName in jsonSchema.definitions) {
|
|
912
|
+
if ((definitionName.endsWith("Property") || definitionName.endsWith("ListItem")) && jsonSchema.definitions[definitionName].properties) {
|
|
913
|
+
const type = jsonSchema.definitions[definitionName].properties?.type;
|
|
914
|
+
if (isRefSchema(type)) {
|
|
915
|
+
const typeSchema = resolveRefSchema(type, jsonSchema);
|
|
916
|
+
if (typeSchema && typeSchema.type === 'string' && Array.isArray(typeSchema.enum)) {
|
|
917
|
+
let typeValue = definitionName.substring(0, definitionName.length - 8);
|
|
918
|
+
typeValue = typeValue[0].toLowerCase() + typeValue.substring(1);
|
|
919
|
+
typeValue = typeValue === 'list' ? 'array' : typeValue;
|
|
920
|
+
typeValue = typeValue === 'jsonString' ? 'json' : typeValue;
|
|
921
|
+
if (typeSchema.enum.includes(typeValue)) {
|
|
922
|
+
const newTypeDef = deepmerge$1({}, typeSchema);
|
|
923
|
+
newTypeDef.enum = newTypeDef.enum.filter((x) => x === typeValue);
|
|
924
|
+
jsonSchema.definitions[definitionName].properties.type = newTypeDef;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return jsonSchema;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Schema normalization to tranform the schema from a .Net generated OpenAPI Schema
|
|
934
|
+
* to a AJV compatible JSON Schema
|
|
935
|
+
*
|
|
936
|
+
* @see https://ajv.js.org/
|
|
937
|
+
* @param schema
|
|
938
|
+
* @param defs
|
|
939
|
+
* @param spec
|
|
940
|
+
* @returns
|
|
941
|
+
*/
|
|
942
|
+
function processSchema(schema, defs, spec, mergeAllOf = true) {
|
|
943
|
+
if (Array.isArray(schema))
|
|
944
|
+
return schema.map(s => processSchema(s, defs, spec, mergeAllOf));
|
|
945
|
+
if (typeof schema !== 'object' || schema === null)
|
|
946
|
+
return schema;
|
|
947
|
+
const props = Object.getOwnPropertyNames(schema);
|
|
948
|
+
if (props.length === 1 && props[0] === '$ref') {
|
|
949
|
+
const ref = schema['$ref'];
|
|
950
|
+
if (isLocalRef(ref)) {
|
|
951
|
+
const refName = getLocalRefName(ref);
|
|
952
|
+
if (!defs[refName]) {
|
|
953
|
+
const refItem = resolveLocalRef(ref, spec);
|
|
954
|
+
defs[refName] = processSchema(refItem, defs, spec, mergeAllOf);
|
|
955
|
+
}
|
|
956
|
+
const newRef = `#/definitions/${refName}`;
|
|
957
|
+
return { "$ref": newRef };
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
return schema;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
else if (mergeAllOf && props.includes('allOf') && Array.isArray(schema['allOf'])) {
|
|
964
|
+
// process all of
|
|
965
|
+
const newObject = props.reduce((generated, propName) => {
|
|
966
|
+
if (propName != 'allOf')
|
|
967
|
+
generated[propName] = schema[propName];
|
|
968
|
+
return generated;
|
|
969
|
+
}, {});
|
|
970
|
+
const allOfSchemas = schema['allOf'].map((subschema) => {
|
|
971
|
+
const resolvedSchema = isRefSchema(subschema) ? resolveRefSchema(subschema, spec) : subschema;
|
|
972
|
+
const resolved = processSchema(resolvedSchema, defs, spec, mergeAllOf);
|
|
973
|
+
return resolved;
|
|
974
|
+
});
|
|
975
|
+
const merged = allOfSchemas.reduce((previous, current) => deepmerge$1(previous, current), newObject);
|
|
976
|
+
if (newObject['description'])
|
|
977
|
+
merged['description'] = newObject['description'];
|
|
978
|
+
if (newObject['title'])
|
|
979
|
+
merged['title'] = newObject['title'];
|
|
980
|
+
return merged;
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
const newSchema = {};
|
|
984
|
+
for (const key of props) {
|
|
985
|
+
if (key === 'pattern' && typeof schema[key] === 'string') {
|
|
986
|
+
newSchema[key] = safeCreateUnicodeRegExp(schema[key]);
|
|
987
|
+
/*} else if (key === 'readOnly' && schema[key] === true) {
|
|
988
|
+
return undefined*/
|
|
989
|
+
}
|
|
990
|
+
else if (['additionalProperties', 'required', 'properties', 'discriminator'].includes(key) && typeof schema[key] !== 'object') ;
|
|
991
|
+
else if (key === 'discriminator' && !Array.isArray(schema['oneOf'])) ;
|
|
992
|
+
else if (key === 'discriminator' && !schema['type']) {
|
|
993
|
+
newSchema.type = 'object';
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
const keyVal = processSchema(schema[key], defs, spec, mergeAllOf);
|
|
997
|
+
if (keyVal !== undefined)
|
|
998
|
+
newSchema[key] = keyVal;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (newSchema['oneOf'] && Array.isArray(newSchema['oneOf']) && newSchema['nullable']) {
|
|
1002
|
+
delete newSchema['nullable'];
|
|
1003
|
+
}
|
|
1004
|
+
return newSchema;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function isRefSchema(schema) {
|
|
1008
|
+
if (typeof schema != 'object' || schema == null)
|
|
1009
|
+
return false;
|
|
1010
|
+
return Object.getOwnPropertyNames(schema).length === 1 && typeof schema['$ref'] === 'string';
|
|
1011
|
+
}
|
|
1012
|
+
function resolveRefSchema(schema, spec) {
|
|
1013
|
+
if (!isRefSchema(schema))
|
|
1014
|
+
return undefined;
|
|
1015
|
+
return resolveLocalRef(schema['$ref'], spec);
|
|
1016
|
+
}
|
|
1017
|
+
function isLocalRef(ref) {
|
|
1018
|
+
return typeof ref === 'string' && ref.startsWith('#');
|
|
1019
|
+
}
|
|
1020
|
+
function getLocalRefName(ref) {
|
|
1021
|
+
if (!isLocalRef(ref))
|
|
1022
|
+
return undefined;
|
|
1023
|
+
const refPath = ref.substring(2).split('/');
|
|
1024
|
+
return refPath.at(refPath.length - 1);
|
|
1025
|
+
}
|
|
1026
|
+
function resolveLocalRef(ref, spec) {
|
|
1027
|
+
const refPath = ref.substring(2).split('/');
|
|
1028
|
+
return refPath.reduce((integrator, current) => {
|
|
1029
|
+
return integrator ? integrator[current] : undefined;
|
|
1030
|
+
}, spec);
|
|
1031
|
+
}
|
|
1032
|
+
const replaceEscapedChars = [
|
|
1033
|
+
[new RegExp('\\\\ ', 'g'), ' '],
|
|
1034
|
+
[new RegExp('\\\\!', 'g'), '!'],
|
|
1035
|
+
[new RegExp('\\\\"', 'g'), '"'],
|
|
1036
|
+
[new RegExp('\\\\#', 'g'), '#'],
|
|
1037
|
+
[new RegExp('\\\\%', 'g'), '%'],
|
|
1038
|
+
[new RegExp('\\\\&', 'g'), '&'],
|
|
1039
|
+
[new RegExp("\\\\'", 'g'), "'"],
|
|
1040
|
+
[new RegExp('\\\\,', 'g'), ','],
|
|
1041
|
+
[new RegExp('\\\\-', 'g'), '-'],
|
|
1042
|
+
[new RegExp('\\\\:', 'g'), ':'],
|
|
1043
|
+
[new RegExp('\\\\;', 'g'), ';'],
|
|
1044
|
+
[new RegExp('\\\\<', 'g'), '<'],
|
|
1045
|
+
[new RegExp('\\\\=', 'g'), '='],
|
|
1046
|
+
[new RegExp('\\\\>', 'g'), '>'],
|
|
1047
|
+
[new RegExp('\\\\@', 'g'), '@'],
|
|
1048
|
+
[new RegExp('\\\\_', 'g'), '_'],
|
|
1049
|
+
[new RegExp('\\\\`', 'g'), '`'],
|
|
1050
|
+
[new RegExp('\\\\~', 'g'), '~'],
|
|
1051
|
+
[new RegExp('\\\\[zZ]', 'g'), '$'],
|
|
1052
|
+
];
|
|
1053
|
+
function safeCreateUnicodeRegExp(pattern) {
|
|
1054
|
+
for (const unEscape of replaceEscapedChars) {
|
|
1055
|
+
pattern = pattern.replace(unEscape[0], unEscape[1]);
|
|
1056
|
+
}
|
|
1057
|
+
return pattern;
|
|
1058
|
+
}
|
|
685
1059
|
|
|
686
1060
|
const TypesPushCommand = {
|
|
687
1061
|
command: "types:push",
|
|
@@ -712,9 +1086,18 @@ const TypesPushCommand = {
|
|
|
712
1086
|
(!baseTypes || baseTypes.length == 0 || baseTypes.includes(data.definition.baseType)) &&
|
|
713
1087
|
(!types || types.length == 0 || types.includes(data.definition.key));
|
|
714
1088
|
});
|
|
1089
|
+
// Create validator
|
|
1090
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Loading OpenAPI Specification for validation\n`));
|
|
1091
|
+
const typeSchema = (await loadSchema(client, 'ContentType')).at(0)?.schema;
|
|
1092
|
+
const typeValidator = await getValidator(typeSchema);
|
|
715
1093
|
// Output selected types
|
|
716
|
-
const results = await Promise.all(typeDefinitions.map(type => {
|
|
1094
|
+
const results = await Promise.all(typeDefinitions.map(async (type) => {
|
|
1095
|
+
if (!type.definition.key) {
|
|
1096
|
+
process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Content Type has no key in ${type.file}\n`));
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
717
1099
|
process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Pushing ${type.definition.displayName} from ${type.file}\n`));
|
|
1100
|
+
// Filter unwanted fields from the ContentType definition
|
|
718
1101
|
const outType = { ...type.definition };
|
|
719
1102
|
if (outType.source)
|
|
720
1103
|
delete outType.source;
|
|
@@ -724,13 +1107,31 @@ const TypesPushCommand = {
|
|
|
724
1107
|
delete outType.lastModified;
|
|
725
1108
|
if (outType.lastModifiedBy || outType.lastModifiedBy == "")
|
|
726
1109
|
delete outType.lastModifiedBy;
|
|
727
|
-
if (outType.features)
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
.
|
|
1110
|
+
//if (outType.features) delete outType.features
|
|
1111
|
+
//if (outType.usage) delete outType.usage
|
|
1112
|
+
const validationResult = typeValidator(outType);
|
|
1113
|
+
if (validationResult || force) {
|
|
1114
|
+
if (!validationResult)
|
|
1115
|
+
process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Ignoring validation errors in ${type.file}\n`));
|
|
1116
|
+
const currentContentType = (await client.contentTypesGet({ path: { key: type.definition.key } }).catch(() => null));
|
|
1117
|
+
function buildError(e) {
|
|
1118
|
+
return { key: type.definition.key, type: type.definition, file: type.file, error: e };
|
|
1119
|
+
}
|
|
1120
|
+
function buildData(ct) {
|
|
1121
|
+
return { key: type.definition.key, type: ct, file: type.file };
|
|
1122
|
+
}
|
|
1123
|
+
return (currentContentType ?
|
|
1124
|
+
client.contentTypesPatch({ path: { key: type.definition.key }, body: outType }) :
|
|
1125
|
+
client.contentTypesCreate({ body: outType })).then(buildData).catch(buildError);
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
return {
|
|
1129
|
+
key: type.definition.key,
|
|
1130
|
+
type: type.definition,
|
|
1131
|
+
file: type.file,
|
|
1132
|
+
error: typeValidator.errors.map(x => x.message).join(', ')
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
734
1135
|
}));
|
|
735
1136
|
const overview = new Table({
|
|
736
1137
|
head: [
|
|
@@ -740,255 +1141,32 @@ const TypesPushCommand = {
|
|
|
740
1141
|
chalk.yellow(chalk.bold("Message"))
|
|
741
1142
|
],
|
|
742
1143
|
colWidths: [31, 20, 9, 20],
|
|
743
|
-
colAligns: ["left", "left", "center", "left"]
|
|
744
|
-
});
|
|
745
|
-
results.forEach(item => {
|
|
746
|
-
overview.push([
|
|
747
|
-
item.type.displayName,
|
|
748
|
-
item.key,
|
|
749
|
-
item.error ? chalk.redBright(chalk.bold(figures.cross)) : chalk.green(chalk.bold(figures.tick)),
|
|
750
|
-
item.error ?? ''
|
|
751
|
-
]);
|
|
752
|
-
});
|
|
753
|
-
process.stdout.write(overview.toString() + "\n");
|
|
754
|
-
// Mark us as done
|
|
755
|
-
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
756
|
-
}
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
const builder = yargs => {
|
|
760
|
-
const updatedArgs = contentTypesBuilder(yargs);
|
|
761
|
-
updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
762
|
-
return updatedArgs;
|
|
763
|
-
};
|
|
764
|
-
function createTypeFolders(contentTypes, basePath, debug = false) {
|
|
765
|
-
const folders = contentTypes.map(contentType => {
|
|
766
|
-
const baseType = contentType.baseType ?? 'default';
|
|
767
|
-
// Create the type folder
|
|
768
|
-
const typePath = path.join(basePath, baseType, contentType.key);
|
|
769
|
-
if (!fs.existsSync(typePath)) {
|
|
770
|
-
fs.mkdirSync(typePath, { recursive: true });
|
|
771
|
-
if (debug)
|
|
772
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Created folder ${typePath} for Content-Type ${contentType.key}\n`));
|
|
773
|
-
}
|
|
774
|
-
// Check folders
|
|
775
|
-
if (!fs.statSync(typePath).isDirectory())
|
|
776
|
-
throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
|
|
777
|
-
return {
|
|
778
|
-
type: contentType.key,
|
|
779
|
-
path: typePath,
|
|
780
|
-
};
|
|
781
|
-
});
|
|
782
|
-
return folders;
|
|
783
|
-
}
|
|
784
|
-
function getTypeFolder(list, type) {
|
|
785
|
-
return list.filter(x => x.type == type).at(0)?.path;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
* Keep track of all generated properties
|
|
790
|
-
*/
|
|
791
|
-
let generatedProps = [];
|
|
792
|
-
const NextJsQueriesCommand = {
|
|
793
|
-
command: "nextjs:fragments",
|
|
794
|
-
describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
|
|
795
|
-
builder,
|
|
796
|
-
handler: async (args, opts) => {
|
|
797
|
-
generatedProps = [];
|
|
798
|
-
// Prepare
|
|
799
|
-
const { loadedContentTypes, createdTypeFolders } = opts || {};
|
|
800
|
-
const { components: basePath, _config: { debug }, force } = parseArgs(args);
|
|
801
|
-
const client = createCmsClient(args);
|
|
802
|
-
const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
|
|
803
|
-
// Start process
|
|
804
|
-
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments for ${contentTypes.map(x => x.key).join(', ')}\n`));
|
|
805
|
-
const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
|
|
806
|
-
const updatedTypes = contentTypes.map(contentType => {
|
|
807
|
-
const typePath = getTypeFolder(typeFolders, contentType.key);
|
|
808
|
-
return createGraphFragments(contentType, typePath, basePath, force, debug, allContentTypes, client.runtimeCmsVersion == OptiCmsVersion.CMS12);
|
|
809
|
-
}).filter(x => x).flat();
|
|
810
|
-
// Report outcome
|
|
811
|
-
if (updatedTypes.length > 0)
|
|
812
|
-
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
|
|
813
|
-
else
|
|
814
|
-
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
|
|
815
|
-
if (!opts)
|
|
816
|
-
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
817
|
-
generatedProps = [];
|
|
818
|
-
}
|
|
819
|
-
};
|
|
820
|
-
function createGraphFragments(contentType, typePath, basePath, force, debug, contentTypes, forCms12 = false) {
|
|
821
|
-
const returnValue = [];
|
|
822
|
-
const baseType = contentType.baseType ?? 'default';
|
|
823
|
-
const baseQueryFile = path.join(typePath, `${contentType.key}.${baseType}.graphql`);
|
|
824
|
-
if (fs.existsSync(baseQueryFile)) {
|
|
825
|
-
if (force) {
|
|
826
|
-
if (debug)
|
|
827
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
828
|
-
}
|
|
829
|
-
else {
|
|
830
|
-
if (debug)
|
|
831
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
|
|
832
|
-
return undefined;
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
else if (debug) {
|
|
836
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
837
|
-
}
|
|
838
|
-
const { fragment, propertyTypes } = createInitialFragment(contentType, false, undefined, forCms12);
|
|
839
|
-
fs.writeFileSync(baseQueryFile, fragment);
|
|
840
|
-
returnValue.push(contentType.key);
|
|
841
|
-
let dependencies = Array.isArray(propertyTypes) ? [...propertyTypes] : [];
|
|
842
|
-
while (Array.isArray(dependencies) && dependencies.length > 0) {
|
|
843
|
-
let newDependencies = [];
|
|
844
|
-
dependencies.forEach(dep => {
|
|
845
|
-
const propContentType = contentTypes.filter(x => x.key == dep[0])[0];
|
|
846
|
-
if (!propContentType) {
|
|
847
|
-
console.warn(`🟠 The content type ${dep[0]} has been referenced, but is not found in the Optimizely CMS instance`);
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
const fullTypeName = forCms12 ? contentType.key + propContentType.key : propContentType.key;
|
|
851
|
-
const propertyFragmentFile = path.join(basePath, propContentType.baseType, propContentType.key, `${fullTypeName}.property.graphql`);
|
|
852
|
-
const propertyFragmentDir = path.dirname(propertyFragmentFile);
|
|
853
|
-
if (!fs.existsSync(propertyFragmentDir))
|
|
854
|
-
fs.mkdirSync(propertyFragmentDir, { recursive: true });
|
|
855
|
-
if (!fs.existsSync(propertyFragmentFile) || force) {
|
|
856
|
-
if (debug)
|
|
857
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Writing ${propContentType.displayName} (${propContentType.key}) property fragment\n`));
|
|
858
|
-
const propContentTypeInfo = createInitialFragment(propContentType, true, contentType, forCms12);
|
|
859
|
-
fs.writeFileSync(propertyFragmentFile, propContentTypeInfo.fragment);
|
|
860
|
-
returnValue.push(propContentType.key);
|
|
861
|
-
if (Array.isArray(propContentTypeInfo.propertyTypes))
|
|
862
|
-
newDependencies.push(...propContentTypeInfo.propertyTypes);
|
|
863
|
-
}
|
|
864
|
-
});
|
|
865
|
-
dependencies = newDependencies;
|
|
866
|
-
}
|
|
867
|
-
return returnValue.length > 0 ? returnValue : undefined;
|
|
868
|
-
}
|
|
869
|
-
function createInitialFragment(contentType, forProperty = false, forBaseType, forCms12 = false) {
|
|
870
|
-
const propertyTypes = [];
|
|
871
|
-
const fragmentFields = [];
|
|
872
|
-
const typeProps = contentType.properties ?? {};
|
|
873
|
-
Object.getOwnPropertyNames(typeProps).forEach(propKey => {
|
|
874
|
-
// Exclude system properties, which are not present in Optimizely Graph
|
|
875
|
-
if (['experience', 'section'].includes(contentType.baseType) && ['AdditionalData', 'UnstructuredData', 'Layout'].includes(propKey))
|
|
876
|
-
return;
|
|
877
|
-
// Exclude CMS 12 System Properties
|
|
878
|
-
if (forCms12 && ['Categories'].includes(propKey))
|
|
879
|
-
return;
|
|
880
|
-
const propType = typeProps[propKey].type;
|
|
881
|
-
const isConflict = generatedProps.some(x => x.propName == propKey && x.propType != propType);
|
|
882
|
-
const propName = isConflict ? `${contentType.key}${propKey}: ${propKey}` : propKey;
|
|
883
|
-
// Write the property
|
|
884
|
-
switch (propType) {
|
|
885
|
-
case IntegrationApi.PropertyDataType.ARRAY:
|
|
886
|
-
{
|
|
887
|
-
const typeData = typeProps[propKey];
|
|
888
|
-
switch (typeData.items.type) {
|
|
889
|
-
case IntegrationApi.PropertyDataType.INTEGER:
|
|
890
|
-
if (typeData.format == 'categorization')
|
|
891
|
-
fragmentFields.push(`${propName} { Id, Name, Description }`);
|
|
892
|
-
else
|
|
893
|
-
fragmentFields.push(propName);
|
|
894
|
-
break;
|
|
895
|
-
case IntegrationApi.PropertyDataType.STRING:
|
|
896
|
-
fragmentFields.push(propName);
|
|
897
|
-
break;
|
|
898
|
-
case IntegrationApi.PropertyDataType.CONTENT:
|
|
899
|
-
if (contentType.baseType == 'page' || contentType.baseType == 'experience')
|
|
900
|
-
fragmentFields.push(`${propName} { ...${forCms12 ? 'PageIContentListItem' : 'BlockData'} }`);
|
|
901
|
-
else
|
|
902
|
-
fragmentFields.push(`${propName} { ...IContentListItem }`);
|
|
903
|
-
break;
|
|
904
|
-
case IntegrationApi.PropertyDataType.COMPONENT:
|
|
905
|
-
const componentType = typeData.items.contentType;
|
|
906
|
-
switch (componentType) {
|
|
907
|
-
case 'link':
|
|
908
|
-
fragmentFields.push(`${propName} { ...LinkItemData }`);
|
|
909
|
-
break;
|
|
910
|
-
default:
|
|
911
|
-
{
|
|
912
|
-
const componentFragmentName = forCms12 ? contentType.key + componentType : componentType + 'Property';
|
|
913
|
-
fragmentFields.push(`${propName} { ...${componentFragmentName}Data }`);
|
|
914
|
-
propertyTypes.push([componentType, true]);
|
|
915
|
-
break;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
break;
|
|
919
|
-
case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
|
|
920
|
-
fragmentFields.push(`${propName} { ...ReferenceData }`);
|
|
921
|
-
break;
|
|
922
|
-
default:
|
|
923
|
-
fragmentFields.push(`${propName} { __typename }`);
|
|
924
|
-
break;
|
|
925
|
-
}
|
|
926
|
-
break;
|
|
927
|
-
}
|
|
928
|
-
case IntegrationApi.PropertyDataType.STRING: {
|
|
929
|
-
const propDetails = typeProps[propKey];
|
|
930
|
-
switch (propDetails.format ?? "") {
|
|
931
|
-
case 'html':
|
|
932
|
-
fragmentFields.push(forCms12 ? `${propName} { Structure, Html }` : `${propName} { json, html }`);
|
|
933
|
-
break;
|
|
934
|
-
case 'shortString':
|
|
935
|
-
case 'selectOne':
|
|
936
|
-
case '':
|
|
937
|
-
fragmentFields.push(propName);
|
|
938
|
-
break;
|
|
939
|
-
default:
|
|
940
|
-
if (!isConflict)
|
|
941
|
-
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`));
|
|
942
|
-
break;
|
|
943
|
-
}
|
|
944
|
-
break;
|
|
945
|
-
}
|
|
946
|
-
case IntegrationApi.PropertyDataType.URL:
|
|
947
|
-
fragmentFields.push(forCms12 ? propName : `${propName} { ...LinkData }`);
|
|
948
|
-
break;
|
|
949
|
-
case IntegrationApi.PropertyDataType.CONTENT_REFERENCE:
|
|
950
|
-
fragmentFields.push(`${propName} { ...ReferenceData }`);
|
|
951
|
-
break;
|
|
952
|
-
case IntegrationApi.PropertyDataType.COMPONENT: {
|
|
953
|
-
const componentType = typeProps[propKey].contentType;
|
|
954
|
-
if (forCms12 && componentType == "link") {
|
|
955
|
-
fragmentFields.push(`${propName} { ...LinkItemData }`);
|
|
956
|
-
}
|
|
957
|
-
else {
|
|
958
|
-
const componentFragmentName = forCms12 ? contentType.key + componentType : componentType + 'Property';
|
|
959
|
-
fragmentFields.push(`${propName} { ...${componentFragmentName}Data }`);
|
|
960
|
-
propertyTypes.push([componentType, true]);
|
|
961
|
-
}
|
|
962
|
-
break;
|
|
963
|
-
}
|
|
964
|
-
case IntegrationApi.PropertyDataType.BINARY:
|
|
965
|
-
fragmentFields.push(`${propName} { ...BinaryData }`);
|
|
966
|
-
break;
|
|
967
|
-
default:
|
|
968
|
-
fragmentFields.push(propName);
|
|
969
|
-
break;
|
|
970
|
-
}
|
|
971
|
-
generatedProps.push({
|
|
972
|
-
propType: typeProps[propKey].type,
|
|
973
|
-
propName: propKey
|
|
1144
|
+
colAligns: ["left", "left", "center", "left"]
|
|
974
1145
|
});
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1146
|
+
results.forEach(item => {
|
|
1147
|
+
overview.push([
|
|
1148
|
+
item.type.displayName,
|
|
1149
|
+
item.key,
|
|
1150
|
+
item.error ? chalk.redBright(chalk.bold(figures.cross)) : chalk.green(chalk.bold(figures.tick)),
|
|
1151
|
+
item.error ?? ''
|
|
1152
|
+
]);
|
|
1153
|
+
});
|
|
1154
|
+
process.stdout.write(overview.toString() + "\n");
|
|
1155
|
+
// Mark us as done
|
|
1156
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
983
1157
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
const builder = yargs => {
|
|
1161
|
+
const updatedArgs = contentTypesBuilder(yargs);
|
|
1162
|
+
updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
1163
|
+
return updatedArgs;
|
|
1164
|
+
};
|
|
1165
|
+
function createTypeFolders(contentTypes, basePath, debug = false) {
|
|
1166
|
+
return contentTypes.map(contentType => getContentTypePaths(contentType, basePath, true, debug));
|
|
1167
|
+
}
|
|
1168
|
+
function getTypeFolder(list, type) {
|
|
1169
|
+
return list.filter(x => x.type == type).at(0);
|
|
992
1170
|
}
|
|
993
1171
|
|
|
994
1172
|
function ucFirst(current) {
|
|
@@ -1024,8 +1202,8 @@ const NextJsComponentsCommand = {
|
|
|
1024
1202
|
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
1025
1203
|
}
|
|
1026
1204
|
};
|
|
1027
|
-
function createComponent(contentType,
|
|
1028
|
-
const componentFile
|
|
1205
|
+
function createComponent(contentType, typePathInfo, force, debug = false) {
|
|
1206
|
+
const { componentFile, path: typePath } = typePathInfo;
|
|
1029
1207
|
if (fs.existsSync(componentFile)) {
|
|
1030
1208
|
if (!force) {
|
|
1031
1209
|
if (debug)
|
|
@@ -1040,8 +1218,9 @@ function createComponent(contentType, typePath, force, debug = false) {
|
|
|
1040
1218
|
// Get type information & short-hands
|
|
1041
1219
|
const displayTemplate = getDisplayTemplateInfo$1(contentType, typePath);
|
|
1042
1220
|
const baseDisplayTemplate = getBaseTypeTemplateInfo(contentType, typePath);
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
1221
|
+
const normalizedBaseType = normalizeBaseType(contentType.baseType ?? 'part');
|
|
1222
|
+
const varName = `${contentType.key}${ucFirst(normalizedBaseType)}`;
|
|
1223
|
+
const tplFn = Templates[normalizedBaseType] ?? Templates['default'];
|
|
1045
1224
|
if (!tplFn) {
|
|
1046
1225
|
if (debug)
|
|
1047
1226
|
process.stdout.write(chalk.redBright(`${figures.cross} Skipping ${contentType.displayName} (${contentType.key}) component - no template for ${contentType.baseType} found\n`));
|
|
@@ -1065,6 +1244,11 @@ function getBaseTypeTemplateInfo(contentType, typePath) {
|
|
|
1065
1244
|
}
|
|
1066
1245
|
return undefined;
|
|
1067
1246
|
}
|
|
1247
|
+
function normalizeBaseType(baseType) {
|
|
1248
|
+
if (baseType.startsWith('_'))
|
|
1249
|
+
return baseType.substring(1);
|
|
1250
|
+
return baseType;
|
|
1251
|
+
}
|
|
1068
1252
|
function getDisplayTemplateInfo$1(contentType, typePath) {
|
|
1069
1253
|
const displayTemplatesFile = path.join(typePath, 'displayTemplates.ts');
|
|
1070
1254
|
if (fs.existsSync(displayTemplatesFile)) {
|
|
@@ -1078,24 +1262,50 @@ function getDisplayTemplateInfo$1(contentType, typePath) {
|
|
|
1078
1262
|
}
|
|
1079
1263
|
const Templates = {
|
|
1080
1264
|
// Default Template for all components without specifics
|
|
1081
|
-
default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { type CmsComponent } from "@remkoj/optimizely-cms-react";
|
|
1265
|
+
default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
|
|
1082
1266
|
import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
|
|
1083
1267
|
import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
|
|
1084
1268
|
import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
|
|
1085
1269
|
|
|
1086
1270
|
/**
|
|
1087
1271
|
* ${contentType.displayName}
|
|
1272
|
+
* ---
|
|
1088
1273
|
* ${contentType.description}
|
|
1089
1274
|
*/
|
|
1090
|
-
export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''},
|
|
1275
|
+
export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps }) => {
|
|
1091
1276
|
const componentName = '${contentType.displayName}'
|
|
1092
1277
|
const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
|
|
1093
|
-
return <
|
|
1278
|
+
return <CmsEditable className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4" {...editProps}>
|
|
1094
1279
|
<div className="font-bold italic">{ componentName }</div>
|
|
1095
1280
|
<div>{ componentInfo }</div>
|
|
1096
1281
|
{ 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> }
|
|
1097
|
-
|
|
1098
|
-
|
|
1282
|
+
</CmsEditable>
|
|
1283
|
+
}
|
|
1284
|
+
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
1285
|
+
${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
|
|
1286
|
+
|
|
1287
|
+
export default ${varName}`,
|
|
1288
|
+
// Default Template for all section types
|
|
1289
|
+
section: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
|
|
1290
|
+
import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
|
|
1291
|
+
import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
|
|
1292
|
+
import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* ${contentType.displayName}
|
|
1296
|
+
* ---
|
|
1297
|
+
* ${contentType.description}
|
|
1298
|
+
*/
|
|
1299
|
+
export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps, children }) => {
|
|
1300
|
+
const componentName = '${contentType.displayName}'
|
|
1301
|
+
const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
|
|
1302
|
+
return <CmsEditable className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4" {...editProps}>
|
|
1303
|
+
<div className="font-bold italic">{ componentName }</div>
|
|
1304
|
+
<div>{ componentInfo }</div>
|
|
1305
|
+
{ 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> }
|
|
1306
|
+
${(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 */}'}
|
|
1307
|
+
<div>{ children }</div>
|
|
1308
|
+
</CmsEditable>
|
|
1099
1309
|
}
|
|
1100
1310
|
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
1101
1311
|
${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
|
|
@@ -1103,26 +1313,32 @@ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}
|
|
|
1103
1313
|
export default ${varName}`,
|
|
1104
1314
|
// Template for all page component types
|
|
1105
1315
|
page: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
|
|
1106
|
-
import { ${contentType.key}
|
|
1316
|
+
import { get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'${displayTemplate ? `
|
|
1107
1317
|
import { ${displayTemplate} } from "./displayTemplates";` : ''}
|
|
1108
1318
|
import { getSdk } from "@/gql"
|
|
1109
1319
|
|
|
1110
1320
|
/**
|
|
1111
1321
|
* ${contentType.displayName}
|
|
1322
|
+
* ---
|
|
1112
1323
|
* ${contentType.description}
|
|
1324
|
+
*
|
|
1325
|
+
* This component uses the content query that is auto-generated with the Optimizely CMS Preset for GraphQL Codegen, if you need
|
|
1326
|
+
* to override this query create a GraphQL file (for example: \`get${contentType.key}Data.query.graphql\`) in the same folder as
|
|
1327
|
+
* this file. This file must include a GraphQL query with the name \`get${contentType.key}Data\`.
|
|
1328
|
+
*
|
|
1329
|
+
* [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
|
|
1113
1330
|
*/
|
|
1114
|
-
export const ${varName} : CmsComponent
|
|
1331
|
+
export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''} }) => {
|
|
1115
1332
|
const componentName = '${contentType.displayName}'
|
|
1116
1333
|
const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
|
|
1117
1334
|
return <div className="mx-auto px-2 container">
|
|
1118
1335
|
<div className="font-bold italic">{ componentName }</div>
|
|
1119
1336
|
<div>{ componentInfo }</div>
|
|
1120
1337
|
{ 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> }
|
|
1121
|
-
{ children && <div className="flex flex-col mt-4 mx-4">{ children }</div>}
|
|
1122
1338
|
</div>
|
|
1123
1339
|
}
|
|
1124
1340
|
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
1125
|
-
${varName}.
|
|
1341
|
+
${varName}.getDataQuery = () => get${contentType.key}DataDocument
|
|
1126
1342
|
${varName}.getMetaData = async (contentLink, locale, client) => {
|
|
1127
1343
|
const sdk = getSdk(client);
|
|
1128
1344
|
// Add your metadata logic here
|
|
@@ -1132,24 +1348,36 @@ ${varName}.getMetaData = async (contentLink, locale, client) => {
|
|
|
1132
1348
|
export default ${varName}`,
|
|
1133
1349
|
// Template for all experience component types
|
|
1134
1350
|
experience: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
|
|
1135
|
-
import {
|
|
1136
|
-
import {
|
|
1351
|
+
import { ExperienceDataFragmentDoc, get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'
|
|
1352
|
+
import { getFragmentData } from '@/gql/fragment-masking'
|
|
1137
1353
|
import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
|
|
1138
1354
|
import { ${displayTemplate} } from "./displayTemplates";` : ''}
|
|
1139
|
-
import { getSdk } from "@/gql"
|
|
1355
|
+
import { getSdk } from "@/gql/client"
|
|
1140
1356
|
|
|
1141
1357
|
/**
|
|
1142
1358
|
* ${contentType.displayName}
|
|
1359
|
+
* ---
|
|
1143
1360
|
* ${contentType.description}
|
|
1361
|
+
*
|
|
1362
|
+
* This component uses the content query that is auto-generated with the Optimizely CMS Preset for GraphQL Codegen, if you need
|
|
1363
|
+
* to override this query create the file \`${contentType.key}.query.graphql\` in the same folder as this file. This file then
|
|
1364
|
+
* must include a GraphQL query with the name \`get${contentType.key} Data\`.
|
|
1365
|
+
*
|
|
1366
|
+
* [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
|
|
1144
1367
|
*/
|
|
1145
|
-
export const ${varName} : CmsComponent
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1368
|
+
export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, ctx }) => {
|
|
1369
|
+
if (ctx) ctx.editableContentIsExperience = true
|
|
1370
|
+
const composition = getFragmentData(ExperienceDataFragmentDoc, data).composition
|
|
1371
|
+
const componentName = '${contentType.displayName}'
|
|
1372
|
+
const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
|
|
1373
|
+
return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData" ctx={ctx}>
|
|
1374
|
+
<div className="font-bold italic">{ componentName }</div>
|
|
1375
|
+
<div>{ componentInfo }</div>
|
|
1376
|
+
{ composition && isNode(composition) && <OptimizelyComposition node={composition} ctx={ctx} /> }
|
|
1377
|
+
</CmsEditable>
|
|
1150
1378
|
}
|
|
1151
1379
|
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
1152
|
-
${varName}.
|
|
1380
|
+
${varName}.getDataQuery = () => get${contentType.key}DataDocument
|
|
1153
1381
|
${varName}.getMetaData = async (contentLink, locale, client) => {
|
|
1154
1382
|
const sdk = getSdk(client);
|
|
1155
1383
|
// Add your metadata logic here
|
|
@@ -1167,7 +1395,7 @@ const NextJsVisualBuilderCommand = {
|
|
|
1167
1395
|
// Prepare
|
|
1168
1396
|
const { components: basePath, _config: { debug }, force } = parseArgs(args);
|
|
1169
1397
|
// Create the fall-back node and style defintions
|
|
1170
|
-
await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node'], definitions: true });
|
|
1398
|
+
await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'], definitions: true });
|
|
1171
1399
|
createGenericNode(basePath, force, debug);
|
|
1172
1400
|
// Get all styles
|
|
1173
1401
|
const { styles } = await getStyles(createCmsClient(args), { ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'] });
|
|
@@ -1178,7 +1406,8 @@ const NextJsVisualBuilderCommand = {
|
|
|
1178
1406
|
});
|
|
1179
1407
|
// Process base styles
|
|
1180
1408
|
styles.filter(x => typeof (x.baseType) == 'string' && x.baseType.length > 0).map(styleDefinition => {
|
|
1181
|
-
const
|
|
1409
|
+
const baseType = (styleDefinition.baseType ?? '').startsWith('_') ? (styleDefinition.baseType ?? '').substring(1) : styleDefinition.baseType;
|
|
1410
|
+
const templatePath = path.join(basePath, baseType, 'styles', styleDefinition.key);
|
|
1182
1411
|
createSpecificNode(styleDefinition, templatePath, force, debug);
|
|
1183
1412
|
});
|
|
1184
1413
|
}
|
|
@@ -1198,13 +1427,30 @@ function createSpecificNode(template, templatePath, force = false, debug = false
|
|
|
1198
1427
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${template.displayName} (${template.key}) component\n`));
|
|
1199
1428
|
if (!fs.existsSync(templatePath))
|
|
1200
1429
|
fs.mkdirSync(templatePath, { recursive: true });
|
|
1430
|
+
const baseType = template.nodeType ?? template.baseType ?? 'unknown';
|
|
1201
1431
|
const displayTemplateName = getDisplayTemplateInfo(template, templatePath);
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1432
|
+
const imports = ['import { extractSettings, type CmsLayoutComponent } from "@remkoj/optimizely-cms-react/rsc";'];
|
|
1433
|
+
const componentProps = [`className="vb:${baseType} vb:${baseType}:${template.key}"`];
|
|
1434
|
+
const componentArgs = ['layoutProps', 'children'];
|
|
1435
|
+
let componentType = 'div';
|
|
1436
|
+
if (displayTemplateName)
|
|
1437
|
+
imports.push(`import { ${displayTemplateName} } from "../displayTemplates";`);
|
|
1438
|
+
if (baseType.toLowerCase() == 'section') {
|
|
1439
|
+
imports.push('import { CmsEditable } from "@remkoj/optimizely-cms-react/rsc";');
|
|
1440
|
+
componentType = 'CmsEditable';
|
|
1441
|
+
componentProps.push('{ ...editProps }');
|
|
1442
|
+
componentArgs.push('editProps');
|
|
1443
|
+
}
|
|
1444
|
+
let style = '';
|
|
1445
|
+
if (baseType == "row")
|
|
1446
|
+
style = ` style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "stretch" }}`;
|
|
1447
|
+
if (baseType == "column")
|
|
1448
|
+
style = ` style={{ display: "flex", flexDirection: "column", flexGrow: 1 }}`;
|
|
1449
|
+
const component = `${imports.join("\n")}
|
|
1204
1450
|
|
|
1205
|
-
export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({
|
|
1451
|
+
export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ ${componentArgs.join(', ')} }) => {
|
|
1206
1452
|
const layout = extractSettings(layoutProps);
|
|
1207
|
-
return (
|
|
1453
|
+
return (<${componentType} ${componentProps.join(' ')}${style}>{ children }</${componentType}>);
|
|
1208
1454
|
}
|
|
1209
1455
|
|
|
1210
1456
|
export default ${template.key};`;
|
|
@@ -1224,13 +1470,19 @@ function createGenericNode(basePath, force, debug) {
|
|
|
1224
1470
|
else if (debug)
|
|
1225
1471
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating generic node component\n`));
|
|
1226
1472
|
const nodeContent = `import { CmsEditable, type CmsLayoutComponent } from '@remkoj/optimizely-cms-react/rsc'
|
|
1473
|
+
import { type CSSProperties } from 'react';
|
|
1227
1474
|
|
|
1228
|
-
export const VisualBuilderNode : CmsLayoutComponent = ({
|
|
1475
|
+
export const VisualBuilderNode : CmsLayoutComponent = ({ editProps, layoutProps, children }) =>
|
|
1229
1476
|
{
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1477
|
+
let className = \`vb:\${layoutProps?.layoutType}\`;
|
|
1478
|
+
let style : CSSProperties | undefined = undefined;
|
|
1479
|
+
if (layoutProps?.layoutType == "row")
|
|
1480
|
+
style = {display: "flex", flexDirection: "row"}
|
|
1481
|
+
if (layoutProps?.layoutType == "column")
|
|
1482
|
+
style = {display: "flex", flexDirection: "column"}
|
|
1483
|
+
if (layoutProps && layoutProps.layoutType == "section")
|
|
1484
|
+
return <CmsEditable as="div" className={ className } {...editProps}>{ children }</CmsEditable>
|
|
1485
|
+
return <div className={ className } style={ style }>{ children }</div>
|
|
1234
1486
|
}
|
|
1235
1487
|
|
|
1236
1488
|
export default VisualBuilderNode`;
|
|
@@ -1478,7 +1730,7 @@ const NextJsCreateCommand = {
|
|
|
1478
1730
|
await Promise.all([
|
|
1479
1731
|
TypesPullCommand.handler(args),
|
|
1480
1732
|
StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: [], definitions: true }),
|
|
1481
|
-
|
|
1733
|
+
//createFragments.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
|
|
1482
1734
|
]);
|
|
1483
1735
|
process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared types, styles and fragments") + "\n");
|
|
1484
1736
|
// Then create the components
|
|
@@ -1493,6 +1745,484 @@ const NextJsCreateCommand = {
|
|
|
1493
1745
|
}
|
|
1494
1746
|
};
|
|
1495
1747
|
|
|
1748
|
+
const NextJsFragmentsCommand = {
|
|
1749
|
+
command: "nextjs:fragments",
|
|
1750
|
+
describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
|
|
1751
|
+
builder,
|
|
1752
|
+
handler: async (args, opts) => {
|
|
1753
|
+
// Prepare
|
|
1754
|
+
const { loadedContentTypes, createdTypeFolders } = opts || {};
|
|
1755
|
+
const { components: basePath, _config: { debug }, force } = parseArgs(args);
|
|
1756
|
+
const client = createCmsClient(args);
|
|
1757
|
+
const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
|
|
1758
|
+
// Start process
|
|
1759
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments\n`));
|
|
1760
|
+
const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
|
|
1761
|
+
const tracker = new Map();
|
|
1762
|
+
const dependencies = [];
|
|
1763
|
+
const updatedTypes = contentTypes.map(contentType => {
|
|
1764
|
+
const { written, propertyTypes } = createComponentFragments(contentType, getTypeFolder(typeFolders, contentType.key), force, debug, tracker);
|
|
1765
|
+
dependencies.push(...propertyTypes);
|
|
1766
|
+
return written ? contentType.key : undefined;
|
|
1767
|
+
}).filter(x => x);
|
|
1768
|
+
// Report outcome
|
|
1769
|
+
if (updatedTypes.length > 0)
|
|
1770
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
|
|
1771
|
+
else
|
|
1772
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
|
|
1773
|
+
// Start property generation process
|
|
1774
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL property fragments\n`));
|
|
1775
|
+
const generatedProps = createPropertyFragments(dependencies, allContentTypes, (contentType) => {
|
|
1776
|
+
if (!contentType.key)
|
|
1777
|
+
return undefined;
|
|
1778
|
+
let tf = getTypeFolder(typeFolders, contentType.key);
|
|
1779
|
+
if (!tf) {
|
|
1780
|
+
tf = getContentTypePaths(contentType, basePath, true, debug);
|
|
1781
|
+
typeFolders.push(tf);
|
|
1782
|
+
}
|
|
1783
|
+
return tf;
|
|
1784
|
+
}, force, debug);
|
|
1785
|
+
// Report outcome
|
|
1786
|
+
if (generatedProps.length > 0)
|
|
1787
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
|
|
1788
|
+
else
|
|
1789
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL property fragments created/updated\n`));
|
|
1790
|
+
if (!opts)
|
|
1791
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
function createComponentFragments(contentType, typePath, force, debug, propertyTracker = new Map()) {
|
|
1795
|
+
const baseQueryFile = typePath.fragmentFile;
|
|
1796
|
+
let writeFragment = true;
|
|
1797
|
+
if (fs.existsSync(baseQueryFile)) {
|
|
1798
|
+
if (force) {
|
|
1799
|
+
if (debug)
|
|
1800
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
1801
|
+
}
|
|
1802
|
+
else {
|
|
1803
|
+
if (debug)
|
|
1804
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
|
|
1805
|
+
writeFragment = false;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
else if (debug) {
|
|
1809
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
1810
|
+
}
|
|
1811
|
+
let written = false;
|
|
1812
|
+
if (writeFragment) {
|
|
1813
|
+
const fragment = GraphQLGen.buildFragment(contentType, undefined, false, propertyTracker);
|
|
1814
|
+
fs.writeFileSync(baseQueryFile, fragment);
|
|
1815
|
+
written = true;
|
|
1816
|
+
}
|
|
1817
|
+
return { written, propertyTypes: GraphQLGen.getReferencedPropertyComponents(contentType) };
|
|
1818
|
+
}
|
|
1819
|
+
function createPropertyFragments(propertyTypesList, allContentTypes, selectTypeFolder, force = false, debug = false) {
|
|
1820
|
+
const returnValue = [];
|
|
1821
|
+
for (const propertyContentTypeKey of propertyTypesList.filter((v, i, a) => a.indexOf(v, i + 1) <= i)) {
|
|
1822
|
+
// Load the ContentType definition
|
|
1823
|
+
const contentType = allContentTypes.filter(x => x.key == propertyContentTypeKey).at(0);
|
|
1824
|
+
if (!contentType) {
|
|
1825
|
+
console.warn(`🟠 The content type ${propertyContentTypeKey} has been referenced, but is not found in the Optimizely CMS instance`);
|
|
1826
|
+
continue;
|
|
1827
|
+
}
|
|
1828
|
+
// Load the paths for this ContentType
|
|
1829
|
+
const depFolder = selectTypeFolder(contentType);
|
|
1830
|
+
if (!depFolder) {
|
|
1831
|
+
console.warn(`🟠 The content type ${propertyContentTypeKey} cannot be stored in the project`);
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
const propertyFragmentFile = depFolder.propertyFragmentFile;
|
|
1835
|
+
let mustWrite = true;
|
|
1836
|
+
if (fs.existsSync(propertyFragmentFile)) {
|
|
1837
|
+
if (force) {
|
|
1838
|
+
if (debug)
|
|
1839
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) property fragment\n`));
|
|
1840
|
+
}
|
|
1841
|
+
else {
|
|
1842
|
+
if (debug)
|
|
1843
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) property fragment - file already exists\n`));
|
|
1844
|
+
mustWrite = false;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
else if (debug) {
|
|
1848
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) property fragment\n`));
|
|
1849
|
+
}
|
|
1850
|
+
if (mustWrite) {
|
|
1851
|
+
const fragment = GraphQLGen.buildFragment(contentType, undefined, true);
|
|
1852
|
+
fs.writeFileSync(propertyFragmentFile, fragment);
|
|
1853
|
+
returnValue.push(contentType.key);
|
|
1854
|
+
}
|
|
1855
|
+
// Recurse down for properties that we're not yet rendering
|
|
1856
|
+
const referencedPropertyTypes = GraphQLGen.getReferencedPropertyComponents(contentType).filter(x => !propertyTypesList.includes(x));
|
|
1857
|
+
if (referencedPropertyTypes.length > 0) {
|
|
1858
|
+
if (debug)
|
|
1859
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Component property ${propertyContentTypeKey} uses components a property, recursing down\n`));
|
|
1860
|
+
const additionalProperties = createPropertyFragments(referencedPropertyTypes, allContentTypes, selectTypeFolder, force, debug);
|
|
1861
|
+
returnValue.push(...additionalProperties);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
return returnValue;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
const NextJsQueriesCommand = {
|
|
1868
|
+
command: "nextjs:queries",
|
|
1869
|
+
describe: "Create the GrapQL Queries to use two queries to load content",
|
|
1870
|
+
builder: yargs => {
|
|
1871
|
+
const updatedArgs = contentTypesBuilder(yargs, { ...ContentTypesArgsDefaults, baseTypes: ['page', 'experience'] });
|
|
1872
|
+
updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
1873
|
+
return updatedArgs;
|
|
1874
|
+
},
|
|
1875
|
+
handler: async (args, opts) => {
|
|
1876
|
+
// Prepare
|
|
1877
|
+
const { loadedContentTypes, createdTypeFolders } = opts || {};
|
|
1878
|
+
const { components: basePath, _config: { debug }, force } = parseArgs(args);
|
|
1879
|
+
const client = createCmsClient(args);
|
|
1880
|
+
const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
|
|
1881
|
+
// Start process
|
|
1882
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL queries\n`));
|
|
1883
|
+
const dependencies = [];
|
|
1884
|
+
const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
|
|
1885
|
+
const updatedTypes = contentTypes.map(contentType => {
|
|
1886
|
+
const typePath = getTypeFolder(typeFolders, contentType.key);
|
|
1887
|
+
const { written, propertyTypes } = createGraphQueries(contentType, typePath, force, debug);
|
|
1888
|
+
dependencies.push(...propertyTypes);
|
|
1889
|
+
return written ? contentType.key : undefined;
|
|
1890
|
+
}).filter(x => x).flat();
|
|
1891
|
+
// Report outcome
|
|
1892
|
+
if (updatedTypes.length > 0)
|
|
1893
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL queries for ${updatedTypes.join(', ')}\n`));
|
|
1894
|
+
else
|
|
1895
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL queries created/updated\n`));
|
|
1896
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL property fragments\n`));
|
|
1897
|
+
const generatedProps = createPropertyFragments(dependencies, allContentTypes, (contentType) => {
|
|
1898
|
+
if (!contentType.key)
|
|
1899
|
+
return undefined;
|
|
1900
|
+
let tf = getTypeFolder(typeFolders, contentType.key);
|
|
1901
|
+
if (!tf) {
|
|
1902
|
+
tf = getContentTypePaths(contentType, basePath, true, debug);
|
|
1903
|
+
typeFolders.push(tf);
|
|
1904
|
+
}
|
|
1905
|
+
return tf;
|
|
1906
|
+
}, force, debug);
|
|
1907
|
+
// Report outcome
|
|
1908
|
+
if (generatedProps.length > 0)
|
|
1909
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
|
|
1910
|
+
else
|
|
1911
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL property fragments created/updated\n`));
|
|
1912
|
+
if (!opts)
|
|
1913
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
1914
|
+
}
|
|
1915
|
+
};
|
|
1916
|
+
function createGraphQueries(contentType, typePath, force, debug) {
|
|
1917
|
+
const baseQueryFile = typePath.queryFile;
|
|
1918
|
+
let mustWrite = true;
|
|
1919
|
+
if (fs.existsSync(baseQueryFile)) {
|
|
1920
|
+
if (force) {
|
|
1921
|
+
if (debug)
|
|
1922
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
1923
|
+
}
|
|
1924
|
+
else {
|
|
1925
|
+
if (debug)
|
|
1926
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
|
|
1927
|
+
mustWrite = false;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
else if (debug) {
|
|
1931
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
1932
|
+
}
|
|
1933
|
+
if (mustWrite) {
|
|
1934
|
+
const fragment = GraphQLGen.buildGetQuery(contentType, undefined, new Map());
|
|
1935
|
+
fs.writeFileSync(baseQueryFile, fragment);
|
|
1936
|
+
}
|
|
1937
|
+
return {
|
|
1938
|
+
written: mustWrite,
|
|
1939
|
+
propertyTypes: GraphQLGen.getReferencedPropertyComponents(contentType)
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
const SchemaDownloadCommand = {
|
|
1944
|
+
command: "schema:download",
|
|
1945
|
+
describe: "Create JSON schema files for the specified types, which you can use to configure JSON validation in your IDE of choice.",
|
|
1946
|
+
builder: (yargs) => {
|
|
1947
|
+
const newYargs = yargs;
|
|
1948
|
+
newYargs.option('schemaDir', { alias: 'd', description: "The schema path relative to your project root directory", string: true, type: 'string', demandOption: false, default: './.schema' });
|
|
1949
|
+
newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] });
|
|
1950
|
+
newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
1951
|
+
return newYargs;
|
|
1952
|
+
},
|
|
1953
|
+
async handler(args, opts) {
|
|
1954
|
+
const { _config: cmsConfig, path: projectPath, force, schemaDir, schemas } = parseArgs(args);
|
|
1955
|
+
const client = createCmsClient(cmsConfig);
|
|
1956
|
+
// Loading Schema
|
|
1957
|
+
const schemaDefs = await loadSchema(client, schemas);
|
|
1958
|
+
const fullSchemaDir = path.normalize(path.join(projectPath, schemaDir));
|
|
1959
|
+
const relativeSchemaDir = path.relative(projectPath, fullSchemaDir);
|
|
1960
|
+
process.stdout.write(`\n${figures.arrowRight} Validating schema folder ${relativeSchemaDir}\n`);
|
|
1961
|
+
const schemaDirInfo = await fsAsync.stat(fullSchemaDir).catch((e) => { if (e.code == 'ENOENT')
|
|
1962
|
+
return undefined;
|
|
1963
|
+
else
|
|
1964
|
+
throw e; });
|
|
1965
|
+
if (!schemaDirInfo) {
|
|
1966
|
+
await fsAsync.mkdir(fullSchemaDir, { recursive: true });
|
|
1967
|
+
}
|
|
1968
|
+
else if (!schemaDirInfo.isDirectory()) {
|
|
1969
|
+
process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] The schema directory exists, but is not a directory (${relativeSchemaDir})\n`)));
|
|
1970
|
+
process.exit(1);
|
|
1971
|
+
}
|
|
1972
|
+
process.stdout.write(chalk.greenBright(` ${figures.tick} `));
|
|
1973
|
+
process.stdout.write(`Schema directory exists\n`);
|
|
1974
|
+
process.stdout.write(`\n${figures.arrowRight} Writing schema files\n`);
|
|
1975
|
+
for (const schema of schemaDefs) {
|
|
1976
|
+
const schemaFile = path.normalize(path.join(fullSchemaDir, `${schema.title.toLowerCase()}.schema.json`));
|
|
1977
|
+
const relativePath = path.relative(projectPath, schemaFile);
|
|
1978
|
+
try {
|
|
1979
|
+
await fsAsync.writeFile(schemaFile, JSON.stringify(schema.schema, undefined, 2), {
|
|
1980
|
+
encoding: "utf-8",
|
|
1981
|
+
flag: force ? 'w' : 'wx'
|
|
1982
|
+
});
|
|
1983
|
+
process.stdout.write(chalk.greenBright(` ${figures.tick} `));
|
|
1984
|
+
process.stdout.write(`Written the ${schema.title} schema to ${relativePath}\n`);
|
|
1985
|
+
}
|
|
1986
|
+
catch (err) {
|
|
1987
|
+
if (!force && err.code === 'EEXIST') {
|
|
1988
|
+
process.stdout.write(chalk.yellowBright(` ${figures.info} `));
|
|
1989
|
+
process.stdout.write(`The schema ${schema.title} already exists in ${relativePath}, run with -f to overwrite\n`);
|
|
1990
|
+
}
|
|
1991
|
+
else
|
|
1992
|
+
process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] ${err.toString()}\n`)));
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
|
|
1998
|
+
const SchemaListCommand = {
|
|
1999
|
+
command: "schema:list",
|
|
2000
|
+
describe: "List all schema's that are available within the SaaS CMS instance",
|
|
2001
|
+
builder: (yargs) => {
|
|
2002
|
+
const newYargs = yargs;
|
|
2003
|
+
return newYargs;
|
|
2004
|
+
},
|
|
2005
|
+
async handler(args, opts) {
|
|
2006
|
+
const client = createCmsClient(args);
|
|
2007
|
+
process.stdout.write(`\n${figures.arrowRight} Downloading OpenAPI Specification\n`);
|
|
2008
|
+
const spec = await client.getOpenApiSpec();
|
|
2009
|
+
process.stdout.write(chalk.greenBright(` ${figures.tick} `));
|
|
2010
|
+
process.stdout.write(`The ${spec.info.title} (Version: ${spec.info.version}) specification has been downloaded.\n`);
|
|
2011
|
+
const specSchemas = (spec?.components?.schemas ?? {});
|
|
2012
|
+
const schemas = new Table({
|
|
2013
|
+
head: [
|
|
2014
|
+
chalk.yellow(chalk.bold("Key")),
|
|
2015
|
+
chalk.yellow(chalk.bold("Description"))
|
|
2016
|
+
],
|
|
2017
|
+
colWidths: [35, 60],
|
|
2018
|
+
colAligns: ["left", "left"],
|
|
2019
|
+
wordWrap: true,
|
|
2020
|
+
wrapOnWordBoundary: true
|
|
2021
|
+
});
|
|
2022
|
+
for (const key in specSchemas)
|
|
2023
|
+
if (key) {
|
|
2024
|
+
schemas.push([key, specSchemas[key].description?.replaceAll(/\s+/g, ' ') ?? `-`]);
|
|
2025
|
+
}
|
|
2026
|
+
process.stdout.write("\n" + schemas.toString() + "\n");
|
|
2027
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
|
|
2031
|
+
const deepmerge = createDeepMerge();
|
|
2032
|
+
const SchemaValidateCommand = {
|
|
2033
|
+
command: "schema:validate",
|
|
2034
|
+
describe: "Validate the opti-type.json & opti-style.json files",
|
|
2035
|
+
builder: (yargs) => {
|
|
2036
|
+
const newYargs = yargs;
|
|
2037
|
+
//newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] })
|
|
2038
|
+
//newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false })
|
|
2039
|
+
//newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true })
|
|
2040
|
+
return newYargs;
|
|
2041
|
+
},
|
|
2042
|
+
handler: async (args) => {
|
|
2043
|
+
const projectPath = args.path;
|
|
2044
|
+
const client = createCmsClient(args);
|
|
2045
|
+
const schemas = await loadSchema(client, ['DisplayTemplate', 'ContentType']);
|
|
2046
|
+
process.stdout.write(``);
|
|
2047
|
+
const styleSchema = schemas.find(x => x.title == 'DisplayTemplate')?.schema;
|
|
2048
|
+
const typeSchema = schemas.find(x => x.title == 'ContentType')?.schema;
|
|
2049
|
+
const [styleValidator, typeValidator] = await Promise.all([getValidator(styleSchema), getValidator(typeSchema)]);
|
|
2050
|
+
process.stdout.write(`\n${figures.arrowRight} Validating display templates (*.opti-style.json) and content types (*.opti-type.json).\n`);
|
|
2051
|
+
const patterns = ["./**/*.opti-type.json", "./**/*.opti-style.json"];
|
|
2052
|
+
const globMatches = globIterate(patterns, { cwd: projectPath, absolute: true });
|
|
2053
|
+
let allOk = true;
|
|
2054
|
+
for await (const fileMatch of globMatches) {
|
|
2055
|
+
try {
|
|
2056
|
+
const content = JSON.parse(fs.readFileSync(fileMatch).toString());
|
|
2057
|
+
let errors = undefined;
|
|
2058
|
+
if (fileMatch.endsWith('.opti-type.json') && !typeValidator(content)) {
|
|
2059
|
+
errors = typeValidator.errors;
|
|
2060
|
+
}
|
|
2061
|
+
else if (fileMatch.endsWith('.opti-style.json') && !styleValidator(content)) {
|
|
2062
|
+
errors = styleValidator.errors;
|
|
2063
|
+
}
|
|
2064
|
+
if (errors) {
|
|
2065
|
+
allOk = false;
|
|
2066
|
+
process.stdout.write(chalk.redBright(chalk.bold(`\n${figures.cross} ${path.relative(projectPath, fileMatch)}\n`)));
|
|
2067
|
+
const groupedErrors = groupErrors(errors);
|
|
2068
|
+
for (const instancepath in groupedErrors) {
|
|
2069
|
+
for (const key in groupedErrors[instancepath]) {
|
|
2070
|
+
const errorList = groupedErrors[instancepath][key].filter((v, i, a) => a.findIndex(pv => deepEqual(v, pv)) === i);
|
|
2071
|
+
for (const error of errorList) {
|
|
2072
|
+
process.stdout.write(` Node ${instancepath} ${error.message}\n`);
|
|
2073
|
+
switch (error.keyword) {
|
|
2074
|
+
case 'enum':
|
|
2075
|
+
process.stdout.write(` Allowed values: ${error.params.allowedValues.map((x) => x.toString()).join(', ')}\n`);
|
|
2076
|
+
break;
|
|
2077
|
+
case 'oneOf':
|
|
2078
|
+
const matching = Array.isArray(error.params.passingSchemas) ? error.params.passingSchemas : [];
|
|
2079
|
+
if (matching.length === 0)
|
|
2080
|
+
process.stdout.write(` Currently none matching\n`);
|
|
2081
|
+
else
|
|
2082
|
+
process.stdout.write(` Currently matching ${matching.map((x) => x.toString()).join(", ")}\n`);
|
|
2083
|
+
break;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
catch (e) {
|
|
2091
|
+
allOk = false;
|
|
2092
|
+
process.stdout.write(chalk.redBright(chalk.bold(`${figures.cross} ${path.relative(projectPath, fileMatch)}\n`)));
|
|
2093
|
+
process.stdout.write(` ${e.message}\n`);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
process.stdout.write(`\n`);
|
|
2097
|
+
if (allOk)
|
|
2098
|
+
process.stdout.write(chalk.greenBright(`${figures.tick} All types & styles match the schema\n`));
|
|
2099
|
+
else {
|
|
2100
|
+
process.stdout.write(chalk.redBright(`${figures.cross} Some types & styles contain errors\n`));
|
|
2101
|
+
process.exit(1);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
function groupErrors(errors) {
|
|
2106
|
+
const errorrsByInstance = groupErrorsByPath(errors);
|
|
2107
|
+
const instancePaths = Object.getOwnPropertyNames(errorrsByInstance);
|
|
2108
|
+
return instancePaths.reduce((previous, current) => {
|
|
2109
|
+
previous[current] = groupErrorsByKeyword(errorrsByInstance[current]);
|
|
2110
|
+
return previous;
|
|
2111
|
+
}, {});
|
|
2112
|
+
}
|
|
2113
|
+
function groupErrorsByPath(errors) {
|
|
2114
|
+
return errors.reduce((previous, current) => {
|
|
2115
|
+
const instancePath = current.instancePath == "" ? "[ROOT]" : current.instancePath;
|
|
2116
|
+
const partial = {};
|
|
2117
|
+
partial[instancePath] = [current];
|
|
2118
|
+
return deepmerge(previous, partial);
|
|
2119
|
+
}, {});
|
|
2120
|
+
}
|
|
2121
|
+
function groupErrorsByKeyword(errors) {
|
|
2122
|
+
const toMerge = errors.reduce((previous, current) => {
|
|
2123
|
+
const keyword = current.keyword;
|
|
2124
|
+
const partial = {};
|
|
2125
|
+
partial[keyword] = [current];
|
|
2126
|
+
return deepmerge(previous, partial);
|
|
2127
|
+
}, {});
|
|
2128
|
+
// Enum errors must be merged
|
|
2129
|
+
if (toMerge.enum)
|
|
2130
|
+
toMerge.enum = [toMerge.enum.reduce((prev, current) => prev ? deepmerge(prev, current) : current, undefined)];
|
|
2131
|
+
return toMerge;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
const SchemaVsCodeCommand = {
|
|
2135
|
+
command: "schema:vscode",
|
|
2136
|
+
describe: "Configure schema validation for opti-type.json & opti-style.json files by VSCode in the current project folder",
|
|
2137
|
+
builder: (yargs) => {
|
|
2138
|
+
const newYargs = yargs;
|
|
2139
|
+
//newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] })
|
|
2140
|
+
//newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false })
|
|
2141
|
+
//newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true })
|
|
2142
|
+
return newYargs;
|
|
2143
|
+
},
|
|
2144
|
+
handler: async (args) => {
|
|
2145
|
+
const projectPath = args.path;
|
|
2146
|
+
const client = createCmsClient(args);
|
|
2147
|
+
const schemas = await loadSchema(client, ['DisplayTemplate', 'ContentType']);
|
|
2148
|
+
process.stdout.write(`\n${figures.arrowRight} Writing schema files\n`);
|
|
2149
|
+
for (const schema of schemas) {
|
|
2150
|
+
const schemaFile = path.join(projectPath, '.vscode', `${schema.title.toLowerCase()}.schema.json`);
|
|
2151
|
+
void await writeFileAsync(schemaFile, JSON.stringify(schema.schema, undefined, 2));
|
|
2152
|
+
process.stdout.write(` ${figures.tick} Written the ${schema.title} schema to ${path.relative(projectPath, schemaFile)}\n`);
|
|
2153
|
+
}
|
|
2154
|
+
process.stdout.write(`\n${figures.arrowRight} Updating folder settings\n`);
|
|
2155
|
+
const settingsFile = path.join(projectPath, '.vscode', 'settings.json');
|
|
2156
|
+
const settings = (await readJsonFileAsync(settingsFile).catch(() => undefined)) ?? {};
|
|
2157
|
+
settings['json.validate.enable'] = true;
|
|
2158
|
+
settings['json.schemaDownload.enable'] = true;
|
|
2159
|
+
settings['json.schemas'] = settings['json.schemas'] || [];
|
|
2160
|
+
const hasContentType = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.opti-type.json')) >= 0) >= 0;
|
|
2161
|
+
const hasStyleDefinition = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.opti-style.json')) >= 0) >= 0;
|
|
2162
|
+
const hasSchemaType = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.schema.json')) >= 0) >= 0;
|
|
2163
|
+
if (!hasContentType)
|
|
2164
|
+
settings['json.schemas'].push({
|
|
2165
|
+
"fileMatch": [
|
|
2166
|
+
"**/*.opti-type.json"
|
|
2167
|
+
],
|
|
2168
|
+
"url": "./.vscode/contenttype.schema.json"
|
|
2169
|
+
});
|
|
2170
|
+
if (!hasStyleDefinition)
|
|
2171
|
+
settings['json.schemas'].push({
|
|
2172
|
+
"fileMatch": [
|
|
2173
|
+
"**/*.opti-style.json"
|
|
2174
|
+
],
|
|
2175
|
+
"url": "./.vscode/displaytemplate.schema.json"
|
|
2176
|
+
});
|
|
2177
|
+
if (!hasSchemaType)
|
|
2178
|
+
settings['json.schemas'].push({
|
|
2179
|
+
"fileMatch": [
|
|
2180
|
+
"**/*.schema.json"
|
|
2181
|
+
],
|
|
2182
|
+
"url": "https://json-schema.org/draft-07/schema"
|
|
2183
|
+
});
|
|
2184
|
+
if (settings['json.schemas'].length == 0)
|
|
2185
|
+
delete settings['json.schemas'];
|
|
2186
|
+
await writeFileAsync(settingsFile, JSON.stringify(settings, undefined, 2));
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
function readJsonFileAsync(path) {
|
|
2190
|
+
return new Promise((resolve, reject) => fs.readFile(path, (err, data) => {
|
|
2191
|
+
if (err) {
|
|
2192
|
+
if (err?.code === 'ENOENT')
|
|
2193
|
+
resolve(undefined);
|
|
2194
|
+
else
|
|
2195
|
+
reject(err);
|
|
2196
|
+
}
|
|
2197
|
+
else {
|
|
2198
|
+
try {
|
|
2199
|
+
const d = data.toString();
|
|
2200
|
+
resolve(JSON.parse(d));
|
|
2201
|
+
}
|
|
2202
|
+
catch (e) {
|
|
2203
|
+
reject(e);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}));
|
|
2207
|
+
}
|
|
2208
|
+
function writeFileAsync(path, data) {
|
|
2209
|
+
return new Promise((resolve, reject) => {
|
|
2210
|
+
fs.writeFile(path, data, (err) => {
|
|
2211
|
+
if (err)
|
|
2212
|
+
reject(err);
|
|
2213
|
+
else
|
|
2214
|
+
resolve();
|
|
2215
|
+
});
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
var version = "6.0.0-pre10";
|
|
2220
|
+
var name = "opti-cms";
|
|
2221
|
+
var APP = {
|
|
2222
|
+
version: version,
|
|
2223
|
+
name: name
|
|
2224
|
+
};
|
|
2225
|
+
|
|
1496
2226
|
const CmsVersionCommand = {
|
|
1497
2227
|
command: "cms:version",
|
|
1498
2228
|
aliases: "$0",
|
|
@@ -1511,19 +2241,25 @@ const CmsVersionCommand = {
|
|
|
1511
2241
|
throw error;
|
|
1512
2242
|
}
|
|
1513
2243
|
});
|
|
2244
|
+
const hasPreview2Info = versionInfo.results.preview2Data?.baseUrl ? true : false;
|
|
1514
2245
|
const info = new Table({
|
|
1515
2246
|
head: [
|
|
1516
2247
|
chalk.yellow(chalk.bold("Component")),
|
|
1517
2248
|
chalk.yellow(chalk.bold("Version"))
|
|
1518
2249
|
],
|
|
1519
|
-
colWidths: [
|
|
2250
|
+
colWidths: [30, 60],
|
|
1520
2251
|
colAligns: ["left", "left"]
|
|
1521
2252
|
});
|
|
1522
|
-
info.push(["
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
info.push(["
|
|
1526
|
-
info.push(["Service", versionInfo.
|
|
2253
|
+
info.push(["Base URL", versionInfo.baseUrl ?? client.cmsUrl.href]);
|
|
2254
|
+
if (hasPreview2Info)
|
|
2255
|
+
info.push(["Base URL (Preview 2)", versionInfo.results.preview2Data?.baseUrl ?? 'n/a']);
|
|
2256
|
+
info.push(["Client API", client.apiVersion]);
|
|
2257
|
+
info.push(["Service API", versionInfo.apiVersion]);
|
|
2258
|
+
if (hasPreview2Info)
|
|
2259
|
+
info.push(["Service API (Preview 2)", versionInfo.results.preview2Data?.apiVersion ?? 'n/a']);
|
|
2260
|
+
info.push(["CMS Build", versionInfo.cmsVersion]);
|
|
2261
|
+
info.push(["Service Build", versionInfo.serviceVersion]);
|
|
2262
|
+
info.push(["SDK", APP.version]);
|
|
1527
2263
|
process.stdout.write(info.toString() + "\n");
|
|
1528
2264
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Optimizely CMS Status: ${versionInfo.status}\n`));
|
|
1529
2265
|
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
@@ -1643,7 +2379,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
|
|
|
1643
2379
|
process.stderr.write(chalk.redBright(` ${figures.cross}The ContentItem ${key} is a reserved item, skipping deletion\n`));
|
|
1644
2380
|
return removedCount;
|
|
1645
2381
|
}
|
|
1646
|
-
const deleteResult = await client.
|
|
2382
|
+
const deleteResult = await client.preview2ContentDelete({ path: { key }, query: { permanent: true } }).then(r => r.key).catch((e) => {
|
|
1647
2383
|
if (e.status == 404)
|
|
1648
2384
|
return key;
|
|
1649
2385
|
throw e;
|
|
@@ -1669,8 +2405,7 @@ async function resetSystemTypes(client) {
|
|
|
1669
2405
|
body: {
|
|
1670
2406
|
key: systemType,
|
|
1671
2407
|
properties: {}
|
|
1672
|
-
}
|
|
1673
|
-
query: { ignoreDataLossWarnings: true }
|
|
2408
|
+
}
|
|
1674
2409
|
}).catch((e) => e.status == 404 ? undefined : null);
|
|
1675
2410
|
if (newType)
|
|
1676
2411
|
resetTypes++;
|
|
@@ -1771,7 +2506,7 @@ async function getAllTypes(client, batchSize = 100) {
|
|
|
1771
2506
|
return actualItems;
|
|
1772
2507
|
}
|
|
1773
2508
|
async function getAllAssets(client, parentKey, batchSize = 100) {
|
|
1774
|
-
const items = await client.
|
|
2509
|
+
const items = await client.preview2ContentListAssets({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
|
|
1775
2510
|
if (e.status == 404) {
|
|
1776
2511
|
return {
|
|
1777
2512
|
items: [],
|
|
@@ -1786,7 +2521,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
|
|
|
1786
2521
|
const actualItems = items.items ?? [];
|
|
1787
2522
|
const pageCount = Math.ceil(totalItemCount / pageSize);
|
|
1788
2523
|
for (let pageNr = 1; pageNr < pageCount; pageNr++) {
|
|
1789
|
-
const pageData = await client.
|
|
2524
|
+
const pageData = await client.preview2ContentListAssets({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
|
|
1790
2525
|
if (e.status == 404) {
|
|
1791
2526
|
return {
|
|
1792
2527
|
items: [],
|
|
@@ -1801,7 +2536,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
|
|
|
1801
2536
|
return actualItems;
|
|
1802
2537
|
}
|
|
1803
2538
|
async function getAllItems(client, parentKey, batchSize = 100) {
|
|
1804
|
-
const items = await client.
|
|
2539
|
+
const items = await client.preview2ContentListItems({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
|
|
1805
2540
|
if (e.status == 404) {
|
|
1806
2541
|
return {
|
|
1807
2542
|
items: [],
|
|
@@ -1816,7 +2551,7 @@ async function getAllItems(client, parentKey, batchSize = 100) {
|
|
|
1816
2551
|
const actualItems = items.items ?? [];
|
|
1817
2552
|
const pageCount = Math.ceil(totalItemCount / pageSize);
|
|
1818
2553
|
for (let pageNr = 1; pageNr < pageCount; pageNr++) {
|
|
1819
|
-
const pageData = await client.
|
|
2554
|
+
const pageData = await client.preview2ContentListItems({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
|
|
1820
2555
|
if (e.status == 404) {
|
|
1821
2556
|
return {
|
|
1822
2557
|
items: [],
|
|
@@ -1833,28 +2568,26 @@ async function getAllItems(client, parentKey, batchSize = 100) {
|
|
|
1833
2568
|
|
|
1834
2569
|
// Style processing
|
|
1835
2570
|
const commands = [
|
|
2571
|
+
CmsResetCommand,
|
|
2572
|
+
CmsVersionCommand,
|
|
2573
|
+
NextJsComponentsCommand,
|
|
2574
|
+
NextJsCreateCommand,
|
|
2575
|
+
NextJsFactoryCommand,
|
|
2576
|
+
NextJsFragmentsCommand,
|
|
2577
|
+
NextJsQueriesCommand,
|
|
2578
|
+
NextJsVisualBuilderCommand,
|
|
2579
|
+
SchemaDownloadCommand,
|
|
2580
|
+
SchemaListCommand,
|
|
2581
|
+
SchemaValidateCommand,
|
|
2582
|
+
SchemaVsCodeCommand,
|
|
1836
2583
|
StylesCreateCommand,
|
|
1837
|
-
StylesPushCommand,
|
|
1838
2584
|
StylesListCommand,
|
|
1839
2585
|
StylesPullCommand,
|
|
2586
|
+
StylesPushCommand,
|
|
1840
2587
|
TypesPullCommand,
|
|
1841
|
-
TypesPushCommand
|
|
1842
|
-
NextJsCreateCommand,
|
|
1843
|
-
NextJsQueriesCommand,
|
|
1844
|
-
NextJsComponentsCommand,
|
|
1845
|
-
NextJsVisualBuilderCommand,
|
|
1846
|
-
NextJsFactoryCommand,
|
|
1847
|
-
CmsResetCommand,
|
|
1848
|
-
CmsVersionCommand
|
|
2588
|
+
TypesPushCommand
|
|
1849
2589
|
];
|
|
1850
2590
|
|
|
1851
|
-
var version = "6.0.0-pre1";
|
|
1852
|
-
var name = "opti-cms";
|
|
1853
|
-
var APP = {
|
|
1854
|
-
version: version,
|
|
1855
|
-
name: name
|
|
1856
|
-
};
|
|
1857
|
-
|
|
1858
2591
|
async function main() {
|
|
1859
2592
|
const envFiles = prepare();
|
|
1860
2593
|
const app = createOptiCmsApp(APP.name, APP.version, undefined, envFiles);
|
|
@@ -1864,7 +2597,7 @@ async function main() {
|
|
|
1864
2597
|
}
|
|
1865
2598
|
catch {
|
|
1866
2599
|
//We're ignoring error here, as yargs will already generate the "nice output" for it
|
|
1867
|
-
//console.log
|
|
2600
|
+
//console.log('Caught error')
|
|
1868
2601
|
}
|
|
1869
2602
|
}
|
|
1870
2603
|
main();
|