@remkoj/optimizely-cms-cli 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +857 -295
- package/dist/index.js.map +1 -1
- package/package.json +10 -9
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from 'node:fs';
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import figures from 'figures';
|
|
10
10
|
import Table from 'cli-table3';
|
|
11
|
+
import { input, select, confirm } from '@inquirer/prompts';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Prepare the application context, by parsing the .env files in the main
|
|
@@ -22,10 +23,18 @@ function prepare() {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function createOptiCmsApp(scriptName, version, epilogue) {
|
|
25
|
-
|
|
26
|
+
let config;
|
|
27
|
+
try {
|
|
28
|
+
config = getCmsIntegrationApiConfigFromEnvironment();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
config = {
|
|
32
|
+
base: new URL('https://example.cms.optimizely.com')
|
|
33
|
+
};
|
|
34
|
+
}
|
|
26
35
|
return yargs(process.argv)
|
|
27
36
|
.scriptName(scriptName)
|
|
28
|
-
.version(version
|
|
37
|
+
.version(version)
|
|
29
38
|
.usage('$0 <cmd> [args]')
|
|
30
39
|
.option("path", { alias: "p", description: "Application root folder", string: true, type: "string", demandOption: false, default: process.cwd() })
|
|
31
40
|
.option("components", { alias: "c", description: "Path to components folder", string: true, type: "string", demandOption: false, default: "./src/components/cms" })
|
|
@@ -55,11 +64,20 @@ function isDemanded(value) {
|
|
|
55
64
|
* @param param0 The parameters from the Command Line application
|
|
56
65
|
* @returns The arguments, with the Optimizely CMS Client parameters transformed into a configuration object
|
|
57
66
|
*/
|
|
58
|
-
function parseArgs({ client_id, client_secret, cms_url, user_id, verbose, path: argsPath, components: argsComponents, ...args }) {
|
|
67
|
+
function parseArgs({ client_id, client_secret, cms_url, user_id, verbose, path: argsPath, components: argsComponents, ...args }, createBasePath = true) {
|
|
59
68
|
const appPath = path.isAbsolute(argsPath) ? argsPath : path.normalize(path.join(process.cwd(), argsPath));
|
|
60
69
|
const componentDir = path.normalize(path.join(argsPath, argsComponents));
|
|
61
70
|
if (!componentDir.startsWith(argsPath))
|
|
62
71
|
throw new Error(`The component directory ${componentDir} is outside the application directory (${appPath})`);
|
|
72
|
+
if (createBasePath && !fs.existsSync(componentDir)) {
|
|
73
|
+
if (verbose)
|
|
74
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} The components directory ${componentDir} does not exist yet, it will be created\n`));
|
|
75
|
+
fs.mkdirSync(componentDir, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
if (!fs.statSync(componentDir).isDirectory()) {
|
|
78
|
+
process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The components path ${componentDir} exists, but is not a folder\n`));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
63
81
|
return {
|
|
64
82
|
_config: {
|
|
65
83
|
base: cms_url,
|
|
@@ -143,26 +161,167 @@ function isNotNullOrUndefined(i) {
|
|
|
143
161
|
return i ? true : false;
|
|
144
162
|
}
|
|
145
163
|
|
|
164
|
+
function createCmsClient(args) {
|
|
165
|
+
const { _config: cfg } = parseArgs(args);
|
|
166
|
+
const client = createClient(cfg);
|
|
167
|
+
if (cfg.debug)
|
|
168
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Connecting to ${cfg.base.href} as ${cfg.actAs ?? cfg.clientId}\n`));
|
|
169
|
+
return client;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const contentTypesBuilder = yargs => {
|
|
173
|
+
yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
|
|
174
|
+
yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
|
|
175
|
+
yargs.option("baseTypes", { alias: 'b', description: "Select only these base types", string: true, type: 'array', demandOption: false, default: [] });
|
|
176
|
+
yargs.option("types", { alias: 't', description: "Select only these types", string: true, type: 'array', demandOption: false, default: [] });
|
|
177
|
+
return yargs;
|
|
178
|
+
};
|
|
179
|
+
async function getContentTypes(client, args, pageSize = 100, allowSystem = false) {
|
|
180
|
+
const { _config: cfg, excludeBaseTypes, excludeTypes, baseTypes, types } = parseArgs(args);
|
|
181
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
|
|
182
|
+
if (cfg.debug)
|
|
183
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
|
|
184
|
+
let resultsPage = await client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
|
|
185
|
+
const results = resultsPage.items ?? [];
|
|
186
|
+
let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
|
|
187
|
+
while (pagesRemaining > 0 && results.length < resultsPage.totalItemCount) {
|
|
188
|
+
if (cfg.debug)
|
|
189
|
+
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`));
|
|
190
|
+
resultsPage = await client.contentTypes.contentTypesList(undefined, undefined, resultsPage.pageIndex + 1, resultsPage.pageSize);
|
|
191
|
+
results.push(...resultsPage.items);
|
|
192
|
+
pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
|
|
193
|
+
}
|
|
194
|
+
if (cfg.debug) {
|
|
195
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
|
|
196
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Content-Types based upon arguments\n`));
|
|
197
|
+
}
|
|
198
|
+
const contentTypes = results.filter(data => {
|
|
199
|
+
// Remove items based upon filters
|
|
200
|
+
const keepType = (!excludeBaseTypes.includes(data.baseType)) &&
|
|
201
|
+
(!excludeTypes.includes(data.key)) &&
|
|
202
|
+
(baseTypes.length == 0 || baseTypes.includes(data.baseType)) &&
|
|
203
|
+
(types.length == 0 || types.includes(data.key));
|
|
204
|
+
if (!keepType) {
|
|
205
|
+
if (cfg.debug)
|
|
206
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${data.key} due to applied filters\n`));
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
// Skip system types if desired
|
|
210
|
+
if (data.source == 'system' && !allowSystem) {
|
|
211
|
+
if (cfg.debug)
|
|
212
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${data.key} due to it being a system type\n`));
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
});
|
|
217
|
+
if (cfg.debug)
|
|
218
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${contentTypes.length} items\n`));
|
|
219
|
+
return {
|
|
220
|
+
all: results,
|
|
221
|
+
contentTypes
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const stylesBuilder = yargs => {
|
|
226
|
+
const newArgs = contentTypesBuilder(yargs);
|
|
227
|
+
newArgs.option('excludeNodeTypes', { alias: 'ent', description: "Exclude these node types", string: true, type: 'array', demandOption: false, default: [] });
|
|
228
|
+
newArgs.option('excludeTemplates', { alias: 'et', description: "Exclude these templates", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
|
|
229
|
+
newArgs.option("nodes", { alias: 'n', description: "Select only these node types", string: true, type: 'array', demandOption: false, default: [] });
|
|
230
|
+
newArgs.option("templates", { alias: 'd', description: "Select only these templates", string: true, type: 'array', demandOption: false, default: [] });
|
|
231
|
+
newArgs.option("templateTypes", { alias: 'tt', description: "Select only these template types", choices: ['node', 'base', 'component'], type: 'array', demandOption: false, default: [] });
|
|
232
|
+
return newArgs;
|
|
233
|
+
};
|
|
234
|
+
async function getStyles(client, args, pageSize = 100) {
|
|
235
|
+
const { _config: cfg, excludeBaseTypes, excludeTypes, excludeNodeTypes, excludeTemplates, baseTypes, types, nodes, templates, templateTypes } = parseArgs(args);
|
|
236
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Style-Definitions from Optimizely CMS\n`));
|
|
237
|
+
if (cfg.debug)
|
|
238
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
|
|
239
|
+
let resultsPage = await client.displayTemplates.displayTemplatesList(0, pageSize);
|
|
240
|
+
const results = resultsPage.items ?? [];
|
|
241
|
+
let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
|
|
242
|
+
while (pagesRemaining > 0 && results.length < resultsPage.totalItemCount) {
|
|
243
|
+
if (cfg.debug)
|
|
244
|
+
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`));
|
|
245
|
+
resultsPage = await client.displayTemplates.displayTemplatesList(resultsPage.pageIndex + 1, resultsPage.pageSize);
|
|
246
|
+
results.push(...resultsPage.items);
|
|
247
|
+
pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
|
|
248
|
+
}
|
|
249
|
+
if (cfg.debug) {
|
|
250
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Style-Definitions from Optimizely CMS\n`));
|
|
251
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Filtering Style-Definitions based upon arguments\n`));
|
|
252
|
+
}
|
|
253
|
+
const styles = results.filter(data => {
|
|
254
|
+
if (isExcluded(data.key, excludeTemplates, templates)) {
|
|
255
|
+
if (cfg.debug)
|
|
256
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style defintion key filtering active\n`));
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
const templateType = data.baseType ? 'base' : data.nodeType ? 'node' : data.contentType ? 'component' : 'unknown';
|
|
260
|
+
if (isExcluded(templateType, [], templateTypes)) {
|
|
261
|
+
if (cfg.debug)
|
|
262
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style type filtering is active\n`));
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
if (data.baseType && isExcluded(data.baseType, excludeBaseTypes, baseTypes)) {
|
|
266
|
+
if (cfg.debug)
|
|
267
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is defined at base type level and base type filtering is active\n`));
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
if (data.contentType && isExcluded(data.contentType, excludeTypes, types)) {
|
|
271
|
+
if (cfg.debug)
|
|
272
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is defined at component type level and component type filtering is active\n`));
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
if (data.nodeType && isExcluded(data.nodeType, excludeNodeTypes, nodes)) {
|
|
276
|
+
if (cfg.debug)
|
|
277
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${data.key} - Style is defined at node type level and node type filtering is active\n`));
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
});
|
|
282
|
+
if (cfg.debug)
|
|
283
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${styles.length} items\n`));
|
|
284
|
+
return {
|
|
285
|
+
all: results,
|
|
286
|
+
styles
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function isExcluded(value, exclusions, inclusions) {
|
|
290
|
+
if (value == undefined || value == null)
|
|
291
|
+
return false;
|
|
292
|
+
return exclusions.includes(value) || (inclusions.length > 0 && !inclusions.includes(value));
|
|
293
|
+
}
|
|
294
|
+
async function getStyleFilePath(definition, opts) {
|
|
295
|
+
if (definition.nodeType)
|
|
296
|
+
return `nodes/${definition.nodeType}/${definition.key}/${definition.key}.opti-style.json`;
|
|
297
|
+
if (definition.baseType)
|
|
298
|
+
return `${definition.baseType}/styles/${definition.key}/${definition.key}.opti-style.json`;
|
|
299
|
+
if (definition.contentType) {
|
|
300
|
+
if (opts?.contentBaseType)
|
|
301
|
+
return `${opts?.contentBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
|
|
302
|
+
if (!opts?.client)
|
|
303
|
+
throw new Error("Neither the contentBaseType, nor the ApiClient has been provided for a definition for a specific ContentType - unable to generate the path");
|
|
304
|
+
const pageSize = 50;
|
|
305
|
+
let resultsPage = await opts.client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
|
|
306
|
+
const contentTypes = resultsPage.items ?? [];
|
|
307
|
+
let pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
|
|
308
|
+
while (pagesRemaining > 0 && contentTypes.length < resultsPage.totalItemCount) {
|
|
309
|
+
resultsPage = await opts.client.contentTypes.contentTypesList(undefined, undefined, resultsPage.pageIndex + 1, resultsPage.pageSize);
|
|
310
|
+
contentTypes.push(...resultsPage.items);
|
|
311
|
+
pagesRemaining = Math.ceil(resultsPage.totalItemCount / resultsPage.pageSize) - (resultsPage.pageIndex + 1);
|
|
312
|
+
}
|
|
313
|
+
const fetchedBaseType = contentTypes.filter(x => x.key == definition.contentType).map(x => x.baseType).at(0);
|
|
314
|
+
if (fetchedBaseType)
|
|
315
|
+
return `${fetchedBaseType}/${definition.contentType}/${definition.key}.opti-style.json`;
|
|
316
|
+
}
|
|
317
|
+
throw new Error(`Unable to resolve the target for the DisplayTemplate: ${definition.key}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
146
320
|
const StylesListCommand = {
|
|
147
321
|
command: "styles:list",
|
|
148
322
|
describe: "List Visual Builder style definitions from the CMS",
|
|
149
323
|
handler: async (args) => {
|
|
150
|
-
const {
|
|
151
|
-
const client = createClient(cfg);
|
|
152
|
-
const pageSize = 100;
|
|
153
|
-
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Reading DisplayStyles from Optimizely CMS\n`));
|
|
154
|
-
if (cfg.debug)
|
|
155
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
|
|
156
|
-
let templatesPage = await client.displayTemplates.displayTemplatesList(0, pageSize);
|
|
157
|
-
const results = templatesPage.items ?? [];
|
|
158
|
-
let pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
|
|
159
|
-
while (pagesRemaining > 0 && results.length < templatesPage.totalItemCount) {
|
|
160
|
-
if (cfg.debug)
|
|
161
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${templatesPage.pageIndex + 2} of ${Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize)} (${templatesPage.pageSize} items per page)\n`));
|
|
162
|
-
templatesPage = await client.displayTemplates.displayTemplatesList(templatesPage.pageIndex + 1, templatesPage.pageSize);
|
|
163
|
-
results.push(...templatesPage.items);
|
|
164
|
-
pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
|
|
165
|
-
}
|
|
324
|
+
const { all: results } = await getStyles(createCmsClient(args), { ...args, excludeBaseTypes: [], excludeNodeTypes: [], excludeTemplates: [], excludeTypes: [], baseTypes: [], nodes: [], templates: [], types: [], templateTypes: [] });
|
|
166
325
|
const styles = new Table({
|
|
167
326
|
head: [
|
|
168
327
|
chalk.yellow(chalk.bold("Name")),
|
|
@@ -190,50 +349,15 @@ const StylesPullCommand = {
|
|
|
190
349
|
command: "styles:pull",
|
|
191
350
|
describe: "Create Visual Builder style definitions from the CMS",
|
|
192
351
|
builder: (yargs) => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return yargs;
|
|
352
|
+
const newYargs = stylesBuilder(yargs);
|
|
353
|
+
newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
354
|
+
newYargs.option("definitions", { alias: 'd', description: "Create/overwrite typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true });
|
|
355
|
+
return newYargs;
|
|
198
356
|
},
|
|
199
357
|
handler: async (args) => {
|
|
200
|
-
const { _config: cfg, components: basePath, force, definitions
|
|
201
|
-
const client =
|
|
202
|
-
const
|
|
203
|
-
//#region Load all templates from Optimizely CMS
|
|
204
|
-
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Reading DisplayStyles from Optimizely CMS\n`));
|
|
205
|
-
if (cfg.debug)
|
|
206
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
|
|
207
|
-
let templatesPage = await client.displayTemplates.displayTemplatesList(0, pageSize);
|
|
208
|
-
const results = templatesPage.items ?? [];
|
|
209
|
-
let pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
|
|
210
|
-
while (pagesRemaining > 0 && results.length < templatesPage.totalItemCount) {
|
|
211
|
-
if (cfg.debug)
|
|
212
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${templatesPage.pageIndex + 2} of ${Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize)} (${templatesPage.pageSize} items per page)\n`));
|
|
213
|
-
templatesPage = await client.displayTemplates.displayTemplatesList(templatesPage.pageIndex + 1, templatesPage.pageSize);
|
|
214
|
-
results.push(...templatesPage.items);
|
|
215
|
-
pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
|
|
216
|
-
}
|
|
217
|
-
if (cfg.debug)
|
|
218
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
|
|
219
|
-
//#endregion
|
|
220
|
-
//#region Ensure target base path exists
|
|
221
|
-
if (!fs.existsSync(basePath))
|
|
222
|
-
fs.mkdirSync(basePath, { recursive: true });
|
|
223
|
-
if (!fs.statSync(basePath).isDirectory()) {
|
|
224
|
-
process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The components path ${basePath} is not a folder\n`));
|
|
225
|
-
process.exit(1);
|
|
226
|
-
}
|
|
227
|
-
//#endregion
|
|
228
|
-
//#region Apply template filters
|
|
229
|
-
const filteredResults = results.filter(result => {
|
|
230
|
-
if (excludeTemplates.includes(result.key))
|
|
231
|
-
return false;
|
|
232
|
-
if (templates.length > 0 && !templates.includes(result.key))
|
|
233
|
-
return false;
|
|
234
|
-
return true;
|
|
235
|
-
});
|
|
236
|
-
//#endregion
|
|
358
|
+
const { _config: cfg, components: basePath, force, definitions } = parseArgs(args);
|
|
359
|
+
const client = createCmsClient(args);
|
|
360
|
+
const { styles: filteredResults } = await getStyles(client, args);
|
|
237
361
|
//#region Create & Write opti-style.json files
|
|
238
362
|
const typeFiles = {};
|
|
239
363
|
const updatedTemplates = (await Promise.all(filteredResults.map(async (displayTemplate) => {
|
|
@@ -241,8 +365,8 @@ const StylesPullCommand = {
|
|
|
241
365
|
let targetType;
|
|
242
366
|
let typesPath;
|
|
243
367
|
if (displayTemplate.nodeType) {
|
|
244
|
-
itemPath = path.join(basePath, '
|
|
245
|
-
typesPath = path.join(basePath, '
|
|
368
|
+
itemPath = path.join(basePath, 'nodes', displayTemplate.nodeType, displayTemplate.key);
|
|
369
|
+
typesPath = path.join(basePath, 'nodes', displayTemplate.nodeType);
|
|
246
370
|
targetType = 'node/' + displayTemplate.nodeType;
|
|
247
371
|
}
|
|
248
372
|
else if (displayTemplate.baseType) {
|
|
@@ -260,6 +384,17 @@ const StylesPullCommand = {
|
|
|
260
384
|
fs.mkdirSync(itemPath, { recursive: true });
|
|
261
385
|
// Write Style JSON
|
|
262
386
|
const filePath = path.join(itemPath, `${displayTemplate.key}.opti-style.json`);
|
|
387
|
+
if (fs.existsSync(filePath)) {
|
|
388
|
+
if (!force) {
|
|
389
|
+
if (cfg.debug)
|
|
390
|
+
process.stdout.write(chalk.gray(`${figures.cross} Skipping style file for ${displayTemplate.key} - File already exists\n`));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (cfg.debug)
|
|
394
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting style file for ${displayTemplate.key}\n`));
|
|
395
|
+
}
|
|
396
|
+
else if (cfg.debug)
|
|
397
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating style file for ${displayTemplate.key}\n`));
|
|
263
398
|
fs.writeFileSync(filePath, JSON.stringify(displayTemplate, undefined, 2));
|
|
264
399
|
if (!typeFiles[targetType]) {
|
|
265
400
|
typeFiles[targetType] = {
|
|
@@ -269,17 +404,23 @@ const StylesPullCommand = {
|
|
|
269
404
|
}
|
|
270
405
|
typeFiles[targetType].templates.push({ file: filePath, data: displayTemplate });
|
|
271
406
|
return displayTemplate.key;
|
|
272
|
-
})));
|
|
407
|
+
}))).filter(x => x);
|
|
273
408
|
//#endregion
|
|
274
409
|
//#region Create needed definition files
|
|
275
410
|
if (definitions) {
|
|
276
411
|
for (const targetId of Object.getOwnPropertyNames(typeFiles)) {
|
|
277
412
|
const { filePath: typeFilePath, templates } = typeFiles[targetId];
|
|
278
|
-
if (fs.existsSync(typeFilePath)
|
|
413
|
+
if (fs.existsSync(typeFilePath)) {
|
|
414
|
+
if (!force) {
|
|
415
|
+
if (cfg.debug)
|
|
416
|
+
process.stdout.write(chalk.gray(`${figures.cross} Skipped writing definition file for ${targetId} - it already exists\n`));
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
279
419
|
if (cfg.debug)
|
|
280
|
-
process.stdout.write(chalk.gray(`${figures.
|
|
281
|
-
continue;
|
|
420
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting definition file for ${targetId}\n`));
|
|
282
421
|
}
|
|
422
|
+
else if (cfg.debug)
|
|
423
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating definition file for ${targetId}\n`));
|
|
283
424
|
// Write Style definition
|
|
284
425
|
const imports = ['import type { LayoutProps } from "@remkoj/optimizely-cms-react/components"', 'import type { ReactNode } from "react"'];
|
|
285
426
|
const typeContents = [];
|
|
@@ -314,8 +455,7 @@ export type ${typeId}Component<DT extends Record<string, any> = Record<string, a
|
|
|
314
455
|
}
|
|
315
456
|
}
|
|
316
457
|
//#endregion
|
|
317
|
-
process.stdout.write(chalk.
|
|
318
|
-
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
458
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + ` Created/updated style definitions for ${updatedTemplates.join(', ')}`)) + "\n");
|
|
319
459
|
}
|
|
320
460
|
};
|
|
321
461
|
function ucFirst$1(input) {
|
|
@@ -324,63 +464,110 @@ function ucFirst$1(input) {
|
|
|
324
464
|
return input[0].toUpperCase() + input.substring(1);
|
|
325
465
|
}
|
|
326
466
|
|
|
467
|
+
// Node JS and 3rd Party libraries
|
|
468
|
+
const StylesCreateCommand = {
|
|
469
|
+
command: "style:create",
|
|
470
|
+
describe: "Create a new style definition",
|
|
471
|
+
builder: yargs => {
|
|
472
|
+
return yargs;
|
|
473
|
+
},
|
|
474
|
+
handler: async (args) => {
|
|
475
|
+
const { components: basePath } = parseArgs(args);
|
|
476
|
+
const client = createCmsClient(args);
|
|
477
|
+
const allowedBaseTypes = ['section', 'element'];
|
|
478
|
+
// Prepare
|
|
479
|
+
process.stdout.write(chalk.yellowBright(chalk.bold(`Reading current information from Optimizely CMS\n`)));
|
|
480
|
+
const [{ contentTypes }, { styles }] = await Promise.all([
|
|
481
|
+
getContentTypes(client, { ...args, excludeBaseTypes: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [] }),
|
|
482
|
+
getStyles(client, { ...args, excludeBaseTypes: [], excludeNodeTypes: [], excludeTemplates: [], excludeTypes: [], baseTypes: allowedBaseTypes, types: [], nodes: [], components: '', templates: [], templateTypes: [] })
|
|
483
|
+
]);
|
|
484
|
+
const styleKeys = styles.map(x => x.key);
|
|
485
|
+
// Define the configuration
|
|
486
|
+
process.stdout.write(chalk.yellowBright(chalk.bold(`\nConfigure your style definition\n`)));
|
|
487
|
+
const key = await input({ message: "Identifier:", validate: (val) => { return val.match(/^[a-zA-Z][A-Za-z0-9\-_]*$/) != null && !styleKeys.includes(val); } });
|
|
488
|
+
const displayName = await input({ message: "Display name:" });
|
|
489
|
+
const type = (await select({ message: "Style target:", choices: [
|
|
490
|
+
{ value: "baseType", name: "Base Type", description: "Target all Content-Types that inherit from the specified base type" },
|
|
491
|
+
{ value: "contentType", name: "Content-Type", description: "Target a specific Content-Type that supports styling" },
|
|
492
|
+
{ value: "nodeType", name: "Experience Node", description: "Target a specific node type from a Section" }
|
|
493
|
+
] }));
|
|
494
|
+
let typeId = "";
|
|
495
|
+
switch (type) {
|
|
496
|
+
case 'contentType':
|
|
497
|
+
typeId = await select({ message: "Content-Type:", choices: contentTypes.map(x => { return { value: x.key, description: x.description, name: x.displayName }; }) });
|
|
498
|
+
break;
|
|
499
|
+
case 'nodeType':
|
|
500
|
+
typeId = await select({ message: "Node type:", choices: [
|
|
501
|
+
{ value: "row", description: "Row", name: "Row" },
|
|
502
|
+
{ value: "column", description: "Column", name: "Column" }
|
|
503
|
+
] });
|
|
504
|
+
break;
|
|
505
|
+
case 'baseType':
|
|
506
|
+
typeId = await select({ message: "Base type:", choices: [
|
|
507
|
+
{ value: "section", description: "Target all section types", name: "Section" },
|
|
508
|
+
{ value: "element", description: "Target all element types", name: "Element" }
|
|
509
|
+
] });
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
const isDefault = await confirm({ message: "Should this style be marked as default?" });
|
|
513
|
+
//const commitToCms = await confirm({ message: "Do you want to upload this style immediately to the CMS?"})
|
|
514
|
+
const definition = { key, displayName, isDefault };
|
|
515
|
+
definition[type] = typeId;
|
|
516
|
+
definition['settings'] = {};
|
|
517
|
+
const contentBaseType = type == "contentType" ? contentTypes.filter(x => x.key == typeId).map(x => x.baseType).at(0) : undefined;
|
|
518
|
+
const styleFilePath = await getStyleFilePath(definition, { contentBaseType, client });
|
|
519
|
+
if (!fs.existsSync(path.join(basePath, path.dirname(styleFilePath))))
|
|
520
|
+
fs.mkdirSync(path.join(basePath, path.dirname(styleFilePath)), { recursive: true });
|
|
521
|
+
if (fs.existsSync(path.join(basePath, styleFilePath))) {
|
|
522
|
+
const overwrite = await confirm({ message: "The style definition file already exists, do you want to overwrite it?" });
|
|
523
|
+
if (!overwrite) {
|
|
524
|
+
process.stdout.write("\n");
|
|
525
|
+
process.stdout.write(chalk.redBright(chalk.bold(figures.cross + " Aborted")) + "\n");
|
|
526
|
+
process.exit(0);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
fs.writeFileSync(path.join(basePath, styleFilePath), JSON.stringify(definition, undefined, 4));
|
|
530
|
+
process.stdout.write(chalk.yellowBright(chalk.bold(`\nWritten style defintion template to: ${path.normalize(path.join(basePath, styleFilePath))}\n`)));
|
|
531
|
+
process.stdout.write(chalk.yellowBright(`\n1. Add your properties to the 'settings' list in the file`));
|
|
532
|
+
process.stdout.write(chalk.yellowBright(`\n2. Run `));
|
|
533
|
+
process.stdout.write(chalk.whiteBright(`yarn opti-cms styles:push -t ${definition.key}`));
|
|
534
|
+
process.stdout.write(chalk.yellowBright(` to upload the Display Template to Optimizely CMS`));
|
|
535
|
+
process.stdout.write(chalk.yellowBright(`\n3. Run `));
|
|
536
|
+
process.stdout.write(chalk.whiteBright(`yarn opti-cms styles:pull -d ${definition.key}`));
|
|
537
|
+
process.stdout.write(chalk.yellowBright(` to fully create the TypeScript definitions for this Display Template\n`));
|
|
538
|
+
process.stdout.write("\n");
|
|
539
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
327
543
|
const TypesPullCommand = {
|
|
328
544
|
command: "types:pull",
|
|
329
545
|
describe: "Pull content type definition files into the project",
|
|
330
546
|
builder: (yargs) => {
|
|
547
|
+
const newArgs = contentTypesBuilder(yargs);
|
|
331
548
|
yargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
332
|
-
|
|
333
|
-
yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
|
|
334
|
-
yargs.option("baseTypes", { alias: 'b', description: "Select only these base types", string: true, type: 'array', demandOption: false, default: [] });
|
|
335
|
-
yargs.option("types", { alias: 't', description: "Select only these types", string: true, type: 'array', demandOption: false, default: [] });
|
|
336
|
-
return yargs;
|
|
549
|
+
return newArgs;
|
|
337
550
|
},
|
|
338
551
|
handler: async (args) => {
|
|
339
|
-
const { _config:
|
|
340
|
-
const client =
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
if (cfg.debug)
|
|
344
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
|
|
345
|
-
let templatesPage = await client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
|
|
346
|
-
const results = templatesPage.items ?? [];
|
|
347
|
-
let pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
|
|
348
|
-
while (pagesRemaining > 0 && results.length < templatesPage.totalItemCount) {
|
|
349
|
-
if (cfg.debug)
|
|
350
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${templatesPage.pageIndex + 2} of ${Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize)} (${templatesPage.pageSize} items per page)\n`));
|
|
351
|
-
templatesPage = await client.contentTypes.contentTypesList(undefined, undefined, templatesPage.pageIndex + 1, templatesPage.pageSize);
|
|
352
|
-
results.push(...templatesPage.items);
|
|
353
|
-
pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
|
|
354
|
-
}
|
|
355
|
-
if (cfg.debug)
|
|
356
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
|
|
357
|
-
if (!fs.existsSync(basePath))
|
|
358
|
-
fs.mkdirSync(basePath, { recursive: true });
|
|
359
|
-
if (!fs.statSync(basePath).isDirectory()) {
|
|
360
|
-
process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The components path ${basePath} is not a folder\n`));
|
|
361
|
-
process.exit(1);
|
|
362
|
-
}
|
|
363
|
-
const updatedTypes = results.filter(data => {
|
|
364
|
-
return (!excludeBaseTypes.includes(data.baseType)) &&
|
|
365
|
-
(!excludeTypes.includes(data.key)) &&
|
|
366
|
-
(!baseTypes || baseTypes.length == 0 || baseTypes.includes(data.baseType)) &&
|
|
367
|
-
(!types || types.length == 0 || types.includes(data.key));
|
|
368
|
-
}).map(contentType => {
|
|
552
|
+
const { _config: { debug }, components: basePath, force } = parseArgs(args);
|
|
553
|
+
const client = createCmsClient(args);
|
|
554
|
+
const { contentTypes } = await getContentTypes(client, args);
|
|
555
|
+
const updatedTypes = contentTypes.map(contentType => {
|
|
369
556
|
const typePath = path.join(basePath, contentType.baseType, contentType.key);
|
|
370
557
|
const typeFile = path.join(typePath, `${contentType.key}.opti-type.json`);
|
|
371
558
|
if (!fs.existsSync(typePath))
|
|
372
559
|
fs.mkdirSync(typePath, { recursive: true });
|
|
373
560
|
if (fs.existsSync(typeFile) && !force) {
|
|
374
|
-
|
|
561
|
+
if (debug)
|
|
562
|
+
process.stdout.write(chalk.yellow(`${figures.cross} Skipping type definition for ${contentType.displayName} (${contentType.key}) - File already exists\n`));
|
|
375
563
|
return contentType.key;
|
|
376
564
|
}
|
|
377
|
-
if (
|
|
565
|
+
if (debug)
|
|
378
566
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key})\n`));
|
|
379
567
|
fs.writeFileSync(typeFile, JSON.stringify(contentType, undefined, 2));
|
|
380
568
|
return contentType.key;
|
|
381
569
|
}).filter(x => x);
|
|
382
|
-
process.stdout.write(chalk.
|
|
383
|
-
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
570
|
+
process.stdout.write(chalk.green(chalk.bold(`${figures.tick} Created/updated type definitions for ${updatedTypes.join(', ')}\n`)));
|
|
384
571
|
}
|
|
385
572
|
};
|
|
386
573
|
|
|
@@ -444,209 +631,81 @@ const TypesPushCommand = {
|
|
|
444
631
|
}
|
|
445
632
|
};
|
|
446
633
|
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
yargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
452
|
-
yargs.option('excludeTypes', { alias: 'ect', description: "Exclude these content types", string: true, type: 'array', demandOption: false, default: [] });
|
|
453
|
-
yargs.option('excludeBaseTypes', { alias: 'ebt', description: "Exclude these base types", string: true, type: 'array', demandOption: false, default: ['folder', 'media', 'image', 'video'] });
|
|
454
|
-
return yargs;
|
|
455
|
-
},
|
|
456
|
-
handler: async (args) => {
|
|
457
|
-
const { _config: cfg, components: basePath, excludeBaseTypes, excludeTypes, force } = parseArgs(args);
|
|
458
|
-
const client = createClient(cfg);
|
|
459
|
-
const pageSize = 100;
|
|
460
|
-
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
|
|
461
|
-
if (cfg.debug)
|
|
462
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page 1 of ? (${pageSize} items per page)\n`));
|
|
463
|
-
let templatesPage = await client.contentTypes.contentTypesList(undefined, undefined, 0, pageSize);
|
|
464
|
-
const results = templatesPage.items ?? [];
|
|
465
|
-
let pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
|
|
466
|
-
while (pagesRemaining > 0 && results.length < templatesPage.totalItemCount) {
|
|
467
|
-
if (cfg.debug)
|
|
468
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetching page ${templatesPage.pageIndex + 2} of ${Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize)} (${templatesPage.pageSize} items per page)\n`));
|
|
469
|
-
templatesPage = await client.contentTypes.contentTypesList(undefined, undefined, templatesPage.pageIndex + 1, templatesPage.pageSize);
|
|
470
|
-
results.push(...templatesPage.items);
|
|
471
|
-
pagesRemaining = Math.ceil(templatesPage.totalItemCount / templatesPage.pageSize) - (templatesPage.pageIndex + 1);
|
|
472
|
-
}
|
|
473
|
-
if (cfg.debug)
|
|
474
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched ${results.length} Content-Types from Optimizely CMS\n`));
|
|
475
|
-
if (!fs.existsSync(basePath))
|
|
476
|
-
fs.mkdirSync(basePath, { recursive: true });
|
|
477
|
-
if (!fs.statSync(basePath).isDirectory()) {
|
|
478
|
-
process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} The components path ${basePath} is not a folder\n`));
|
|
479
|
-
process.exit(1);
|
|
480
|
-
}
|
|
481
|
-
// Apply content type filters
|
|
482
|
-
if (cfg.debug)
|
|
483
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Applying content type filters\n`));
|
|
484
|
-
const contentTypes = results.filter(contentType => {
|
|
485
|
-
if (contentType.source == 'system') {
|
|
486
|
-
if (cfg.debug)
|
|
487
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) - Internal CMS type\n`));
|
|
488
|
-
return false;
|
|
489
|
-
}
|
|
490
|
-
const baseType = contentType.baseType ?? 'default';
|
|
491
|
-
if (excludeBaseTypes.includes(baseType)) {
|
|
492
|
-
if (cfg.debug)
|
|
493
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) - Base type excluded\n`));
|
|
494
|
-
return false;
|
|
495
|
-
}
|
|
496
|
-
if (excludeTypes.includes(contentType.key)) {
|
|
497
|
-
if (cfg.debug)
|
|
498
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) - Content type excluded\n`));
|
|
499
|
-
return false;
|
|
500
|
-
}
|
|
501
|
-
return true;
|
|
502
|
-
});
|
|
503
|
-
if (cfg.debug)
|
|
504
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${results.length} to ${contentTypes.length} items\n`));
|
|
505
|
-
// Create the type specific
|
|
506
|
-
const updatedTypes = contentTypes.map(contentType => {
|
|
507
|
-
const baseType = contentType.baseType ?? 'default';
|
|
508
|
-
// Create the type folder
|
|
509
|
-
const typePath = path.join(basePath, baseType, contentType.key);
|
|
510
|
-
if (!fs.existsSync(typePath))
|
|
511
|
-
fs.mkdirSync(typePath, { recursive: true });
|
|
512
|
-
// Create fragments
|
|
513
|
-
createGraphFragments(contentType, typePath, basePath, force, cfg, contentTypes);
|
|
514
|
-
// Create component
|
|
515
|
-
createComponent(contentType, typePath, force, cfg);
|
|
516
|
-
return contentType.key;
|
|
517
|
-
}).filter(x => x);
|
|
518
|
-
createFactory(contentTypes, basePath, force, cfg);
|
|
519
|
-
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated type definitions for ${updatedTypes.join(', ')}\n`));
|
|
520
|
-
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
521
|
-
}
|
|
634
|
+
const builder = yargs => {
|
|
635
|
+
const updatedArgs = contentTypesBuilder(yargs);
|
|
636
|
+
updatedArgs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
637
|
+
return updatedArgs;
|
|
522
638
|
};
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Factory for ${ucFirst(baseType)} will contain these components: ${inheritingTypes.map(x => x.displayName ?? x.key).join(", ")}\n`));
|
|
533
|
-
// Check the file presence and if we should overwrite
|
|
534
|
-
const baseTypeFactory = path.join(basePath, baseType, 'index.ts');
|
|
535
|
-
if (fs.existsSync(baseTypeFactory)) {
|
|
536
|
-
if (cfg.debug)
|
|
537
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${ucFirst(baseType)} factory\n`));
|
|
639
|
+
function createTypeFolders(contentTypes, basePath, debug = false) {
|
|
640
|
+
const folders = contentTypes.map(contentType => {
|
|
641
|
+
const baseType = contentType.baseType ?? 'default';
|
|
642
|
+
// Create the type folder
|
|
643
|
+
const typePath = path.join(basePath, baseType, contentType.key);
|
|
644
|
+
if (!fs.existsSync(typePath)) {
|
|
645
|
+
fs.mkdirSync(typePath, { recursive: true });
|
|
646
|
+
if (debug)
|
|
647
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Created folder ${typePath} for Content-Type ${contentType.key}\n`));
|
|
538
648
|
}
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
});
|
|
547
|
-
lines.push('');
|
|
548
|
-
lines.push(`export const ${baseType}Dictionary : ComponentTypeDictionary = [`);
|
|
549
|
-
inheritingTypes.forEach(type => {
|
|
550
|
-
lines.push(' {', ` type: '${type.key}',`, ` component: ${type.key}`, ' },');
|
|
551
|
-
});
|
|
552
|
-
lines.push(']', '', `export default ${baseType}Dictionary`);
|
|
553
|
-
fs.writeFileSync(baseTypeFactory, lines.join("\n"));
|
|
554
|
-
return baseType;
|
|
555
|
-
}).filter(x => x);
|
|
556
|
-
const cmsFactoryFile = path.join(basePath, 'index.ts');
|
|
557
|
-
if (fs.existsSync(cmsFactoryFile)) {
|
|
558
|
-
if (cfg.debug)
|
|
559
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting CMS Components factory\n`));
|
|
560
|
-
}
|
|
561
|
-
const lines = [
|
|
562
|
-
'// Auto generated dictionary',
|
|
563
|
-
'import { ComponentTypeDictionary } from "@remkoj/optimizely-cms-react";'
|
|
564
|
-
];
|
|
565
|
-
baseTypeFactories.forEach(type => {
|
|
566
|
-
lines.push(`import ${type}Components from "./${type}";`);
|
|
567
|
-
});
|
|
568
|
-
lines.push('');
|
|
569
|
-
baseTypeFactories.forEach(type => {
|
|
570
|
-
lines.push(`prefixDictionaryEntries(${type}Components, '${ucFirst(type)}');`);
|
|
571
|
-
if (type == "experience")
|
|
572
|
-
lines.push(`prefixDictionaryEntries(${type}Components, 'Page'); // Experiences are a subtype of Page`);
|
|
573
|
-
if (type == "element")
|
|
574
|
-
lines.push(`prefixDictionaryEntries(${type}Components, 'Component'); // Elements are a subtype of Component`);
|
|
575
|
-
});
|
|
576
|
-
lines.push('');
|
|
577
|
-
lines.push('export const cmsComponentDictionary : ComponentTypeDictionary = [');
|
|
578
|
-
baseTypeFactories.forEach(type => {
|
|
579
|
-
lines.push(` ...${type}Components,`);
|
|
649
|
+
// Check folders
|
|
650
|
+
if (!fs.statSync(typePath).isDirectory())
|
|
651
|
+
throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
|
|
652
|
+
return {
|
|
653
|
+
type: contentType.key,
|
|
654
|
+
path: typePath,
|
|
655
|
+
};
|
|
580
656
|
});
|
|
581
|
-
|
|
582
|
-
{
|
|
583
|
-
list.forEach((component, idx, dictionary) => {
|
|
584
|
-
dictionary[idx].type = typeof component.type == 'string' ? prefix + "/" + component.type : [ prefix, ...component.type ]
|
|
585
|
-
})
|
|
586
|
-
return list
|
|
587
|
-
}`);
|
|
588
|
-
fs.writeFileSync(cmsFactoryFile, lines.join("\n"));
|
|
657
|
+
return folders;
|
|
589
658
|
}
|
|
590
|
-
function
|
|
591
|
-
|
|
592
|
-
if (fs.existsSync(componentFile)) {
|
|
593
|
-
if (force) {
|
|
594
|
-
if (cfg.debug)
|
|
595
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) component\n`));
|
|
596
|
-
}
|
|
597
|
-
else {
|
|
598
|
-
if (cfg.debug)
|
|
599
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) component - file already exists\n`));
|
|
600
|
-
return undefined;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
const isPage = contentType.baseType == 'page' || contentType.baseType == 'experience';
|
|
604
|
-
const varName = `${contentType.key}${ucFirst(contentType.baseType ?? 'part')}`;
|
|
605
|
-
const component = `import ${isPage ? '{ OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs"' : '{ CmsComponent } from "@remkoj/optimizely-cms-react"'};
|
|
606
|
-
import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* ${contentType.displayName}
|
|
610
|
-
* ${contentType.description}
|
|
611
|
-
*/
|
|
612
|
-
export const ${varName} : CmsComponent<${contentType.key}DataFragment> = ({ data }) => {
|
|
613
|
-
const componentName = '${contentType.displayName}'
|
|
614
|
-
const componentInfo = '${contentType.description ?? ''}'
|
|
615
|
-
return <div className="mx-auto px-2 container">
|
|
616
|
-
<div>{ componentName }</div>
|
|
617
|
-
<div>{ componentInfo }</div>
|
|
618
|
-
<pre className="w-full overflow-x-hidden font-mono text-sm">{ JSON.stringify(data, undefined, 4) }</pre>
|
|
619
|
-
</div>
|
|
620
|
-
}
|
|
621
|
-
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
622
|
-
${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
|
|
623
|
-
${isPage && `${varName}.getMetaData = async (contentLink) => {
|
|
624
|
-
// Add your metadata logic here
|
|
625
|
-
return {}
|
|
626
|
-
}`.trim()}
|
|
627
|
-
|
|
628
|
-
export default ${varName}`;
|
|
629
|
-
fs.writeFileSync(componentFile, component);
|
|
630
|
-
}
|
|
631
|
-
function ucFirst(current) {
|
|
632
|
-
return current[0]?.toUpperCase() + current.substring(1);
|
|
659
|
+
function getTypeFolder(list, type) {
|
|
660
|
+
return list.filter(x => x.type == type).at(0)?.path;
|
|
633
661
|
}
|
|
634
|
-
|
|
662
|
+
|
|
663
|
+
const NextJsQueriesCommand = {
|
|
664
|
+
command: "nextjs:fragments",
|
|
665
|
+
describe: "Create the GrapQL Fragments for a Next.JS / Optimizely Graph structure",
|
|
666
|
+
builder,
|
|
667
|
+
handler: async (args, opts) => {
|
|
668
|
+
// Prepare
|
|
669
|
+
const { loadedContentTypes, createdTypeFolders } = opts || {};
|
|
670
|
+
const { components: basePath, _config: { debug }, force } = parseArgs(args);
|
|
671
|
+
const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(createCmsClient(args), args);
|
|
672
|
+
// Start process
|
|
673
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments for ${contentTypes.map(x => x.key).join(', ')}\n`));
|
|
674
|
+
const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
|
|
675
|
+
const updatedTypes = contentTypes.map(contentType => {
|
|
676
|
+
const typePath = getTypeFolder(typeFolders, contentType.key);
|
|
677
|
+
return createGraphFragments(contentType, typePath, basePath, force, debug, allContentTypes);
|
|
678
|
+
}).filter(x => x).flat();
|
|
679
|
+
// Report outcome
|
|
680
|
+
if (updatedTypes.length > 0)
|
|
681
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL fragments for ${updatedTypes.join(', ')}\n`));
|
|
682
|
+
else
|
|
683
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No GraphQL fragments created/updated\n`));
|
|
684
|
+
if (!opts)
|
|
685
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
function createGraphFragments(contentType, typePath, basePath, force, debug, contentTypes) {
|
|
689
|
+
const returnValue = [];
|
|
635
690
|
const baseType = contentType.baseType ?? 'default';
|
|
636
691
|
const baseQueryFile = path.join(typePath, `${contentType.key}.${baseType}.graphql`);
|
|
637
692
|
if (fs.existsSync(baseQueryFile)) {
|
|
638
693
|
if (force) {
|
|
639
|
-
if (
|
|
694
|
+
if (debug)
|
|
640
695
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
641
696
|
}
|
|
642
697
|
else {
|
|
643
|
-
if (
|
|
698
|
+
if (debug)
|
|
644
699
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) base fragment - file already exists\n`));
|
|
645
700
|
return undefined;
|
|
646
701
|
}
|
|
647
702
|
}
|
|
703
|
+
else if (debug) {
|
|
704
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
705
|
+
}
|
|
648
706
|
const { fragment, propertyTypes } = createInitialFragment(contentType);
|
|
649
707
|
fs.writeFileSync(baseQueryFile, fragment);
|
|
708
|
+
returnValue.push(contentType.key);
|
|
650
709
|
let dependencies = Array.isArray(propertyTypes) ? [...propertyTypes] : [];
|
|
651
710
|
while (Array.isArray(dependencies) && dependencies.length > 0) {
|
|
652
711
|
let newDependencies = [];
|
|
@@ -661,15 +720,18 @@ function createGraphFragments(contentType, typePath, basePath, force, cfg, conte
|
|
|
661
720
|
if (!fs.existsSync(propertyFragmentDir))
|
|
662
721
|
fs.mkdirSync(propertyFragmentDir, { recursive: true });
|
|
663
722
|
if (!fs.existsSync(propertyFragmentFile) || force) {
|
|
664
|
-
|
|
723
|
+
if (debug)
|
|
724
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Writing ${propContentType.displayName} (${propContentType.key}) property fragment\n`));
|
|
665
725
|
const propContentTypeInfo = createInitialFragment(propContentType, true);
|
|
666
726
|
fs.writeFileSync(propertyFragmentFile, propContentTypeInfo.fragment);
|
|
727
|
+
returnValue.push(propContentType.key);
|
|
667
728
|
if (Array.isArray(propContentTypeInfo.propertyTypes))
|
|
668
729
|
newDependencies.push(...propContentTypeInfo.propertyTypes);
|
|
669
730
|
}
|
|
670
731
|
});
|
|
671
732
|
dependencies = newDependencies;
|
|
672
733
|
}
|
|
734
|
+
return returnValue.length > 0 ? returnValue : undefined;
|
|
673
735
|
}
|
|
674
736
|
function createInitialFragment(contentType, forProperty = false) {
|
|
675
737
|
const propertyTypes = [];
|
|
@@ -727,6 +789,8 @@ function createInitialFragment(contentType, forProperty = false) {
|
|
|
727
789
|
break;
|
|
728
790
|
}
|
|
729
791
|
});
|
|
792
|
+
if (contentType.baseType == "experience")
|
|
793
|
+
fragmentFields.push('...ExperienceData');
|
|
730
794
|
if (fragmentFields.length == 0)
|
|
731
795
|
fragmentFields.push('_metadata { key }');
|
|
732
796
|
const tpl = `fragment ${contentType.key}${forProperty ? 'Property' : ''}Data on ${contentType.key}${forProperty ? 'Property' : ''} {
|
|
@@ -738,6 +802,499 @@ function createInitialFragment(contentType, forProperty = false) {
|
|
|
738
802
|
};
|
|
739
803
|
}
|
|
740
804
|
|
|
805
|
+
function ucFirst(current) {
|
|
806
|
+
if (typeof current != 'string')
|
|
807
|
+
throw new Error("Only strings can be transformed");
|
|
808
|
+
if (current == "")
|
|
809
|
+
return current;
|
|
810
|
+
return current[0]?.toUpperCase() + current.substring(1);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const NextJsComponentsCommand = {
|
|
814
|
+
command: "nextjs:components",
|
|
815
|
+
describe: "Create the React Components for a Next.JS / Optimizely Graph structure",
|
|
816
|
+
builder,
|
|
817
|
+
handler: async (args, opts) => {
|
|
818
|
+
// Prepare
|
|
819
|
+
const { loadedContentTypes, createdTypeFolders } = opts || {};
|
|
820
|
+
const { components: basePath, _config: { debug }, force } = parseArgs(args);
|
|
821
|
+
const { contentTypes } = loadedContentTypes ?? await getContentTypes(createCmsClient(args), args);
|
|
822
|
+
// Start process
|
|
823
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating React Components for ${contentTypes.map(x => x.key).join(', ')}\n`));
|
|
824
|
+
const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
|
|
825
|
+
const updatedTypes = contentTypes.map(contentType => {
|
|
826
|
+
const typePath = getTypeFolder(typeFolders, contentType.key);
|
|
827
|
+
return createComponent(contentType, typePath, force, debug);
|
|
828
|
+
}).filter(x => x);
|
|
829
|
+
// Report outcome
|
|
830
|
+
if (updatedTypes.length > 0)
|
|
831
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated React Components for ${updatedTypes.join(', ')}\n`));
|
|
832
|
+
else
|
|
833
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} No React Components created/updated\n`));
|
|
834
|
+
if (!opts)
|
|
835
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
function createComponent(contentType, typePath, force, debug = false) {
|
|
839
|
+
const componentFile = path.join(typePath, 'index.tsx');
|
|
840
|
+
if (fs.existsSync(componentFile)) {
|
|
841
|
+
if (!force) {
|
|
842
|
+
if (debug)
|
|
843
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.displayName} (${contentType.key}) component - file already exists\n`));
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
if (debug)
|
|
847
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${contentType.displayName} (${contentType.key}) component\n`));
|
|
848
|
+
}
|
|
849
|
+
else if (debug)
|
|
850
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) component\n`));
|
|
851
|
+
// Get type information & short-hands
|
|
852
|
+
const displayTemplate = getDisplayTemplateInfo$1(contentType, typePath);
|
|
853
|
+
const baseDisplayTemplate = getBaseTypeTemplateInfo(contentType, typePath);
|
|
854
|
+
const varName = `${contentType.key}${ucFirst(contentType.baseType ?? 'part')}`;
|
|
855
|
+
const tplFn = Templates[contentType.baseType] ?? Templates['default'];
|
|
856
|
+
if (!tplFn) {
|
|
857
|
+
if (debug)
|
|
858
|
+
process.stdout.write(chalk.redBright(`${figures.cross} Skipping ${contentType.displayName} (${contentType.key}) component - no template for ${contentType.baseType} found\n`));
|
|
859
|
+
return undefined;
|
|
860
|
+
}
|
|
861
|
+
// Apply template
|
|
862
|
+
fs.writeFileSync(componentFile, tplFn(contentType, varName, displayTemplate, baseDisplayTemplate));
|
|
863
|
+
return contentType.key;
|
|
864
|
+
}
|
|
865
|
+
function getBaseTypeTemplateInfo(contentType, typePath) {
|
|
866
|
+
const displayTemplatesFile = path.join(typePath, '..', 'styles', 'displayTemplates.ts');
|
|
867
|
+
if (fs.existsSync(displayTemplatesFile)) {
|
|
868
|
+
const baseTypeVarName = contentType.baseType ? contentType.baseType[0].toUpperCase() + contentType.baseType.substring(1) : undefined;
|
|
869
|
+
if (!baseTypeVarName)
|
|
870
|
+
return undefined;
|
|
871
|
+
const displayTemplateContent = fs.readFileSync(displayTemplatesFile).toString();
|
|
872
|
+
const displayTemplateName = baseTypeVarName + 'LayoutProps';
|
|
873
|
+
if (displayTemplateContent.includes(`export type ${displayTemplateName} =`)) {
|
|
874
|
+
return displayTemplateName;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return undefined;
|
|
878
|
+
}
|
|
879
|
+
function getDisplayTemplateInfo$1(contentType, typePath) {
|
|
880
|
+
const displayTemplatesFile = path.join(typePath, 'displayTemplates.ts');
|
|
881
|
+
if (fs.existsSync(displayTemplatesFile)) {
|
|
882
|
+
const displayTemplateContent = fs.readFileSync(displayTemplatesFile).toString();
|
|
883
|
+
const displayTemplateName = contentType.key + 'LayoutProps';
|
|
884
|
+
if (displayTemplateContent.includes(`export type ${displayTemplateName} =`)) {
|
|
885
|
+
return displayTemplateName;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
const Templates = {
|
|
891
|
+
// Default Template for all components without specifics
|
|
892
|
+
default: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsComponent } from "@remkoj/optimizely-cms-react";
|
|
893
|
+
import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
|
|
894
|
+
import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
|
|
895
|
+
import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* ${contentType.displayName}
|
|
899
|
+
* ${contentType.description}
|
|
900
|
+
*/
|
|
901
|
+
export const ${varName} : CmsComponent<${contentType.key}DataFragment${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, children }) => {
|
|
902
|
+
const componentName = '${contentType.displayName}'
|
|
903
|
+
const componentInfo = '${contentType.description ?? ''}'
|
|
904
|
+
return <div className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4">
|
|
905
|
+
<div className="font-bold italic">{ componentName }</div>
|
|
906
|
+
<div>{ componentInfo }</div>
|
|
907
|
+
{ 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> }
|
|
908
|
+
{ children && <div className="mt-4 mx-4 flex flex-col">{ children }</div>}
|
|
909
|
+
</div>
|
|
910
|
+
}
|
|
911
|
+
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
912
|
+
${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
|
|
913
|
+
|
|
914
|
+
export default ${varName}`,
|
|
915
|
+
// Template for all page component types
|
|
916
|
+
page: (contentType, varName, displayTemplate) => `import { OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
|
|
917
|
+
import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";${displayTemplate ? `
|
|
918
|
+
import { ${displayTemplate} } from "./displayTemplates";` : ''}
|
|
919
|
+
import { getSdk } from "@/gql"
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* ${contentType.displayName}
|
|
923
|
+
* ${contentType.description}
|
|
924
|
+
*/
|
|
925
|
+
export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, children }) => {
|
|
926
|
+
const componentName = '${contentType.displayName}'
|
|
927
|
+
const componentInfo = '${contentType.description ?? ''}'
|
|
928
|
+
return <div className="mx-auto px-2 container">
|
|
929
|
+
<div className="font-bold italic">{ componentName }</div>
|
|
930
|
+
<div>{ componentInfo }</div>
|
|
931
|
+
{ 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> }
|
|
932
|
+
{ children && <div className="flex flex-col mt-4 mx-4">{ children }</div>}
|
|
933
|
+
</div>
|
|
934
|
+
}
|
|
935
|
+
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
936
|
+
${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
|
|
937
|
+
${varName}.getMetaData = async (contentLink, locale, client) => {
|
|
938
|
+
const sdk = getSdk(client);
|
|
939
|
+
// Add your metadata logic here
|
|
940
|
+
return {}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
export default ${varName}`,
|
|
944
|
+
// Template for all experience component types
|
|
945
|
+
experience: (contentType, varName, displayTemplate) => `import { OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
|
|
946
|
+
import { ${contentType.key}DataFragmentDoc, type ${contentType.key}DataFragment } from "@/gql/graphql";
|
|
947
|
+
import { type Maybe, type ICompositionNode, type ExperienceDataFragment } from "@/gql/graphql";
|
|
948
|
+
import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
|
|
949
|
+
import { ${displayTemplate} } from "./displayTemplates";` : ''}
|
|
950
|
+
import { getSdk } from "@/gql"
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* ${contentType.displayName}
|
|
954
|
+
* ${contentType.description}
|
|
955
|
+
*/
|
|
956
|
+
export const ${varName} : CmsComponent<${contentType.key}DataFragment${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''} }) => {
|
|
957
|
+
const composition = (data as ExperienceDataFragment).composition as Maybe<ICompositionNode>
|
|
958
|
+
return <CmsEditable as="div" className="mx-auto px-2 container" cmsFieldName="unstructuredData">
|
|
959
|
+
{ composition && isNode(composition) && <OptimizelyComposition node={composition} /> }
|
|
960
|
+
</CmsEditable>
|
|
961
|
+
}
|
|
962
|
+
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
963
|
+
${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}DataFragmentDoc]
|
|
964
|
+
${varName}.getMetaData = async (contentLink, locale, client) => {
|
|
965
|
+
const sdk = getSdk(client);
|
|
966
|
+
// Add your metadata logic here
|
|
967
|
+
return {}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export default ${varName}`
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
const NextJsVisualBuilderCommand = {
|
|
974
|
+
command: "nextjs:visualbuilder",
|
|
975
|
+
describe: "Create the React Components for Visual Builder in a Next.JS / Optimizely Graph structure",
|
|
976
|
+
builder,
|
|
977
|
+
handler: async (args) => {
|
|
978
|
+
// Prepare
|
|
979
|
+
const { components: basePath, _config: { debug }, force } = parseArgs(args);
|
|
980
|
+
// Create the fall-back node and style defintions
|
|
981
|
+
await StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node'], definitions: true });
|
|
982
|
+
createGenericNode(basePath, force, debug);
|
|
983
|
+
// Get all styles
|
|
984
|
+
const { styles } = await getStyles(createCmsClient(args), { ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'] });
|
|
985
|
+
// Process node styles
|
|
986
|
+
styles.filter(x => typeof (x.nodeType) == 'string' && x.nodeType.length > 0).map(styleDefintion => {
|
|
987
|
+
const templatePath = path.join(basePath, 'nodes', styleDefintion.nodeType, styleDefintion.key);
|
|
988
|
+
createSpecificNode(styleDefintion, templatePath, force, debug);
|
|
989
|
+
});
|
|
990
|
+
// Process base styles
|
|
991
|
+
styles.filter(x => typeof (x.baseType) == 'string' && x.baseType.length > 0).map(styleDefinition => {
|
|
992
|
+
const templatePath = path.join(basePath, styleDefinition.baseType, 'styles', styleDefinition.key);
|
|
993
|
+
createSpecificNode(styleDefinition, templatePath, force, debug);
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
function createSpecificNode(template, templatePath, force = false, debug = false) {
|
|
998
|
+
const nodeFile = path.join(templatePath, 'index.tsx');
|
|
999
|
+
if (fs.existsSync(nodeFile)) {
|
|
1000
|
+
if (!force) {
|
|
1001
|
+
if (debug)
|
|
1002
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${template.displayName} (${template.key}) component - file already exists\n`));
|
|
1003
|
+
return undefined;
|
|
1004
|
+
}
|
|
1005
|
+
if (debug)
|
|
1006
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${template.displayName} (${template.key}) component\n`));
|
|
1007
|
+
}
|
|
1008
|
+
else if (debug)
|
|
1009
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${template.displayName} (${template.key}) component\n`));
|
|
1010
|
+
if (!fs.existsSync(templatePath))
|
|
1011
|
+
fs.mkdirSync(templatePath, { recursive: true });
|
|
1012
|
+
const displayTemplateName = getDisplayTemplateInfo(template, templatePath);
|
|
1013
|
+
const component = `import { type CmsLayoutComponent } from "@remkoj/optimizely-cms-react";
|
|
1014
|
+
import { extractSettings } from "@remkoj/optimizely-cms-react/components";${displayTemplateName ? `
|
|
1015
|
+
import { ${displayTemplateName} } from "../displayTemplates";` : ''}
|
|
1016
|
+
|
|
1017
|
+
export const ${template.key} : CmsLayoutComponent<${displayTemplateName ?? '{}'}> = ({ contentLink, layoutProps, children }) => {
|
|
1018
|
+
const layout = extractSettings(layoutProps);
|
|
1019
|
+
return (<div className="vb:${template.nodeType} vb:${template.nodeType}:${template.key}">{ children }</div>);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
export default ${template.key};`;
|
|
1023
|
+
fs.writeFileSync(nodeFile, component);
|
|
1024
|
+
}
|
|
1025
|
+
function createGenericNode(basePath, force, debug) {
|
|
1026
|
+
const nodeItem = path.join(basePath, 'node.tsx');
|
|
1027
|
+
if (fs.existsSync(nodeItem)) {
|
|
1028
|
+
if (!force) {
|
|
1029
|
+
if (debug)
|
|
1030
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping generic node component - file already exists\n`));
|
|
1031
|
+
return undefined;
|
|
1032
|
+
}
|
|
1033
|
+
if (debug)
|
|
1034
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting generic node component\n`));
|
|
1035
|
+
}
|
|
1036
|
+
else if (debug)
|
|
1037
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating generic node component\n`));
|
|
1038
|
+
const nodeContent = `import { type CmsLayoutComponent } from "@remkoj/optimizely-cms-react"
|
|
1039
|
+
import { CmsEditable } from '@remkoj/optimizely-cms-react/rsc'
|
|
1040
|
+
|
|
1041
|
+
export const VisualBuilderNode : CmsLayoutComponent = ({ contentLink, layoutProps, children }) =>
|
|
1042
|
+
{
|
|
1043
|
+
let className = \`vb:\${layoutProps?.layoutType}\`
|
|
1044
|
+
if (layoutProps && layoutProps.layoutType == "section")
|
|
1045
|
+
return <CmsEditable as="div" className={ className } cmsId={ contentLink.key }>{ children }</CmsEditable>
|
|
1046
|
+
return <div className={ className }>{ children }</div>
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
export default VisualBuilderNode`;
|
|
1050
|
+
fs.writeFileSync(nodeItem, nodeContent);
|
|
1051
|
+
}
|
|
1052
|
+
function getDisplayTemplateInfo(template, typePath) {
|
|
1053
|
+
const displayTemplatesFile = path.join(typePath, '..', 'displayTemplates.ts');
|
|
1054
|
+
if (fs.existsSync(displayTemplatesFile)) {
|
|
1055
|
+
const displayTemplateContent = fs.readFileSync(displayTemplatesFile).toString();
|
|
1056
|
+
const displayTemplateName = template.key + 'Props';
|
|
1057
|
+
if (displayTemplateContent.includes(`export type ${displayTemplateName} =`)) {
|
|
1058
|
+
return displayTemplateName;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return undefined;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const NextJsFactoryCommand = {
|
|
1065
|
+
command: "nextjs:factory",
|
|
1066
|
+
describe: "Create the ComponentFactory for a Next.JS / Optimizely Graph structure",
|
|
1067
|
+
builder,
|
|
1068
|
+
handler: async (args) => {
|
|
1069
|
+
const { components: basePath, force, _config: { debug } } = parseArgs(args);
|
|
1070
|
+
if (debug)
|
|
1071
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Start generating component factories\n`));
|
|
1072
|
+
const baseDirEntries = fs.readdirSync(basePath);
|
|
1073
|
+
const entries = baseDirEntries
|
|
1074
|
+
.filter(entry => fs.statSync(path.join(basePath, entry)).isDirectory())
|
|
1075
|
+
.map(entry => entry == "nodes" ? processSubGroupFolder(entry, path.join(basePath, entry), force, debug) : processTypeListFolder(entry, path.join(basePath, entry), force, debug))
|
|
1076
|
+
.filter(x => x);
|
|
1077
|
+
// Post process the entries
|
|
1078
|
+
if (debug)
|
|
1079
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Post processing resolved top-level factory entries\n`));
|
|
1080
|
+
entries.forEach((e, i) => {
|
|
1081
|
+
if (isImportInfoList(e) && typeof e.prefix == 'string')
|
|
1082
|
+
switch (e.prefix) {
|
|
1083
|
+
case "Experience":
|
|
1084
|
+
entries[i].prefix = [e.prefix, "Page"];
|
|
1085
|
+
break;
|
|
1086
|
+
case "Element":
|
|
1087
|
+
entries[i].prefix = [e.prefix, "Component"];
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
// Add any global components
|
|
1092
|
+
if (debug)
|
|
1093
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Adding global components\n`));
|
|
1094
|
+
baseDirEntries.filter(x => x.endsWith('.tsx') && fs.statSync(path.join(basePath, x)).isFile()).forEach(entry => {
|
|
1095
|
+
const entryName = entry.substring(0, entry.length - 4).replaceAll('.', '').split('-').map(p => ucFirst(p)).join('');
|
|
1096
|
+
const importPath = './' + entry.substring(0, entry.length - 4);
|
|
1097
|
+
entries.push({
|
|
1098
|
+
isList: false,
|
|
1099
|
+
component: (entryName[0].toLowerCase() + entryName.substring(1)) + "Component",
|
|
1100
|
+
path: importPath,
|
|
1101
|
+
type: entryName
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1104
|
+
// We're always overwriting the main factory
|
|
1105
|
+
if (debug)
|
|
1106
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating/overwriting main factory\n`));
|
|
1107
|
+
const mainFactoryFile = path.join(basePath, "index.ts");
|
|
1108
|
+
fs.writeFileSync(mainFactoryFile, generateFactory(entries, "cms"));
|
|
1109
|
+
process.stdout.write(chalk.yellow(`${figures.arrowRight} Generated main factory in: ${mainFactoryFile}.\n`));
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
function processSubGroupFolder(groupName, groupTypePath, force = false, debug = false) {
|
|
1113
|
+
const subgroupInfo = fs
|
|
1114
|
+
.readdirSync(groupTypePath)
|
|
1115
|
+
.filter(entry => {
|
|
1116
|
+
return fs.statSync(path.join(groupTypePath, entry)).isDirectory();
|
|
1117
|
+
}).map(entry => processTypeListFolder(entry, path.join(groupTypePath, entry), force, debug))
|
|
1118
|
+
.filter(x => x);
|
|
1119
|
+
if (subgroupInfo.length == 0)
|
|
1120
|
+
return null;
|
|
1121
|
+
const baseTypeIndex = path.join(groupTypePath, "index.ts");
|
|
1122
|
+
/* Check file existence and determine if we can continue */
|
|
1123
|
+
if (debug)
|
|
1124
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Identified base type ${groupName}\n`));
|
|
1125
|
+
if (fs.existsSync(baseTypeIndex)) {
|
|
1126
|
+
if (!force) {
|
|
1127
|
+
if (debug)
|
|
1128
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping factory for ${groupName} - file already exists\n`));
|
|
1129
|
+
return {
|
|
1130
|
+
isList: true,
|
|
1131
|
+
component: groupName + 'Dictionary',
|
|
1132
|
+
prefix: groupName[0].toUpperCase() + groupName.substring(1),
|
|
1133
|
+
path: './' + groupName
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
if (debug)
|
|
1137
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting factory for ${groupName}\n`));
|
|
1138
|
+
}
|
|
1139
|
+
else if (debug)
|
|
1140
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Generating new factory for ${groupName}\n`));
|
|
1141
|
+
fs.writeFileSync(baseTypeIndex, generateFactory(subgroupInfo, groupName));
|
|
1142
|
+
process.stdout.write(chalk.yellow(`${figures.arrowRight} Generated partial factory in: ${baseTypeIndex}.\n`));
|
|
1143
|
+
return {
|
|
1144
|
+
isList: true,
|
|
1145
|
+
component: groupName + 'Dictionary',
|
|
1146
|
+
prefix: groupName[0].toUpperCase() + groupName.substring(1),
|
|
1147
|
+
path: './' + groupName
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
function isImportInfoList(toTest) {
|
|
1151
|
+
return toTest.isList == true;
|
|
1152
|
+
}
|
|
1153
|
+
function isPrefixedImportInfoList(toTest) {
|
|
1154
|
+
return isImportInfoList(toTest) && ((typeof toTest.prefix == 'string' && toTest.prefix.length > 0) || (Array.isArray(toTest.prefix) && toTest.prefix.length > 0 && toTest.prefix.every(x => typeof x == 'string' && x.length > 0)));
|
|
1155
|
+
}
|
|
1156
|
+
function processTypeListFolder(baseType, baseTypePath, force = false, debug = false) {
|
|
1157
|
+
const baseTypeIndex = path.join(baseTypePath, "index.ts");
|
|
1158
|
+
/* Check file existence and determine if we can continue */
|
|
1159
|
+
if (debug)
|
|
1160
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Identified base type ${baseType}\n`));
|
|
1161
|
+
if (fs.existsSync(baseTypeIndex)) {
|
|
1162
|
+
if (!force) {
|
|
1163
|
+
if (debug)
|
|
1164
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping factory for ${baseType} - file already exists\n`));
|
|
1165
|
+
return {
|
|
1166
|
+
isList: true,
|
|
1167
|
+
component: baseType + 'Dictionary',
|
|
1168
|
+
prefix: baseType[0].toUpperCase() + baseType.substring(1),
|
|
1169
|
+
path: './' + baseType
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
if (debug)
|
|
1173
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting factory for ${baseType}\n`));
|
|
1174
|
+
}
|
|
1175
|
+
else if (debug)
|
|
1176
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Generating new factory for ${baseType}\n`));
|
|
1177
|
+
// Get the components to import
|
|
1178
|
+
let hasStyles = false;
|
|
1179
|
+
const components = fs
|
|
1180
|
+
.readdirSync(baseTypePath)
|
|
1181
|
+
.filter(entry => {
|
|
1182
|
+
if (entry == 'styles') {
|
|
1183
|
+
hasStyles = true;
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
return fs.statSync(path.join(baseTypePath, entry)).isDirectory() &&
|
|
1187
|
+
fs.existsSync(path.join(baseTypePath, entry, "index.tsx"));
|
|
1188
|
+
})
|
|
1189
|
+
.map(entry => {
|
|
1190
|
+
return {
|
|
1191
|
+
component: entry + 'Component',
|
|
1192
|
+
path: './' + entry,
|
|
1193
|
+
type: entry
|
|
1194
|
+
};
|
|
1195
|
+
});
|
|
1196
|
+
// Handle styles folder
|
|
1197
|
+
if (hasStyles) {
|
|
1198
|
+
const styles = processTypeListFolder("styles", path.join(baseTypePath, 'styles'), force, debug);
|
|
1199
|
+
if (styles) {
|
|
1200
|
+
components.push({
|
|
1201
|
+
path: "./styles",
|
|
1202
|
+
component: "styleDictionary",
|
|
1203
|
+
isList: true,
|
|
1204
|
+
prefix: "Styles"
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
components.sort((a, b) => {
|
|
1209
|
+
if (isImportInfoList(a) && !isImportInfoList(b))
|
|
1210
|
+
return -1;
|
|
1211
|
+
if (!isImportInfoList(a) && isImportInfoList(b))
|
|
1212
|
+
return 1;
|
|
1213
|
+
return 0;
|
|
1214
|
+
});
|
|
1215
|
+
if (components.length == 0) {
|
|
1216
|
+
if (debug)
|
|
1217
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} The base type ${baseType} has no components.\n`));
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
if (debug)
|
|
1221
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Generating factory for ${baseType} with the components: ${components.map(x => x.isList == true ? x.component : x.type).join(", ")}.\n`));
|
|
1222
|
+
// Create the file
|
|
1223
|
+
const factoryContent = generateFactory(components, baseType);
|
|
1224
|
+
fs.writeFileSync(baseTypeIndex, factoryContent);
|
|
1225
|
+
process.stdout.write(chalk.yellow(`${figures.arrowRight} Generated partial factory in: ${baseTypeIndex}.\n`));
|
|
1226
|
+
// Build the result
|
|
1227
|
+
return {
|
|
1228
|
+
isList: true,
|
|
1229
|
+
component: baseType + 'Dictionary',
|
|
1230
|
+
prefix: baseType[0].toUpperCase() + baseType.substring(1),
|
|
1231
|
+
path: './' + baseType
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
function generateFactory(components, typeName) {
|
|
1235
|
+
const needsPrefixFunction = components.some(isPrefixedImportInfoList);
|
|
1236
|
+
const factoryContent = `// Auto generated dictionary
|
|
1237
|
+
import { ComponentTypeDictionary } from "@remkoj/optimizely-cms-react";
|
|
1238
|
+
${components.map(x => `import ${x.component} from "${x.path}";`).join("\n")}
|
|
1239
|
+
|
|
1240
|
+
${needsPrefixFunction ? `// Prefix entries - if needed
|
|
1241
|
+
${components.filter(isPrefixedImportInfoList).map(x => Array.isArray(x.prefix) ?
|
|
1242
|
+
x.prefix.map(z => `prefixDictionaryEntries(${x.component}, "${z}");`).join("\n") :
|
|
1243
|
+
`prefixDictionaryEntries(${x.component}, "${x.prefix}");`).join("\n")}
|
|
1244
|
+
|
|
1245
|
+
` : ''}// Build dictionary
|
|
1246
|
+
export const ${typeName}Dictionary : ComponentTypeDictionary = [
|
|
1247
|
+
${components.map(x => x.isList == true ? `...${x.component}` : `{
|
|
1248
|
+
type: "${x.type}",
|
|
1249
|
+
component: ${x.component}
|
|
1250
|
+
}`).join(",\n ")}
|
|
1251
|
+
];
|
|
1252
|
+
|
|
1253
|
+
// Export dictionary
|
|
1254
|
+
export default ${typeName}Dictionary;${needsPrefixFunction ? `
|
|
1255
|
+
|
|
1256
|
+
// Helper functions
|
|
1257
|
+
function prefixDictionaryEntries(list: ComponentTypeDictionary, prefix: string) : ComponentTypeDictionary
|
|
1258
|
+
{
|
|
1259
|
+
list.forEach((component, idx, dictionary) => {
|
|
1260
|
+
dictionary[idx].type = typeof component.type == 'string' ? prefix + "/" + component.type : [ prefix, ...component.type ]
|
|
1261
|
+
});
|
|
1262
|
+
return list;
|
|
1263
|
+
}` : ''}
|
|
1264
|
+
`;
|
|
1265
|
+
return factoryContent;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const NextJsCreateCommand = {
|
|
1269
|
+
command: "nextjs:create",
|
|
1270
|
+
describe: "Scaffold a complete Next.JS / Optimizely Graph structure",
|
|
1271
|
+
builder,
|
|
1272
|
+
handler: async (args) => {
|
|
1273
|
+
const { _config: cfg, components: basePath } = parseArgs(args);
|
|
1274
|
+
const client = createCmsClient(args);
|
|
1275
|
+
const getContentTypesResult = await getContentTypes(client, args);
|
|
1276
|
+
const { contentTypes } = getContentTypesResult;
|
|
1277
|
+
const folders = createTypeFolders(contentTypes, basePath, cfg.debug);
|
|
1278
|
+
process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared context, to limit duplicate work") + "\n");
|
|
1279
|
+
// First create the base files in parallel
|
|
1280
|
+
await Promise.all([
|
|
1281
|
+
TypesPullCommand.handler(args),
|
|
1282
|
+
StylesPullCommand.handler({ ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: [], definitions: true }),
|
|
1283
|
+
NextJsQueriesCommand.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders })
|
|
1284
|
+
]);
|
|
1285
|
+
process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Prepared types, styles and fragments") + "\n");
|
|
1286
|
+
// Then create the components
|
|
1287
|
+
await NextJsComponentsCommand.handler(args, { loadedContentTypes: getContentTypesResult, createdTypeFolders: folders });
|
|
1288
|
+
await NextJsVisualBuilderCommand.handler(args);
|
|
1289
|
+
process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Created components") + "\n");
|
|
1290
|
+
// Finally create the factories
|
|
1291
|
+
await NextJsFactoryCommand.handler(args);
|
|
1292
|
+
process.stdout.write(chalk.yellowBright(chalk.bold(figures.tick) + " Created component factory") + "\n");
|
|
1293
|
+
process.stdout.write("\n");
|
|
1294
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Scaffolded a complete Next.JS / Optimizely Graph structure")) + "\n");
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
|
|
741
1298
|
const CmsVersionCommand = {
|
|
742
1299
|
command: "cms:version",
|
|
743
1300
|
describe: "Get the CMS Version information",
|
|
@@ -766,16 +1323,21 @@ const CmsVersionCommand = {
|
|
|
766
1323
|
|
|
767
1324
|
// Style processing
|
|
768
1325
|
const commands = [
|
|
1326
|
+
StylesCreateCommand,
|
|
769
1327
|
StylesPushCommand,
|
|
770
1328
|
StylesListCommand,
|
|
771
1329
|
StylesPullCommand,
|
|
772
1330
|
TypesPullCommand,
|
|
773
1331
|
TypesPushCommand,
|
|
774
1332
|
NextJsCreateCommand,
|
|
1333
|
+
NextJsQueriesCommand,
|
|
1334
|
+
NextJsComponentsCommand,
|
|
1335
|
+
NextJsVisualBuilderCommand,
|
|
1336
|
+
NextJsFactoryCommand,
|
|
775
1337
|
CmsVersionCommand
|
|
776
1338
|
];
|
|
777
1339
|
|
|
778
|
-
var version = "2.0.
|
|
1340
|
+
var version = "2.0.1";
|
|
779
1341
|
var name = "opti-cms";
|
|
780
1342
|
var APP = {
|
|
781
1343
|
version: version,
|