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