@remkoj/optimizely-cms-cli 6.0.0-pre1 → 6.0.0-pre11
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 +1294 -563
- 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,26 @@ const TypesPullCommand = {
|
|
|
664
835
|
const outContentType = { ...contentType };
|
|
665
836
|
if (outContentType.source || outContentType.source == "")
|
|
666
837
|
delete outContentType.source;
|
|
667
|
-
if (outContentType.
|
|
668
|
-
delete outContentType.features;
|
|
669
|
-
if (outContentType.usage)
|
|
670
|
-
delete outContentType.usage;
|
|
671
|
-
if (outContentType.lastModifiedBy)
|
|
838
|
+
if (outContentType.lastModifiedBy || outContentType.lastModifiedBy == "")
|
|
672
839
|
delete outContentType.lastModifiedBy;
|
|
673
840
|
if (outContentType.lastModified)
|
|
674
841
|
delete outContentType.lastModified;
|
|
675
842
|
if (outContentType.created)
|
|
676
843
|
delete outContentType.created;
|
|
844
|
+
for (const propName of Object.getOwnPropertyNames(outContentType.properties ?? {})) {
|
|
845
|
+
if (!["content", "contentReference"].includes(outContentType.properties[propName].type)) {
|
|
846
|
+
if (isEmptyArray(outContentType.properties[propName].allowedTypes))
|
|
847
|
+
delete outContentType.properties[propName].allowedTypes;
|
|
848
|
+
if (isEmptyArray(outContentType.properties[propName].restrictedTypes))
|
|
849
|
+
delete outContentType.properties[propName].restrictedTypes;
|
|
850
|
+
}
|
|
851
|
+
if (outContentType.properties[propName].type == 'array' && !["content", "contentReference"].includes(outContentType.properties[propName].items?.type)) {
|
|
852
|
+
if (isEmptyArray(outContentType.properties[propName].items.allowedTypes))
|
|
853
|
+
delete outContentType.properties[propName].items.allowedTypes;
|
|
854
|
+
if (isEmptyArray(outContentType.properties[propName].items.restrictedTypes))
|
|
855
|
+
delete outContentType.properties[propName].items.restrictedTypes;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
677
858
|
if (debug)
|
|
678
859
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key})\n`));
|
|
679
860
|
fs.writeFileSync(typeFile, JSON.stringify(outContentType, undefined, 2));
|
|
@@ -682,6 +863,197 @@ const TypesPullCommand = {
|
|
|
682
863
|
process.stdout.write(chalk.green(chalk.bold(`${figures.tick} Created/updated type definitions for ${updatedTypes.join(', ')}\n`)));
|
|
683
864
|
}
|
|
684
865
|
};
|
|
866
|
+
function isEmptyArray(toTest) {
|
|
867
|
+
return toTest === null || !Array.isArray(toTest) || toTest.length === 0;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const deepmerge$1 = createDeepMerge();
|
|
871
|
+
async function loadSchema(client, schemaName) {
|
|
872
|
+
const schemas = Array.isArray(schemaName) ? schemaName : [schemaName];
|
|
873
|
+
process.stdout.write(`${figures.arrowRight} Downloading current JSON Schema\n`);
|
|
874
|
+
const spec = await client.getOpenApiSpec();
|
|
875
|
+
const specSchemas = spec.components?.schemas ?? {};
|
|
876
|
+
process.stdout.write(` ${figures.tick} Complete\n`);
|
|
877
|
+
const result = [];
|
|
878
|
+
process.stdout.write(`\n${figures.arrowRight} Constructing schema for ${schemas.join(', ')}\n`);
|
|
879
|
+
for await (const schema of schemas)
|
|
880
|
+
if (specSchemas[schema]) {
|
|
881
|
+
const definitions = {};
|
|
882
|
+
const processedSchema = processSchema(specSchemas[schema], definitions, spec);
|
|
883
|
+
const jsonSchema = {
|
|
884
|
+
//"$schema": "https://json-schema.org/draft-07/schema",
|
|
885
|
+
"$id": new URL(`schema/${schema}`, client.getSchemaItemBase()).href,
|
|
886
|
+
type: "object",
|
|
887
|
+
title: schema,
|
|
888
|
+
...processedSchema,
|
|
889
|
+
definitions
|
|
890
|
+
};
|
|
891
|
+
result.push({
|
|
892
|
+
title: schema,
|
|
893
|
+
schema: postProcessDefintions(jsonSchema)
|
|
894
|
+
});
|
|
895
|
+
process.stdout.write(` ${figures.tick} Constructed schema of ${schema}\n`);
|
|
896
|
+
}
|
|
897
|
+
return result;
|
|
898
|
+
}
|
|
899
|
+
function getValidator(schemaObject) {
|
|
900
|
+
const ajv = new Ajv({
|
|
901
|
+
discriminator: true
|
|
902
|
+
});
|
|
903
|
+
addFormats(ajv);
|
|
904
|
+
return ajv.compile(schemaObject);
|
|
905
|
+
}
|
|
906
|
+
function postProcessDefintions(jsonSchema) {
|
|
907
|
+
if (!jsonSchema.definitions)
|
|
908
|
+
return jsonSchema;
|
|
909
|
+
for (const definitionName in jsonSchema.definitions) {
|
|
910
|
+
if ((definitionName.endsWith("Property") || definitionName.endsWith("ListItem")) && jsonSchema.definitions[definitionName].properties) {
|
|
911
|
+
const type = jsonSchema.definitions[definitionName].properties?.type;
|
|
912
|
+
if (isRefSchema(type)) {
|
|
913
|
+
const typeSchema = resolveRefSchema(type, jsonSchema);
|
|
914
|
+
if (typeSchema && typeSchema.type === 'string' && Array.isArray(typeSchema.enum)) {
|
|
915
|
+
let typeValue = definitionName.substring(0, definitionName.length - 8);
|
|
916
|
+
typeValue = typeValue[0].toLowerCase() + typeValue.substring(1);
|
|
917
|
+
typeValue = typeValue === 'list' ? 'array' : typeValue;
|
|
918
|
+
typeValue = typeValue === 'jsonString' ? 'json' : typeValue;
|
|
919
|
+
if (typeSchema.enum.includes(typeValue)) {
|
|
920
|
+
const newTypeDef = deepmerge$1({}, typeSchema);
|
|
921
|
+
newTypeDef.enum = newTypeDef.enum.filter((x) => x === typeValue);
|
|
922
|
+
jsonSchema.definitions[definitionName].properties.type = newTypeDef;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return jsonSchema;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Schema normalization to tranform the schema from a .Net generated OpenAPI Schema
|
|
932
|
+
* to a AJV compatible JSON Schema
|
|
933
|
+
*
|
|
934
|
+
* @see https://ajv.js.org/
|
|
935
|
+
* @param schema
|
|
936
|
+
* @param defs
|
|
937
|
+
* @param spec
|
|
938
|
+
* @returns
|
|
939
|
+
*/
|
|
940
|
+
function processSchema(schema, defs, spec, mergeAllOf = true) {
|
|
941
|
+
if (Array.isArray(schema))
|
|
942
|
+
return schema.map(s => processSchema(s, defs, spec, mergeAllOf));
|
|
943
|
+
if (typeof schema !== 'object' || schema === null)
|
|
944
|
+
return schema;
|
|
945
|
+
const props = Object.getOwnPropertyNames(schema);
|
|
946
|
+
if (props.length === 1 && props[0] === '$ref') {
|
|
947
|
+
const ref = schema['$ref'];
|
|
948
|
+
if (isLocalRef(ref)) {
|
|
949
|
+
const refName = getLocalRefName(ref);
|
|
950
|
+
if (!defs[refName]) {
|
|
951
|
+
const refItem = resolveLocalRef(ref, spec);
|
|
952
|
+
defs[refName] = processSchema(refItem, defs, spec, mergeAllOf);
|
|
953
|
+
}
|
|
954
|
+
const newRef = `#/definitions/${refName}`;
|
|
955
|
+
return { "$ref": newRef };
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
return schema;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
else if (mergeAllOf && props.includes('allOf') && Array.isArray(schema['allOf'])) {
|
|
962
|
+
// process all of
|
|
963
|
+
const newObject = props.reduce((generated, propName) => {
|
|
964
|
+
if (propName != 'allOf')
|
|
965
|
+
generated[propName] = schema[propName];
|
|
966
|
+
return generated;
|
|
967
|
+
}, {});
|
|
968
|
+
const allOfSchemas = schema['allOf'].map((subschema) => {
|
|
969
|
+
const resolvedSchema = isRefSchema(subschema) ? resolveRefSchema(subschema, spec) : subschema;
|
|
970
|
+
const resolved = processSchema(resolvedSchema, defs, spec, mergeAllOf);
|
|
971
|
+
return resolved;
|
|
972
|
+
});
|
|
973
|
+
const merged = allOfSchemas.reduce((previous, current) => deepmerge$1(previous, current), newObject);
|
|
974
|
+
if (newObject['description'])
|
|
975
|
+
merged['description'] = newObject['description'];
|
|
976
|
+
if (newObject['title'])
|
|
977
|
+
merged['title'] = newObject['title'];
|
|
978
|
+
return merged;
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
const newSchema = {};
|
|
982
|
+
for (const key of props) {
|
|
983
|
+
if (key === 'pattern' && typeof schema[key] === 'string') {
|
|
984
|
+
newSchema[key] = safeCreateUnicodeRegExp(schema[key]);
|
|
985
|
+
/*} else if (key === 'readOnly' && schema[key] === true) {
|
|
986
|
+
return undefined*/
|
|
987
|
+
}
|
|
988
|
+
else if (['additionalProperties', 'required', 'properties', 'discriminator'].includes(key) && typeof schema[key] !== 'object') ;
|
|
989
|
+
else if (key === 'discriminator' && !Array.isArray(schema['oneOf'])) ;
|
|
990
|
+
else if (key === 'discriminator' && !schema['type']) {
|
|
991
|
+
newSchema.type = 'object';
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
const keyVal = processSchema(schema[key], defs, spec, mergeAllOf);
|
|
995
|
+
if (keyVal !== undefined)
|
|
996
|
+
newSchema[key] = keyVal;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (newSchema['oneOf'] && Array.isArray(newSchema['oneOf']) && newSchema['nullable']) {
|
|
1000
|
+
delete newSchema['nullable'];
|
|
1001
|
+
}
|
|
1002
|
+
return newSchema;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function isRefSchema(schema) {
|
|
1006
|
+
if (typeof schema != 'object' || schema == null)
|
|
1007
|
+
return false;
|
|
1008
|
+
return Object.getOwnPropertyNames(schema).length === 1 && typeof schema['$ref'] === 'string';
|
|
1009
|
+
}
|
|
1010
|
+
function resolveRefSchema(schema, spec) {
|
|
1011
|
+
if (!isRefSchema(schema))
|
|
1012
|
+
return undefined;
|
|
1013
|
+
return resolveLocalRef(schema['$ref'], spec);
|
|
1014
|
+
}
|
|
1015
|
+
function isLocalRef(ref) {
|
|
1016
|
+
return typeof ref === 'string' && ref.startsWith('#');
|
|
1017
|
+
}
|
|
1018
|
+
function getLocalRefName(ref) {
|
|
1019
|
+
if (!isLocalRef(ref))
|
|
1020
|
+
return undefined;
|
|
1021
|
+
const refPath = ref.substring(2).split('/');
|
|
1022
|
+
return refPath.at(refPath.length - 1);
|
|
1023
|
+
}
|
|
1024
|
+
function resolveLocalRef(ref, spec) {
|
|
1025
|
+
const refPath = ref.substring(2).split('/');
|
|
1026
|
+
return refPath.reduce((integrator, current) => {
|
|
1027
|
+
return integrator ? integrator[current] : undefined;
|
|
1028
|
+
}, spec);
|
|
1029
|
+
}
|
|
1030
|
+
const replaceEscapedChars = [
|
|
1031
|
+
[new RegExp('\\\\ ', 'g'), ' '],
|
|
1032
|
+
[new RegExp('\\\\!', 'g'), '!'],
|
|
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('\\\\[zZ]', 'g'), '$'],
|
|
1050
|
+
];
|
|
1051
|
+
function safeCreateUnicodeRegExp(pattern) {
|
|
1052
|
+
for (const unEscape of replaceEscapedChars) {
|
|
1053
|
+
pattern = pattern.replace(unEscape[0], unEscape[1]);
|
|
1054
|
+
}
|
|
1055
|
+
return pattern;
|
|
1056
|
+
}
|
|
685
1057
|
|
|
686
1058
|
const TypesPushCommand = {
|
|
687
1059
|
command: "types:push",
|
|
@@ -712,9 +1084,18 @@ const TypesPushCommand = {
|
|
|
712
1084
|
(!baseTypes || baseTypes.length == 0 || baseTypes.includes(data.definition.baseType)) &&
|
|
713
1085
|
(!types || types.length == 0 || types.includes(data.definition.key));
|
|
714
1086
|
});
|
|
1087
|
+
// Create validator
|
|
1088
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Loading OpenAPI Specification for validation\n`));
|
|
1089
|
+
const typeSchema = (await loadSchema(client, 'ContentType')).at(0)?.schema;
|
|
1090
|
+
const typeValidator = await getValidator(typeSchema);
|
|
715
1091
|
// Output selected types
|
|
716
|
-
const results = await Promise.all(typeDefinitions.map(type => {
|
|
1092
|
+
const results = await Promise.all(typeDefinitions.map(async (type) => {
|
|
1093
|
+
if (!type.definition.key) {
|
|
1094
|
+
process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Content Type has no key in ${type.file}\n`));
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
717
1097
|
process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Pushing ${type.definition.displayName} from ${type.file}\n`));
|
|
1098
|
+
// Filter unwanted fields from the ContentType definition
|
|
718
1099
|
const outType = { ...type.definition };
|
|
719
1100
|
if (outType.source)
|
|
720
1101
|
delete outType.source;
|
|
@@ -724,13 +1105,31 @@ const TypesPushCommand = {
|
|
|
724
1105
|
delete outType.lastModified;
|
|
725
1106
|
if (outType.lastModifiedBy || outType.lastModifiedBy == "")
|
|
726
1107
|
delete outType.lastModifiedBy;
|
|
727
|
-
if (outType.features)
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
.
|
|
1108
|
+
//if (outType.features) delete outType.features
|
|
1109
|
+
//if (outType.usage) delete outType.usage
|
|
1110
|
+
const validationResult = typeValidator(outType);
|
|
1111
|
+
if (validationResult || force) {
|
|
1112
|
+
if (!validationResult)
|
|
1113
|
+
process.stdout.write(chalk.yellowBright(` ${figures.arrowRight} Ignoring validation errors in ${type.file}\n`));
|
|
1114
|
+
const currentContentType = (await client.contentTypesGet({ path: { key: type.definition.key } }).catch(() => null));
|
|
1115
|
+
function buildError(e) {
|
|
1116
|
+
return { key: type.definition.key, type: type.definition, file: type.file, error: e };
|
|
1117
|
+
}
|
|
1118
|
+
function buildData(ct) {
|
|
1119
|
+
return { key: type.definition.key, type: ct, file: type.file };
|
|
1120
|
+
}
|
|
1121
|
+
return (currentContentType ?
|
|
1122
|
+
client.contentTypesPatch({ path: { key: type.definition.key }, body: outType }) :
|
|
1123
|
+
client.contentTypesCreate({ body: outType })).then(buildData).catch(buildError);
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
return {
|
|
1127
|
+
key: type.definition.key,
|
|
1128
|
+
type: type.definition,
|
|
1129
|
+
file: type.file,
|
|
1130
|
+
error: typeValidator.errors.map(x => x.message).join(', ')
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
734
1133
|
}));
|
|
735
1134
|
const overview = new Table({
|
|
736
1135
|
head: [
|
|
@@ -740,255 +1139,32 @@ const TypesPushCommand = {
|
|
|
740
1139
|
chalk.yellow(chalk.bold("Message"))
|
|
741
1140
|
],
|
|
742
1141
|
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
|
|
1142
|
+
colAligns: ["left", "left", "center", "left"]
|
|
974
1143
|
});
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1144
|
+
results.forEach(item => {
|
|
1145
|
+
overview.push([
|
|
1146
|
+
item.type.displayName,
|
|
1147
|
+
item.key,
|
|
1148
|
+
item.error ? chalk.redBright(chalk.bold(figures.cross)) : chalk.green(chalk.bold(figures.tick)),
|
|
1149
|
+
item.error ?? ''
|
|
1150
|
+
]);
|
|
1151
|
+
});
|
|
1152
|
+
process.stdout.write(overview.toString() + "\n");
|
|
1153
|
+
// Mark us as done
|
|
1154
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
983
1155
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
const builder = yargs => {
|
|
1159
|
+
const updatedArgs = contentTypesBuilder(yargs);
|
|
1160
|
+
updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
1161
|
+
return updatedArgs;
|
|
1162
|
+
};
|
|
1163
|
+
function createTypeFolders(contentTypes, basePath, debug = false) {
|
|
1164
|
+
return contentTypes.map(contentType => getContentTypePaths(contentType, basePath, true, debug));
|
|
1165
|
+
}
|
|
1166
|
+
function getTypeFolder(list, type) {
|
|
1167
|
+
return list.filter(x => x.type == type).at(0);
|
|
992
1168
|
}
|
|
993
1169
|
|
|
994
1170
|
function ucFirst(current) {
|
|
@@ -1024,8 +1200,8 @@ const NextJsComponentsCommand = {
|
|
|
1024
1200
|
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
1025
1201
|
}
|
|
1026
1202
|
};
|
|
1027
|
-
function createComponent(contentType,
|
|
1028
|
-
const componentFile
|
|
1203
|
+
function createComponent(contentType, typePathInfo, force, debug = false) {
|
|
1204
|
+
const { componentFile, path: typePath } = typePathInfo;
|
|
1029
1205
|
if (fs.existsSync(componentFile)) {
|
|
1030
1206
|
if (!force) {
|
|
1031
1207
|
if (debug)
|
|
@@ -1040,8 +1216,9 @@ function createComponent(contentType, typePath, force, debug = false) {
|
|
|
1040
1216
|
// Get type information & short-hands
|
|
1041
1217
|
const displayTemplate = getDisplayTemplateInfo$1(contentType, typePath);
|
|
1042
1218
|
const baseDisplayTemplate = getBaseTypeTemplateInfo(contentType, typePath);
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
1219
|
+
const normalizedBaseType = normalizeBaseType(contentType.baseType ?? 'part');
|
|
1220
|
+
const varName = `${contentType.key}${ucFirst(normalizedBaseType)}`;
|
|
1221
|
+
const tplFn = Templates[normalizedBaseType] ?? Templates['default'];
|
|
1045
1222
|
if (!tplFn) {
|
|
1046
1223
|
if (debug)
|
|
1047
1224
|
process.stdout.write(chalk.redBright(`${figures.cross} Skipping ${contentType.displayName} (${contentType.key}) component - no template for ${contentType.baseType} found\n`));
|
|
@@ -1065,6 +1242,11 @@ function getBaseTypeTemplateInfo(contentType, typePath) {
|
|
|
1065
1242
|
}
|
|
1066
1243
|
return undefined;
|
|
1067
1244
|
}
|
|
1245
|
+
function normalizeBaseType(baseType) {
|
|
1246
|
+
if (baseType.startsWith('_'))
|
|
1247
|
+
return baseType.substring(1);
|
|
1248
|
+
return baseType;
|
|
1249
|
+
}
|
|
1068
1250
|
function getDisplayTemplateInfo$1(contentType, typePath) {
|
|
1069
1251
|
const displayTemplatesFile = path.join(typePath, 'displayTemplates.ts');
|
|
1070
1252
|
if (fs.existsSync(displayTemplatesFile)) {
|
|
@@ -1078,24 +1260,50 @@ function getDisplayTemplateInfo$1(contentType, typePath) {
|
|
|
1078
1260
|
}
|
|
1079
1261
|
const Templates = {
|
|
1080
1262
|
// Default Template for all components without specifics
|
|
1081
|
-
default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { type CmsComponent } from "@remkoj/optimizely-cms-react";
|
|
1263
|
+
default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
|
|
1082
1264
|
import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
|
|
1083
1265
|
import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
|
|
1084
1266
|
import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
|
|
1085
1267
|
|
|
1086
1268
|
/**
|
|
1087
1269
|
* ${contentType.displayName}
|
|
1270
|
+
* ---
|
|
1088
1271
|
* ${contentType.description}
|
|
1089
1272
|
*/
|
|
1090
|
-
export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''},
|
|
1273
|
+
export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps }) => {
|
|
1091
1274
|
const componentName = '${contentType.displayName}'
|
|
1092
1275
|
const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
|
|
1093
|
-
return <
|
|
1276
|
+
return <CmsEditable className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4" {...editProps}>
|
|
1094
1277
|
<div className="font-bold italic">{ componentName }</div>
|
|
1095
1278
|
<div>{ componentInfo }</div>
|
|
1096
1279
|
{ 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
|
-
|
|
1280
|
+
</CmsEditable>
|
|
1281
|
+
}
|
|
1282
|
+
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
1283
|
+
${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
|
|
1284
|
+
|
|
1285
|
+
export default ${varName}`,
|
|
1286
|
+
// Default Template for all section types
|
|
1287
|
+
section: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
|
|
1288
|
+
import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
|
|
1289
|
+
import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
|
|
1290
|
+
import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* ${contentType.displayName}
|
|
1294
|
+
* ---
|
|
1295
|
+
* ${contentType.description}
|
|
1296
|
+
*/
|
|
1297
|
+
export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps, children }) => {
|
|
1298
|
+
const componentName = '${contentType.displayName}'
|
|
1299
|
+
const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
|
|
1300
|
+
return <CmsEditable className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4" {...editProps}>
|
|
1301
|
+
<div className="font-bold italic">{ componentName }</div>
|
|
1302
|
+
<div>{ componentInfo }</div>
|
|
1303
|
+
{ 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> }
|
|
1304
|
+
${(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 */}'}
|
|
1305
|
+
<div>{ children }</div>
|
|
1306
|
+
</CmsEditable>
|
|
1099
1307
|
}
|
|
1100
1308
|
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
1101
1309
|
${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
|
|
@@ -1103,26 +1311,32 @@ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}
|
|
|
1103
1311
|
export default ${varName}`,
|
|
1104
1312
|
// Template for all page component types
|
|
1105
1313
|
page: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
|
|
1106
|
-
import { ${contentType.key}
|
|
1314
|
+
import { get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'${displayTemplate ? `
|
|
1107
1315
|
import { ${displayTemplate} } from "./displayTemplates";` : ''}
|
|
1108
1316
|
import { getSdk } from "@/gql"
|
|
1109
1317
|
|
|
1110
1318
|
/**
|
|
1111
1319
|
* ${contentType.displayName}
|
|
1320
|
+
* ---
|
|
1112
1321
|
* ${contentType.description}
|
|
1322
|
+
*
|
|
1323
|
+
* This component uses the content query that is auto-generated with the Optimizely CMS Preset for GraphQL Codegen, if you need
|
|
1324
|
+
* to override this query create a GraphQL file (for example: \`get${contentType.key}Data.query.graphql\`) in the same folder as
|
|
1325
|
+
* this file. This file must include a GraphQL query with the name \`get${contentType.key}Data\`.
|
|
1326
|
+
*
|
|
1327
|
+
* [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
|
|
1113
1328
|
*/
|
|
1114
|
-
export const ${varName} : CmsComponent
|
|
1329
|
+
export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''} }) => {
|
|
1115
1330
|
const componentName = '${contentType.displayName}'
|
|
1116
1331
|
const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
|
|
1117
1332
|
return <div className="mx-auto px-2 container">
|
|
1118
1333
|
<div className="font-bold italic">{ componentName }</div>
|
|
1119
1334
|
<div>{ componentInfo }</div>
|
|
1120
1335
|
{ 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
1336
|
</div>
|
|
1123
1337
|
}
|
|
1124
1338
|
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
1125
|
-
${varName}.
|
|
1339
|
+
${varName}.getDataQuery = () => get${contentType.key}DataDocument
|
|
1126
1340
|
${varName}.getMetaData = async (contentLink, locale, client) => {
|
|
1127
1341
|
const sdk = getSdk(client);
|
|
1128
1342
|
// Add your metadata logic here
|
|
@@ -1132,24 +1346,36 @@ ${varName}.getMetaData = async (contentLink, locale, client) => {
|
|
|
1132
1346
|
export default ${varName}`,
|
|
1133
1347
|
// Template for all experience component types
|
|
1134
1348
|
experience: (contentType, varName, displayTemplate) => `import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
|
|
1135
|
-
import {
|
|
1136
|
-
import {
|
|
1349
|
+
import { ExperienceDataFragmentDoc, get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'
|
|
1350
|
+
import { getFragmentData } from '@/gql/fragment-masking'
|
|
1137
1351
|
import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
|
|
1138
1352
|
import { ${displayTemplate} } from "./displayTemplates";` : ''}
|
|
1139
|
-
import { getSdk } from "@/gql"
|
|
1353
|
+
import { getSdk } from "@/gql/client"
|
|
1140
1354
|
|
|
1141
1355
|
/**
|
|
1142
1356
|
* ${contentType.displayName}
|
|
1357
|
+
* ---
|
|
1143
1358
|
* ${contentType.description}
|
|
1359
|
+
*
|
|
1360
|
+
* This component uses the content query that is auto-generated with the Optimizely CMS Preset for GraphQL Codegen, if you need
|
|
1361
|
+
* to override this query create the file \`${contentType.key}.query.graphql\` in the same folder as this file. This file then
|
|
1362
|
+
* must include a GraphQL query with the name \`get${contentType.key} Data\`.
|
|
1363
|
+
*
|
|
1364
|
+
* [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
|
|
1144
1365
|
*/
|
|
1145
|
-
export const ${varName} : CmsComponent
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1366
|
+
export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, ctx }) => {
|
|
1367
|
+
if (ctx) ctx.editableContentIsExperience = true
|
|
1368
|
+
const composition = getFragmentData(ExperienceDataFragmentDoc, data).composition
|
|
1369
|
+
const componentName = '${contentType.displayName}'
|
|
1370
|
+
const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
|
|
1371
|
+
return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData" ctx={ctx}>
|
|
1372
|
+
<div className="font-bold italic">{ componentName }</div>
|
|
1373
|
+
<div>{ componentInfo }</div>
|
|
1374
|
+
{ composition && isNode(composition) && <OptimizelyComposition node={composition} ctx={ctx} /> }
|
|
1375
|
+
</CmsEditable>
|
|
1150
1376
|
}
|
|
1151
1377
|
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
1152
|
-
${varName}.
|
|
1378
|
+
${varName}.getDataQuery = () => get${contentType.key}DataDocument
|
|
1153
1379
|
${varName}.getMetaData = async (contentLink, locale, client) => {
|
|
1154
1380
|
const sdk = getSdk(client);
|
|
1155
1381
|
// Add your metadata logic here
|
|
@@ -1167,7 +1393,7 @@ const NextJsVisualBuilderCommand = {
|
|
|
1167
1393
|
// Prepare
|
|
1168
1394
|
const { components: basePath, _config: { debug }, force } = parseArgs(args);
|
|
1169
1395
|
// Create the fall-back node and style defintions
|
|
1170
|
-
await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node'], definitions: true });
|
|
1396
|
+
await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'], definitions: true });
|
|
1171
1397
|
createGenericNode(basePath, force, debug);
|
|
1172
1398
|
// Get all styles
|
|
1173
1399
|
const { styles } = await getStyles(createCmsClient(args), { ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'] });
|
|
@@ -1178,7 +1404,8 @@ const NextJsVisualBuilderCommand = {
|
|
|
1178
1404
|
});
|
|
1179
1405
|
// Process base styles
|
|
1180
1406
|
styles.filter(x => typeof (x.baseType) == 'string' && x.baseType.length > 0).map(styleDefinition => {
|
|
1181
|
-
const
|
|
1407
|
+
const baseType = (styleDefinition.baseType ?? '').startsWith('_') ? (styleDefinition.baseType ?? '').substring(1) : styleDefinition.baseType;
|
|
1408
|
+
const templatePath = path.join(basePath, baseType, 'styles', styleDefinition.key);
|
|
1182
1409
|
createSpecificNode(styleDefinition, templatePath, force, debug);
|
|
1183
1410
|
});
|
|
1184
1411
|
}
|
|
@@ -1198,13 +1425,30 @@ function createSpecificNode(template, templatePath, force = false, debug = false
|
|
|
1198
1425
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${template.displayName} (${template.key}) component\n`));
|
|
1199
1426
|
if (!fs.existsSync(templatePath))
|
|
1200
1427
|
fs.mkdirSync(templatePath, { recursive: true });
|
|
1428
|
+
const baseType = template.nodeType ?? template.baseType ?? 'unknown';
|
|
1201
1429
|
const displayTemplateName = getDisplayTemplateInfo(template, templatePath);
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1430
|
+
const imports = ['import { extractSettings, type CmsLayoutComponent } from "@remkoj/optimizely-cms-react/rsc";'];
|
|
1431
|
+
const componentProps = [`className="vb:${baseType} vb:${baseType}:${template.key}"`];
|
|
1432
|
+
const componentArgs = ['layoutProps', 'children'];
|
|
1433
|
+
let componentType = 'div';
|
|
1434
|
+
if (displayTemplateName)
|
|
1435
|
+
imports.push(`import { ${displayTemplateName} } from "../displayTemplates";`);
|
|
1436
|
+
if (baseType.toLowerCase() == 'section') {
|
|
1437
|
+
imports.push('import { CmsEditable } from "@remkoj/optimizely-cms-react/rsc";');
|
|
1438
|
+
componentType = 'CmsEditable';
|
|
1439
|
+
componentProps.push('{ ...editProps }');
|
|
1440
|
+
componentArgs.push('editProps');
|
|
1441
|
+
}
|
|
1442
|
+
let style = '';
|
|
1443
|
+
if (baseType == "row")
|
|
1444
|
+
style = ` style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "stretch" }}`;
|
|
1445
|
+
if (baseType == "column")
|
|
1446
|
+
style = ` style={{ display: "flex", flexDirection: "column", flexGrow: 1 }}`;
|
|
1447
|
+
const component = `${imports.join("\n")}
|
|
1204
1448
|
|
|
1205
|
-
export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({
|
|
1449
|
+
export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ ${componentArgs.join(', ')} }) => {
|
|
1206
1450
|
const layout = extractSettings(layoutProps);
|
|
1207
|
-
return (
|
|
1451
|
+
return (<${componentType} ${componentProps.join(' ')}${style}>{ children }</${componentType}>);
|
|
1208
1452
|
}
|
|
1209
1453
|
|
|
1210
1454
|
export default ${template.key};`;
|
|
@@ -1224,13 +1468,19 @@ function createGenericNode(basePath, force, debug) {
|
|
|
1224
1468
|
else if (debug)
|
|
1225
1469
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating generic node component\n`));
|
|
1226
1470
|
const nodeContent = `import { CmsEditable, type CmsLayoutComponent } from '@remkoj/optimizely-cms-react/rsc'
|
|
1471
|
+
import { type CSSProperties } from 'react';
|
|
1227
1472
|
|
|
1228
|
-
export const VisualBuilderNode : CmsLayoutComponent = ({
|
|
1473
|
+
export const VisualBuilderNode : CmsLayoutComponent = ({ editProps, layoutProps, children }) =>
|
|
1229
1474
|
{
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1475
|
+
let className = \`vb:\${layoutProps?.layoutType}\`;
|
|
1476
|
+
let style : CSSProperties | undefined = undefined;
|
|
1477
|
+
if (layoutProps?.layoutType == "row")
|
|
1478
|
+
style = {display: "flex", flexDirection: "row"}
|
|
1479
|
+
if (layoutProps?.layoutType == "column")
|
|
1480
|
+
style = {display: "flex", flexDirection: "column"}
|
|
1481
|
+
if (layoutProps && layoutProps.layoutType == "section")
|
|
1482
|
+
return <CmsEditable as="div" className={ className } {...editProps}>{ children }</CmsEditable>
|
|
1483
|
+
return <div className={ className } style={ style }>{ children }</div>
|
|
1234
1484
|
}
|
|
1235
1485
|
|
|
1236
1486
|
export default VisualBuilderNode`;
|
|
@@ -1478,7 +1728,7 @@ const NextJsCreateCommand = {
|
|
|
1478
1728
|
await Promise.all([
|
|
1479
1729
|
TypesPullCommand.handler(args),
|
|
1480
1730
|
StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: [], definitions: true }),
|
|
1481
|
-
|
|
1731
|
+
//createFragments.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
|
|
1482
1732
|
]);
|
|
1483
1733
|
process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared types, styles and fragments") + "\n");
|
|
1484
1734
|
// Then create the components
|
|
@@ -1493,6 +1743,484 @@ const NextJsCreateCommand = {
|
|
|
1493
1743
|
}
|
|
1494
1744
|
};
|
|
1495
1745
|
|
|
1746
|
+
const NextJsFragmentsCommand = {
|
|
1747
|
+
command: "nextjs:fragments",
|
|
1748
|
+
describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
|
|
1749
|
+
builder,
|
|
1750
|
+
handler: async (args, opts) => {
|
|
1751
|
+
// Prepare
|
|
1752
|
+
const { loadedContentTypes, createdTypeFolders } = opts || {};
|
|
1753
|
+
const { components: basePath, _config: { debug }, force } = parseArgs(args);
|
|
1754
|
+
const client = createCmsClient(args);
|
|
1755
|
+
const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
|
|
1756
|
+
// Start process
|
|
1757
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments\n`));
|
|
1758
|
+
const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
|
|
1759
|
+
const tracker = new Map();
|
|
1760
|
+
const dependencies = [];
|
|
1761
|
+
const updatedTypes = contentTypes.map(contentType => {
|
|
1762
|
+
const { written, propertyTypes } = createComponentFragments(contentType, getTypeFolder(typeFolders, contentType.key), force, debug, tracker);
|
|
1763
|
+
dependencies.push(...propertyTypes);
|
|
1764
|
+
return written ? contentType.key : undefined;
|
|
1765
|
+
}).filter(x => x);
|
|
1766
|
+
// Report outcome
|
|
1767
|
+
if (updatedTypes.length > 0)
|
|
1768
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
|
|
1769
|
+
else
|
|
1770
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
|
|
1771
|
+
// Start property generation process
|
|
1772
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL property fragments\n`));
|
|
1773
|
+
const generatedProps = createPropertyFragments(dependencies, allContentTypes, (contentType) => {
|
|
1774
|
+
if (!contentType.key)
|
|
1775
|
+
return undefined;
|
|
1776
|
+
let tf = getTypeFolder(typeFolders, contentType.key);
|
|
1777
|
+
if (!tf) {
|
|
1778
|
+
tf = getContentTypePaths(contentType, basePath, true, debug);
|
|
1779
|
+
typeFolders.push(tf);
|
|
1780
|
+
}
|
|
1781
|
+
return tf;
|
|
1782
|
+
}, force, debug);
|
|
1783
|
+
// Report outcome
|
|
1784
|
+
if (generatedProps.length > 0)
|
|
1785
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
|
|
1786
|
+
else
|
|
1787
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL property fragments created/updated\n`));
|
|
1788
|
+
if (!opts)
|
|
1789
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
function createComponentFragments(contentType, typePath, force, debug, propertyTracker = new Map()) {
|
|
1793
|
+
const baseQueryFile = typePath.fragmentFile;
|
|
1794
|
+
let writeFragment = true;
|
|
1795
|
+
if (fs.existsSync(baseQueryFile)) {
|
|
1796
|
+
if (force) {
|
|
1797
|
+
if (debug)
|
|
1798
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
1799
|
+
}
|
|
1800
|
+
else {
|
|
1801
|
+
if (debug)
|
|
1802
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
|
|
1803
|
+
writeFragment = false;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
else if (debug) {
|
|
1807
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
1808
|
+
}
|
|
1809
|
+
let written = false;
|
|
1810
|
+
if (writeFragment) {
|
|
1811
|
+
const fragment = GraphQLGen.buildFragment(contentType, undefined, false, propertyTracker);
|
|
1812
|
+
fs.writeFileSync(baseQueryFile, fragment);
|
|
1813
|
+
written = true;
|
|
1814
|
+
}
|
|
1815
|
+
return { written, propertyTypes: GraphQLGen.getReferencedPropertyComponents(contentType) };
|
|
1816
|
+
}
|
|
1817
|
+
function createPropertyFragments(propertyTypesList, allContentTypes, selectTypeFolder, force = false, debug = false) {
|
|
1818
|
+
const returnValue = [];
|
|
1819
|
+
for (const propertyContentTypeKey of propertyTypesList.filter((v, i, a) => a.indexOf(v, i + 1) <= i)) {
|
|
1820
|
+
// Load the ContentType definition
|
|
1821
|
+
const contentType = allContentTypes.filter(x => x.key == propertyContentTypeKey).at(0);
|
|
1822
|
+
if (!contentType) {
|
|
1823
|
+
console.warn(`🟠 The content type ${propertyContentTypeKey} has been referenced, but is not found in the Optimizely CMS instance`);
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
// Load the paths for this ContentType
|
|
1827
|
+
const depFolder = selectTypeFolder(contentType);
|
|
1828
|
+
if (!depFolder) {
|
|
1829
|
+
console.warn(`🟠 The content type ${propertyContentTypeKey} cannot be stored in the project`);
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
const propertyFragmentFile = depFolder.propertyFragmentFile;
|
|
1833
|
+
let mustWrite = true;
|
|
1834
|
+
if (fs.existsSync(propertyFragmentFile)) {
|
|
1835
|
+
if (force) {
|
|
1836
|
+
if (debug)
|
|
1837
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) property fragment\n`));
|
|
1838
|
+
}
|
|
1839
|
+
else {
|
|
1840
|
+
if (debug)
|
|
1841
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) property fragment - file already exists\n`));
|
|
1842
|
+
mustWrite = false;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
else if (debug) {
|
|
1846
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) property fragment\n`));
|
|
1847
|
+
}
|
|
1848
|
+
if (mustWrite) {
|
|
1849
|
+
const fragment = GraphQLGen.buildFragment(contentType, undefined, true);
|
|
1850
|
+
fs.writeFileSync(propertyFragmentFile, fragment);
|
|
1851
|
+
returnValue.push(contentType.key);
|
|
1852
|
+
}
|
|
1853
|
+
// Recurse down for properties that we're not yet rendering
|
|
1854
|
+
const referencedPropertyTypes = GraphQLGen.getReferencedPropertyComponents(contentType).filter(x => !propertyTypesList.includes(x));
|
|
1855
|
+
if (referencedPropertyTypes.length > 0) {
|
|
1856
|
+
if (debug)
|
|
1857
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Component property ${propertyContentTypeKey} uses components a property, recursing down\n`));
|
|
1858
|
+
const additionalProperties = createPropertyFragments(referencedPropertyTypes, allContentTypes, selectTypeFolder, force, debug);
|
|
1859
|
+
returnValue.push(...additionalProperties);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return returnValue;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const NextJsQueriesCommand = {
|
|
1866
|
+
command: "nextjs:queries",
|
|
1867
|
+
describe: "Create the GrapQL Queries to use two queries to load content",
|
|
1868
|
+
builder: yargs => {
|
|
1869
|
+
const updatedArgs = contentTypesBuilder(yargs, { ...ContentTypesArgsDefaults, baseTypes: ['page', 'experience'] });
|
|
1870
|
+
updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
1871
|
+
return updatedArgs;
|
|
1872
|
+
},
|
|
1873
|
+
handler: async (args, opts) => {
|
|
1874
|
+
// Prepare
|
|
1875
|
+
const { loadedContentTypes, createdTypeFolders } = opts || {};
|
|
1876
|
+
const { components: basePath, _config: { debug }, force } = parseArgs(args);
|
|
1877
|
+
const client = createCmsClient(args);
|
|
1878
|
+
const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
|
|
1879
|
+
// Start process
|
|
1880
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL queries\n`));
|
|
1881
|
+
const dependencies = [];
|
|
1882
|
+
const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
|
|
1883
|
+
const updatedTypes = contentTypes.map(contentType => {
|
|
1884
|
+
const typePath = getTypeFolder(typeFolders, contentType.key);
|
|
1885
|
+
const { written, propertyTypes } = createGraphQueries(contentType, typePath, force, debug);
|
|
1886
|
+
dependencies.push(...propertyTypes);
|
|
1887
|
+
return written ? contentType.key : undefined;
|
|
1888
|
+
}).filter(x => x).flat();
|
|
1889
|
+
// Report outcome
|
|
1890
|
+
if (updatedTypes.length > 0)
|
|
1891
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL queries for ${updatedTypes.join(', ')}\n`));
|
|
1892
|
+
else
|
|
1893
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL queries created/updated\n`));
|
|
1894
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL property fragments\n`));
|
|
1895
|
+
const generatedProps = createPropertyFragments(dependencies, allContentTypes, (contentType) => {
|
|
1896
|
+
if (!contentType.key)
|
|
1897
|
+
return undefined;
|
|
1898
|
+
let tf = getTypeFolder(typeFolders, contentType.key);
|
|
1899
|
+
if (!tf) {
|
|
1900
|
+
tf = getContentTypePaths(contentType, basePath, true, debug);
|
|
1901
|
+
typeFolders.push(tf);
|
|
1902
|
+
}
|
|
1903
|
+
return tf;
|
|
1904
|
+
}, force, debug);
|
|
1905
|
+
// Report outcome
|
|
1906
|
+
if (generatedProps.length > 0)
|
|
1907
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
|
|
1908
|
+
else
|
|
1909
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL property fragments created/updated\n`));
|
|
1910
|
+
if (!opts)
|
|
1911
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
function createGraphQueries(contentType, typePath, force, debug) {
|
|
1915
|
+
const baseQueryFile = typePath.queryFile;
|
|
1916
|
+
let mustWrite = true;
|
|
1917
|
+
if (fs.existsSync(baseQueryFile)) {
|
|
1918
|
+
if (force) {
|
|
1919
|
+
if (debug)
|
|
1920
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
1921
|
+
}
|
|
1922
|
+
else {
|
|
1923
|
+
if (debug)
|
|
1924
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
|
|
1925
|
+
mustWrite = false;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
else if (debug) {
|
|
1929
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
1930
|
+
}
|
|
1931
|
+
if (mustWrite) {
|
|
1932
|
+
const fragment = GraphQLGen.buildGetQuery(contentType, undefined, new Map());
|
|
1933
|
+
fs.writeFileSync(baseQueryFile, fragment);
|
|
1934
|
+
}
|
|
1935
|
+
return {
|
|
1936
|
+
written: mustWrite,
|
|
1937
|
+
propertyTypes: GraphQLGen.getReferencedPropertyComponents(contentType)
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const SchemaDownloadCommand = {
|
|
1942
|
+
command: "schema:download",
|
|
1943
|
+
describe: "Create JSON schema files for the specified types, which you can use to configure JSON validation in your IDE of choice.",
|
|
1944
|
+
builder: (yargs) => {
|
|
1945
|
+
const newYargs = yargs;
|
|
1946
|
+
newYargs.option('schemaDir', { alias: 'd', description: "The schema path relative to your project root directory", string: true, type: 'string', demandOption: false, default: './.schema' });
|
|
1947
|
+
newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] });
|
|
1948
|
+
newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
1949
|
+
return newYargs;
|
|
1950
|
+
},
|
|
1951
|
+
async handler(args, opts) {
|
|
1952
|
+
const { _config: cmsConfig, path: projectPath, force, schemaDir, schemas } = parseArgs(args);
|
|
1953
|
+
const client = createCmsClient(cmsConfig);
|
|
1954
|
+
// Loading Schema
|
|
1955
|
+
const schemaDefs = await loadSchema(client, schemas);
|
|
1956
|
+
const fullSchemaDir = path.normalize(path.join(projectPath, schemaDir));
|
|
1957
|
+
const relativeSchemaDir = path.relative(projectPath, fullSchemaDir);
|
|
1958
|
+
process.stdout.write(`\n${figures.arrowRight} Validating schema folder ${relativeSchemaDir}\n`);
|
|
1959
|
+
const schemaDirInfo = await fsAsync.stat(fullSchemaDir).catch((e) => { if (e.code == 'ENOENT')
|
|
1960
|
+
return undefined;
|
|
1961
|
+
else
|
|
1962
|
+
throw e; });
|
|
1963
|
+
if (!schemaDirInfo) {
|
|
1964
|
+
await fsAsync.mkdir(fullSchemaDir, { recursive: true });
|
|
1965
|
+
}
|
|
1966
|
+
else if (!schemaDirInfo.isDirectory()) {
|
|
1967
|
+
process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] The schema directory exists, but is not a directory (${relativeSchemaDir})\n`)));
|
|
1968
|
+
process.exit(1);
|
|
1969
|
+
}
|
|
1970
|
+
process.stdout.write(chalk.greenBright(` ${figures.tick} `));
|
|
1971
|
+
process.stdout.write(`Schema directory exists\n`);
|
|
1972
|
+
process.stdout.write(`\n${figures.arrowRight} Writing schema files\n`);
|
|
1973
|
+
for (const schema of schemaDefs) {
|
|
1974
|
+
const schemaFile = path.normalize(path.join(fullSchemaDir, `${schema.title.toLowerCase()}.schema.json`));
|
|
1975
|
+
const relativePath = path.relative(projectPath, schemaFile);
|
|
1976
|
+
try {
|
|
1977
|
+
await fsAsync.writeFile(schemaFile, JSON.stringify(schema.schema, undefined, 2), {
|
|
1978
|
+
encoding: "utf-8",
|
|
1979
|
+
flag: force ? 'w' : 'wx'
|
|
1980
|
+
});
|
|
1981
|
+
process.stdout.write(chalk.greenBright(` ${figures.tick} `));
|
|
1982
|
+
process.stdout.write(`Written the ${schema.title} schema to ${relativePath}\n`);
|
|
1983
|
+
}
|
|
1984
|
+
catch (err) {
|
|
1985
|
+
if (!force && err.code === 'EEXIST') {
|
|
1986
|
+
process.stdout.write(chalk.yellowBright(` ${figures.info} `));
|
|
1987
|
+
process.stdout.write(`The schema ${schema.title} already exists in ${relativePath}, run with -f to overwrite\n`);
|
|
1988
|
+
}
|
|
1989
|
+
else
|
|
1990
|
+
process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] ${err.toString()}\n`)));
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
const SchemaListCommand = {
|
|
1997
|
+
command: "schema:list",
|
|
1998
|
+
describe: "List all schema's that are available within the SaaS CMS instance",
|
|
1999
|
+
builder: (yargs) => {
|
|
2000
|
+
const newYargs = yargs;
|
|
2001
|
+
return newYargs;
|
|
2002
|
+
},
|
|
2003
|
+
async handler(args, opts) {
|
|
2004
|
+
const client = createCmsClient(args);
|
|
2005
|
+
process.stdout.write(`\n${figures.arrowRight} Downloading OpenAPI Specification\n`);
|
|
2006
|
+
const spec = await client.getOpenApiSpec();
|
|
2007
|
+
process.stdout.write(chalk.greenBright(` ${figures.tick} `));
|
|
2008
|
+
process.stdout.write(`The ${spec.info.title} (Version: ${spec.info.version}) specification has been downloaded.\n`);
|
|
2009
|
+
const specSchemas = (spec?.components?.schemas ?? {});
|
|
2010
|
+
const schemas = new Table({
|
|
2011
|
+
head: [
|
|
2012
|
+
chalk.yellow(chalk.bold("Key")),
|
|
2013
|
+
chalk.yellow(chalk.bold("Description"))
|
|
2014
|
+
],
|
|
2015
|
+
colWidths: [35, 60],
|
|
2016
|
+
colAligns: ["left", "left"],
|
|
2017
|
+
wordWrap: true,
|
|
2018
|
+
wrapOnWordBoundary: true
|
|
2019
|
+
});
|
|
2020
|
+
for (const key in specSchemas)
|
|
2021
|
+
if (key) {
|
|
2022
|
+
schemas.push([key, specSchemas[key].description?.replaceAll(/\s+/g, ' ') ?? `-`]);
|
|
2023
|
+
}
|
|
2024
|
+
process.stdout.write("\n" + schemas.toString() + "\n");
|
|
2025
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
const deepmerge = createDeepMerge();
|
|
2030
|
+
const SchemaValidateCommand = {
|
|
2031
|
+
command: "schema:validate",
|
|
2032
|
+
describe: "Validate the opti-type.json & opti-style.json files",
|
|
2033
|
+
builder: (yargs) => {
|
|
2034
|
+
const newYargs = yargs;
|
|
2035
|
+
//newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] })
|
|
2036
|
+
//newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false })
|
|
2037
|
+
//newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true })
|
|
2038
|
+
return newYargs;
|
|
2039
|
+
},
|
|
2040
|
+
handler: async (args) => {
|
|
2041
|
+
const projectPath = args.path;
|
|
2042
|
+
const client = createCmsClient(args);
|
|
2043
|
+
const schemas = await loadSchema(client, ['DisplayTemplate', 'ContentType']);
|
|
2044
|
+
process.stdout.write(``);
|
|
2045
|
+
const styleSchema = schemas.find(x => x.title == 'DisplayTemplate')?.schema;
|
|
2046
|
+
const typeSchema = schemas.find(x => x.title == 'ContentType')?.schema;
|
|
2047
|
+
const [styleValidator, typeValidator] = await Promise.all([getValidator(styleSchema), getValidator(typeSchema)]);
|
|
2048
|
+
process.stdout.write(`\n${figures.arrowRight} Validating display templates (*.opti-style.json) and content types (*.opti-type.json).\n`);
|
|
2049
|
+
const patterns = ["./**/*.opti-type.json", "./**/*.opti-style.json"];
|
|
2050
|
+
const globMatches = globIterate(patterns, { cwd: projectPath, absolute: true });
|
|
2051
|
+
let allOk = true;
|
|
2052
|
+
for await (const fileMatch of globMatches) {
|
|
2053
|
+
try {
|
|
2054
|
+
const content = JSON.parse(fs.readFileSync(fileMatch).toString());
|
|
2055
|
+
let errors = undefined;
|
|
2056
|
+
if (fileMatch.endsWith('.opti-type.json') && !typeValidator(content)) {
|
|
2057
|
+
errors = typeValidator.errors;
|
|
2058
|
+
}
|
|
2059
|
+
else if (fileMatch.endsWith('.opti-style.json') && !styleValidator(content)) {
|
|
2060
|
+
errors = styleValidator.errors;
|
|
2061
|
+
}
|
|
2062
|
+
if (errors) {
|
|
2063
|
+
allOk = false;
|
|
2064
|
+
process.stdout.write(chalk.redBright(chalk.bold(`\n${figures.cross} ${path.relative(projectPath, fileMatch)}\n`)));
|
|
2065
|
+
const groupedErrors = groupErrors(errors);
|
|
2066
|
+
for (const instancepath in groupedErrors) {
|
|
2067
|
+
for (const key in groupedErrors[instancepath]) {
|
|
2068
|
+
const errorList = groupedErrors[instancepath][key].filter((v, i, a) => a.findIndex(pv => deepEqual(v, pv)) === i);
|
|
2069
|
+
for (const error of errorList) {
|
|
2070
|
+
process.stdout.write(` Node ${instancepath} ${error.message}\n`);
|
|
2071
|
+
switch (error.keyword) {
|
|
2072
|
+
case 'enum':
|
|
2073
|
+
process.stdout.write(` Allowed values: ${error.params.allowedValues.map((x) => x.toString()).join(', ')}\n`);
|
|
2074
|
+
break;
|
|
2075
|
+
case 'oneOf':
|
|
2076
|
+
const matching = Array.isArray(error.params.passingSchemas) ? error.params.passingSchemas : [];
|
|
2077
|
+
if (matching.length === 0)
|
|
2078
|
+
process.stdout.write(` Currently none matching\n`);
|
|
2079
|
+
else
|
|
2080
|
+
process.stdout.write(` Currently matching ${matching.map((x) => x.toString()).join(", ")}\n`);
|
|
2081
|
+
break;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
catch (e) {
|
|
2089
|
+
allOk = false;
|
|
2090
|
+
process.stdout.write(chalk.redBright(chalk.bold(`${figures.cross} ${path.relative(projectPath, fileMatch)}\n`)));
|
|
2091
|
+
process.stdout.write(` ${e.message}\n`);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
process.stdout.write(`\n`);
|
|
2095
|
+
if (allOk)
|
|
2096
|
+
process.stdout.write(chalk.greenBright(`${figures.tick} All types & styles match the schema\n`));
|
|
2097
|
+
else {
|
|
2098
|
+
process.stdout.write(chalk.redBright(`${figures.cross} Some types & styles contain errors\n`));
|
|
2099
|
+
process.exit(1);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
function groupErrors(errors) {
|
|
2104
|
+
const errorrsByInstance = groupErrorsByPath(errors);
|
|
2105
|
+
const instancePaths = Object.getOwnPropertyNames(errorrsByInstance);
|
|
2106
|
+
return instancePaths.reduce((previous, current) => {
|
|
2107
|
+
previous[current] = groupErrorsByKeyword(errorrsByInstance[current]);
|
|
2108
|
+
return previous;
|
|
2109
|
+
}, {});
|
|
2110
|
+
}
|
|
2111
|
+
function groupErrorsByPath(errors) {
|
|
2112
|
+
return errors.reduce((previous, current) => {
|
|
2113
|
+
const instancePath = current.instancePath == "" ? "[ROOT]" : current.instancePath;
|
|
2114
|
+
const partial = {};
|
|
2115
|
+
partial[instancePath] = [current];
|
|
2116
|
+
return deepmerge(previous, partial);
|
|
2117
|
+
}, {});
|
|
2118
|
+
}
|
|
2119
|
+
function groupErrorsByKeyword(errors) {
|
|
2120
|
+
const toMerge = errors.reduce((previous, current) => {
|
|
2121
|
+
const keyword = current.keyword;
|
|
2122
|
+
const partial = {};
|
|
2123
|
+
partial[keyword] = [current];
|
|
2124
|
+
return deepmerge(previous, partial);
|
|
2125
|
+
}, {});
|
|
2126
|
+
// Enum errors must be merged
|
|
2127
|
+
if (toMerge.enum)
|
|
2128
|
+
toMerge.enum = [toMerge.enum.reduce((prev, current) => prev ? deepmerge(prev, current) : current, undefined)];
|
|
2129
|
+
return toMerge;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
const SchemaVsCodeCommand = {
|
|
2133
|
+
command: "schema:vscode",
|
|
2134
|
+
describe: "Configure schema validation for opti-type.json & opti-style.json files by VSCode in the current project folder",
|
|
2135
|
+
builder: (yargs) => {
|
|
2136
|
+
const newYargs = yargs;
|
|
2137
|
+
//newYargs.option('schemas', { alias: 's', description: "The schema's to download", array: true, type: 'string', demandOption: false, default: ['DisplayTemplate', 'ContentType'] })
|
|
2138
|
+
//newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false })
|
|
2139
|
+
//newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true })
|
|
2140
|
+
return newYargs;
|
|
2141
|
+
},
|
|
2142
|
+
handler: async (args) => {
|
|
2143
|
+
const projectPath = args.path;
|
|
2144
|
+
const client = createCmsClient(args);
|
|
2145
|
+
const schemas = await loadSchema(client, ['DisplayTemplate', 'ContentType']);
|
|
2146
|
+
process.stdout.write(`\n${figures.arrowRight} Writing schema files\n`);
|
|
2147
|
+
for (const schema of schemas) {
|
|
2148
|
+
const schemaFile = path.join(projectPath, '.vscode', `${schema.title.toLowerCase()}.schema.json`);
|
|
2149
|
+
void await writeFileAsync(schemaFile, JSON.stringify(schema.schema, undefined, 2));
|
|
2150
|
+
process.stdout.write(` ${figures.tick} Written the ${schema.title} schema to ${path.relative(projectPath, schemaFile)}\n`);
|
|
2151
|
+
}
|
|
2152
|
+
process.stdout.write(`\n${figures.arrowRight} Updating folder settings\n`);
|
|
2153
|
+
const settingsFile = path.join(projectPath, '.vscode', 'settings.json');
|
|
2154
|
+
const settings = (await readJsonFileAsync(settingsFile).catch(() => undefined)) ?? {};
|
|
2155
|
+
settings['json.validate.enable'] = true;
|
|
2156
|
+
settings['json.schemaDownload.enable'] = true;
|
|
2157
|
+
settings['json.schemas'] = settings['json.schemas'] || [];
|
|
2158
|
+
const hasContentType = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.opti-type.json')) >= 0) >= 0;
|
|
2159
|
+
const hasStyleDefinition = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.opti-style.json')) >= 0) >= 0;
|
|
2160
|
+
const hasSchemaType = settings['json.schemas'].findIndex(v => v.fileMatch.findIndex(fm => fm.endsWith('.schema.json')) >= 0) >= 0;
|
|
2161
|
+
if (!hasContentType)
|
|
2162
|
+
settings['json.schemas'].push({
|
|
2163
|
+
"fileMatch": [
|
|
2164
|
+
"**/*.opti-type.json"
|
|
2165
|
+
],
|
|
2166
|
+
"url": "./.vscode/contenttype.schema.json"
|
|
2167
|
+
});
|
|
2168
|
+
if (!hasStyleDefinition)
|
|
2169
|
+
settings['json.schemas'].push({
|
|
2170
|
+
"fileMatch": [
|
|
2171
|
+
"**/*.opti-style.json"
|
|
2172
|
+
],
|
|
2173
|
+
"url": "./.vscode/displaytemplate.schema.json"
|
|
2174
|
+
});
|
|
2175
|
+
if (!hasSchemaType)
|
|
2176
|
+
settings['json.schemas'].push({
|
|
2177
|
+
"fileMatch": [
|
|
2178
|
+
"**/*.schema.json"
|
|
2179
|
+
],
|
|
2180
|
+
"url": "https://json-schema.org/draft-07/schema"
|
|
2181
|
+
});
|
|
2182
|
+
if (settings['json.schemas'].length == 0)
|
|
2183
|
+
delete settings['json.schemas'];
|
|
2184
|
+
await writeFileAsync(settingsFile, JSON.stringify(settings, undefined, 2));
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
function readJsonFileAsync(path) {
|
|
2188
|
+
return new Promise((resolve, reject) => fs.readFile(path, (err, data) => {
|
|
2189
|
+
if (err) {
|
|
2190
|
+
if (err?.code === 'ENOENT')
|
|
2191
|
+
resolve(undefined);
|
|
2192
|
+
else
|
|
2193
|
+
reject(err);
|
|
2194
|
+
}
|
|
2195
|
+
else {
|
|
2196
|
+
try {
|
|
2197
|
+
const d = data.toString();
|
|
2198
|
+
resolve(JSON.parse(d));
|
|
2199
|
+
}
|
|
2200
|
+
catch (e) {
|
|
2201
|
+
reject(e);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}));
|
|
2205
|
+
}
|
|
2206
|
+
function writeFileAsync(path, data) {
|
|
2207
|
+
return new Promise((resolve, reject) => {
|
|
2208
|
+
fs.writeFile(path, data, (err) => {
|
|
2209
|
+
if (err)
|
|
2210
|
+
reject(err);
|
|
2211
|
+
else
|
|
2212
|
+
resolve();
|
|
2213
|
+
});
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
var version = "6.0.0-pre11";
|
|
2218
|
+
var name = "opti-cms";
|
|
2219
|
+
var APP = {
|
|
2220
|
+
version: version,
|
|
2221
|
+
name: name
|
|
2222
|
+
};
|
|
2223
|
+
|
|
1496
2224
|
const CmsVersionCommand = {
|
|
1497
2225
|
command: "cms:version",
|
|
1498
2226
|
aliases: "$0",
|
|
@@ -1511,19 +2239,25 @@ const CmsVersionCommand = {
|
|
|
1511
2239
|
throw error;
|
|
1512
2240
|
}
|
|
1513
2241
|
});
|
|
2242
|
+
const hasPreview2Info = versionInfo.results.preview2Data?.baseUrl ? true : false;
|
|
1514
2243
|
const info = new Table({
|
|
1515
2244
|
head: [
|
|
1516
2245
|
chalk.yellow(chalk.bold("Component")),
|
|
1517
2246
|
chalk.yellow(chalk.bold("Version"))
|
|
1518
2247
|
],
|
|
1519
|
-
colWidths: [
|
|
2248
|
+
colWidths: [30, 60],
|
|
1520
2249
|
colAligns: ["left", "left"]
|
|
1521
2250
|
});
|
|
1522
|
-
info.push(["
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
info.push(["
|
|
1526
|
-
info.push(["Service", versionInfo.
|
|
2251
|
+
info.push(["Base URL", versionInfo.baseUrl ?? client.cmsUrl.href]);
|
|
2252
|
+
if (hasPreview2Info)
|
|
2253
|
+
info.push(["Base URL (Preview 2)", versionInfo.results.preview2Data?.baseUrl ?? 'n/a']);
|
|
2254
|
+
info.push(["Client API", client.apiVersion]);
|
|
2255
|
+
info.push(["Service API", versionInfo.apiVersion]);
|
|
2256
|
+
if (hasPreview2Info)
|
|
2257
|
+
info.push(["Service API (Preview 2)", versionInfo.results.preview2Data?.apiVersion ?? 'n/a']);
|
|
2258
|
+
info.push(["CMS Build", versionInfo.cmsVersion]);
|
|
2259
|
+
info.push(["Service Build", versionInfo.serviceVersion]);
|
|
2260
|
+
info.push(["SDK", APP.version]);
|
|
1527
2261
|
process.stdout.write(info.toString() + "\n");
|
|
1528
2262
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Optimizely CMS Status: ${versionInfo.status}\n`));
|
|
1529
2263
|
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
@@ -1643,7 +2377,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
|
|
|
1643
2377
|
process.stderr.write(chalk.redBright(` ${figures.cross}The ContentItem ${key} is a reserved item, skipping deletion\n`));
|
|
1644
2378
|
return removedCount;
|
|
1645
2379
|
}
|
|
1646
|
-
const deleteResult = await client.
|
|
2380
|
+
const deleteResult = await client.preview2ContentDelete({ path: { key }, query: { permanent: true } }).then(r => r.key).catch((e) => {
|
|
1647
2381
|
if (e.status == 404)
|
|
1648
2382
|
return key;
|
|
1649
2383
|
throw e;
|
|
@@ -1669,8 +2403,7 @@ async function resetSystemTypes(client) {
|
|
|
1669
2403
|
body: {
|
|
1670
2404
|
key: systemType,
|
|
1671
2405
|
properties: {}
|
|
1672
|
-
}
|
|
1673
|
-
query: { ignoreDataLossWarnings: true }
|
|
2406
|
+
}
|
|
1674
2407
|
}).catch((e) => e.status == 404 ? undefined : null);
|
|
1675
2408
|
if (newType)
|
|
1676
2409
|
resetTypes++;
|
|
@@ -1771,7 +2504,7 @@ async function getAllTypes(client, batchSize = 100) {
|
|
|
1771
2504
|
return actualItems;
|
|
1772
2505
|
}
|
|
1773
2506
|
async function getAllAssets(client, parentKey, batchSize = 100) {
|
|
1774
|
-
const items = await client.
|
|
2507
|
+
const items = await client.preview2ContentListAssets({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
|
|
1775
2508
|
if (e.status == 404) {
|
|
1776
2509
|
return {
|
|
1777
2510
|
items: [],
|
|
@@ -1786,7 +2519,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
|
|
|
1786
2519
|
const actualItems = items.items ?? [];
|
|
1787
2520
|
const pageCount = Math.ceil(totalItemCount / pageSize);
|
|
1788
2521
|
for (let pageNr = 1; pageNr < pageCount; pageNr++) {
|
|
1789
|
-
const pageData = await client.
|
|
2522
|
+
const pageData = await client.preview2ContentListAssets({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
|
|
1790
2523
|
if (e.status == 404) {
|
|
1791
2524
|
return {
|
|
1792
2525
|
items: [],
|
|
@@ -1801,7 +2534,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
|
|
|
1801
2534
|
return actualItems;
|
|
1802
2535
|
}
|
|
1803
2536
|
async function getAllItems(client, parentKey, batchSize = 100) {
|
|
1804
|
-
const items = await client.
|
|
2537
|
+
const items = await client.preview2ContentListItems({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
|
|
1805
2538
|
if (e.status == 404) {
|
|
1806
2539
|
return {
|
|
1807
2540
|
items: [],
|
|
@@ -1816,7 +2549,7 @@ async function getAllItems(client, parentKey, batchSize = 100) {
|
|
|
1816
2549
|
const actualItems = items.items ?? [];
|
|
1817
2550
|
const pageCount = Math.ceil(totalItemCount / pageSize);
|
|
1818
2551
|
for (let pageNr = 1; pageNr < pageCount; pageNr++) {
|
|
1819
|
-
const pageData = await client.
|
|
2552
|
+
const pageData = await client.preview2ContentListItems({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
|
|
1820
2553
|
if (e.status == 404) {
|
|
1821
2554
|
return {
|
|
1822
2555
|
items: [],
|
|
@@ -1833,28 +2566,26 @@ async function getAllItems(client, parentKey, batchSize = 100) {
|
|
|
1833
2566
|
|
|
1834
2567
|
// Style processing
|
|
1835
2568
|
const commands = [
|
|
2569
|
+
CmsResetCommand,
|
|
2570
|
+
CmsVersionCommand,
|
|
2571
|
+
NextJsComponentsCommand,
|
|
2572
|
+
NextJsCreateCommand,
|
|
2573
|
+
NextJsFactoryCommand,
|
|
2574
|
+
NextJsFragmentsCommand,
|
|
2575
|
+
NextJsQueriesCommand,
|
|
2576
|
+
NextJsVisualBuilderCommand,
|
|
2577
|
+
SchemaDownloadCommand,
|
|
2578
|
+
SchemaListCommand,
|
|
2579
|
+
SchemaValidateCommand,
|
|
2580
|
+
SchemaVsCodeCommand,
|
|
1836
2581
|
StylesCreateCommand,
|
|
1837
|
-
StylesPushCommand,
|
|
1838
2582
|
StylesListCommand,
|
|
1839
2583
|
StylesPullCommand,
|
|
2584
|
+
StylesPushCommand,
|
|
1840
2585
|
TypesPullCommand,
|
|
1841
|
-
TypesPushCommand
|
|
1842
|
-
NextJsCreateCommand,
|
|
1843
|
-
NextJsQueriesCommand,
|
|
1844
|
-
NextJsComponentsCommand,
|
|
1845
|
-
NextJsVisualBuilderCommand,
|
|
1846
|
-
NextJsFactoryCommand,
|
|
1847
|
-
CmsResetCommand,
|
|
1848
|
-
CmsVersionCommand
|
|
2586
|
+
TypesPushCommand
|
|
1849
2587
|
];
|
|
1850
2588
|
|
|
1851
|
-
var version = "6.0.0-pre1";
|
|
1852
|
-
var name = "opti-cms";
|
|
1853
|
-
var APP = {
|
|
1854
|
-
version: version,
|
|
1855
|
-
name: name
|
|
1856
|
-
};
|
|
1857
|
-
|
|
1858
2589
|
async function main() {
|
|
1859
2590
|
const envFiles = prepare();
|
|
1860
2591
|
const app = createOptiCmsApp(APP.name, APP.version, undefined, envFiles);
|
|
@@ -1864,7 +2595,7 @@ async function main() {
|
|
|
1864
2595
|
}
|
|
1865
2596
|
catch {
|
|
1866
2597
|
//We're ignoring error here, as yargs will already generate the "nice output" for it
|
|
1867
|
-
//console.log
|
|
2598
|
+
//console.log('Caught error')
|
|
1868
2599
|
}
|
|
1869
2600
|
}
|
|
1870
2601
|
main();
|