@remkoj/optimizely-cms-cli 6.0.0-pre9 → 6.0.0-rc.3
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/AGENTS.md +68 -0
- package/README.md +348 -38
- package/dist/index.js +1239 -508
- package/dist/index.js.map +1 -1
- package/package.json +33 -17
- package/script/postinstall.mjs +45 -0
package/dist/index.js
CHANGED
|
@@ -1,35 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Developer Utitility providing helpers for common tasks to building a
|
|
4
|
+
* frontend application that uses Optimizely CMS/Graph as content repository.
|
|
5
|
+
*
|
|
6
|
+
* License: Apache 2
|
|
7
|
+
* Copyright (c) 2023-2026 - Remko Jantzen
|
|
8
|
+
*/
|
|
9
|
+
|
|
1
10
|
import { globSync, glob, globIterate } from 'glob';
|
|
2
11
|
import { config } from 'dotenv';
|
|
3
12
|
import { expand } from 'dotenv-expand';
|
|
4
|
-
import { readPartialEnvConfig, createClient, OptiCmsVersion, ContentRoots } from '@remkoj/optimizely-cms-api';
|
|
5
|
-
import yargs from 'yargs';
|
|
6
|
-
import chalk from 'chalk';
|
|
7
13
|
import path from 'node:path';
|
|
8
14
|
import fs from 'node:fs';
|
|
15
|
+
import { readPartialEnvConfig, createClient, ContentRoots } from '@remkoj/optimizely-cms-api';
|
|
16
|
+
import yargs from 'yargs';
|
|
17
|
+
import chalk from 'chalk';
|
|
9
18
|
import figures from 'figures';
|
|
10
19
|
import Table from 'cli-table3';
|
|
11
|
-
import
|
|
20
|
+
import slugify from '@sindresorhus/slugify';
|
|
21
|
+
import { diff } from 'deep-object-diff';
|
|
22
|
+
import fs$1 from 'node:fs/promises';
|
|
12
23
|
import { input, select, confirm } from '@inquirer/prompts';
|
|
13
24
|
import createDeepMerge from '@fastify/deepmerge';
|
|
14
25
|
import { Ajv } from 'ajv';
|
|
15
26
|
import addFormats from 'ajv-formats';
|
|
16
|
-
import
|
|
27
|
+
import { DocumentGenerator, PropertyCollisionTracker } from '@remkoj/optimizely-graph-functions/generate';
|
|
17
28
|
import deepEqual from 'fast-deep-equal';
|
|
18
29
|
|
|
30
|
+
function getProjectDir() {
|
|
31
|
+
let testPath = process.cwd();
|
|
32
|
+
let hasPackageJson = false;
|
|
33
|
+
do {
|
|
34
|
+
hasPackageJson = fs.statSync(path.join(testPath, 'package.json'), { throwIfNoEntry: false })?.isFile() ?? false;
|
|
35
|
+
if (!hasPackageJson)
|
|
36
|
+
testPath = path.normalize(path.join(testPath, '..'));
|
|
37
|
+
} while (!hasPackageJson && testPath.length > 2);
|
|
38
|
+
if (!hasPackageJson) {
|
|
39
|
+
throw new Error('No package.json found!');
|
|
40
|
+
}
|
|
41
|
+
return testPath;
|
|
42
|
+
}
|
|
19
43
|
/**
|
|
20
44
|
* Prepare the application context, by parsing the .env files in the main
|
|
21
45
|
* application directory.
|
|
22
46
|
*
|
|
23
47
|
* @returns A string array with the files processed
|
|
24
48
|
*/
|
|
25
|
-
function prepare() {
|
|
26
|
-
const
|
|
27
|
-
const envFiles =
|
|
49
|
+
function prepare(pDir) {
|
|
50
|
+
const projectDir = pDir ?? getProjectDir();
|
|
51
|
+
const envFiles = globSync(".env*", { cwd: projectDir })
|
|
52
|
+
.sort((a, b) => b.length - a.length)
|
|
53
|
+
.filter(n => n == ".env" || n == ".env.local" || (process.env.NODE_ENV && n == `.env.${process.env.NODE_ENV}.local`));
|
|
28
54
|
expand(config({ path: envFiles, debug: false, quiet: true }));
|
|
29
55
|
return envFiles;
|
|
30
56
|
}
|
|
31
57
|
|
|
32
|
-
function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
|
|
58
|
+
function createOptiCmsApp(scriptName, version, epilogue, envFiles, projectDir) {
|
|
33
59
|
if (envFiles) {
|
|
34
60
|
process.stdout.write(chalk.bold(`✅ Loaded environment files:`) + "\n");
|
|
35
61
|
envFiles.forEach(envFile => {
|
|
@@ -45,11 +71,12 @@ function createOptiCmsApp(scriptName, version, epilogue, envFiles) {
|
|
|
45
71
|
console.error(chalk.redBright(`${chalk.bold("[ERROR]:")} Error processing environment variables: ${e.message}`));
|
|
46
72
|
process.exit(1);
|
|
47
73
|
}
|
|
74
|
+
const defaultPath = projectDir ?? process.cwd();
|
|
48
75
|
return yargs(process.argv)
|
|
49
76
|
.scriptName(scriptName)
|
|
50
77
|
.version(version)
|
|
51
78
|
.usage('$0 <cmd> [args]')
|
|
52
|
-
.option("path", { alias: "p", description: "Application root folder", string: true, type: "string", demandOption: false, default:
|
|
79
|
+
.option("path", { alias: "p", description: "Application root folder", string: true, type: "string", demandOption: false, default: defaultPath })
|
|
53
80
|
.option("components", { alias: "c", description: "Path to components folder", string: true, type: "string", demandOption: false, default: "./src/components/cms" })
|
|
54
81
|
.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 })
|
|
55
82
|
.option("client_id", { alias: "ci", description: "API Client ID", string: true, type: "string", demandOption: isDemanded(config.clientId), default: config.clientId })
|
|
@@ -133,84 +160,6 @@ function getCmsIntegrationApiOptions(args) {
|
|
|
133
160
|
return parseArgs(args)._config;
|
|
134
161
|
}
|
|
135
162
|
|
|
136
|
-
const StylesPushCommand = {
|
|
137
|
-
command: "styles:push",
|
|
138
|
-
describe: "Push Visual Builder style definitions into the CMS (create/replace)",
|
|
139
|
-
builder: (yargs) => {
|
|
140
|
-
yargs.option('excludeTemplates', { alias: 'e', description: "Exclude these templates", string: true, type: 'array', demandOption: false, default: [] });
|
|
141
|
-
yargs.option("templates", { alias: 't', description: "Select only these templates", string: true, type: 'array', demandOption: false, default: [] });
|
|
142
|
-
return yargs;
|
|
143
|
-
},
|
|
144
|
-
handler: async (args) => {
|
|
145
|
-
const { _config: cfg, excludeTemplates, templates, ...opts } = parseArgs(args);
|
|
146
|
-
const client = createCmsClient(args);
|
|
147
|
-
if (client.runtimeCmsVersion == OptiCmsVersion.CMS12) {
|
|
148
|
-
process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/replace) DisplayStyles into Optimizely CMS\n`));
|
|
152
|
-
const styleDefinitionFiles = await glob("./**/*.opti-style.json", {
|
|
153
|
-
cwd: opts.components
|
|
154
|
-
});
|
|
155
|
-
const results = (await Promise.all(styleDefinitionFiles.map(async (styleDefinitionFile) => {
|
|
156
|
-
const filePath = path.normalize(path.join(opts.components, styleDefinitionFile));
|
|
157
|
-
const styleDefinition = tryReadJsonFile(filePath, cfg.debug);
|
|
158
|
-
const styleKey = styleDefinition.key;
|
|
159
|
-
if (!styleKey) {
|
|
160
|
-
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`));
|
|
161
|
-
return undefined;
|
|
162
|
-
}
|
|
163
|
-
if (excludeTemplates.includes(styleKey))
|
|
164
|
-
return undefined; // Skip excluded styles
|
|
165
|
-
if (templates.length > 0 && !templates.includes(styleKey))
|
|
166
|
-
return undefined; // Only include defined styles, if any
|
|
167
|
-
if (cfg.debug)
|
|
168
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Pushing: ${styleKey}\n`));
|
|
169
|
-
// Try to fetch the current template
|
|
170
|
-
const currentTemplate = await client.displayTemplatesGet({ path: { key: styleKey } }).catch(() => { return undefined; });
|
|
171
|
-
// Create / Replace the current template
|
|
172
|
-
const newTemplate = await (currentTemplate ?
|
|
173
|
-
client.displayTemplatesPatch({ path: { key: styleKey }, body: styleDefinition }) :
|
|
174
|
-
client.displayTemplatesCreate({ body: styleDefinition }));
|
|
175
|
-
return newTemplate;
|
|
176
|
-
}))).filter(isNotNullOrUndefined);
|
|
177
|
-
const styles = new Table({
|
|
178
|
-
head: [
|
|
179
|
-
chalk.yellow(chalk.bold("Name")),
|
|
180
|
-
chalk.yellow(chalk.bold("Key")),
|
|
181
|
-
chalk.yellow(chalk.bold("Default")),
|
|
182
|
-
chalk.yellow(chalk.bold("Target"))
|
|
183
|
-
],
|
|
184
|
-
colWidths: [31, 20, 9, 20],
|
|
185
|
-
colAligns: ["left", "left", "center", "left"]
|
|
186
|
-
});
|
|
187
|
-
results.forEach(tpl => {
|
|
188
|
-
styles.push([
|
|
189
|
-
tpl.displayName,
|
|
190
|
-
tpl.key,
|
|
191
|
-
tpl.isDefault ? figures.tick : figures.cross,
|
|
192
|
-
tpl.contentType ? `${tpl.contentType} (C)` : tpl.baseType ? `${tpl.baseType} (B)` : `${tpl.nodeType} (N)`
|
|
193
|
-
]);
|
|
194
|
-
});
|
|
195
|
-
process.stdout.write(styles.toString() + "\n");
|
|
196
|
-
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
function tryReadJsonFile(filePath, debug = false) {
|
|
200
|
-
try {
|
|
201
|
-
if (debug)
|
|
202
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Reading style definition from ${filePath}\n`));
|
|
203
|
-
return JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' }));
|
|
204
|
-
}
|
|
205
|
-
catch (e) {
|
|
206
|
-
process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} Error while reading ${filePath}\n`));
|
|
207
|
-
}
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
function isNotNullOrUndefined(i) {
|
|
211
|
-
return i ? true : false;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
163
|
/**
|
|
215
164
|
* This file contains tools that allow using the project that we're targeting
|
|
216
165
|
*/
|
|
@@ -223,8 +172,12 @@ function isNotNullOrUndefined(i) {
|
|
|
223
172
|
* @returns
|
|
224
173
|
*/
|
|
225
174
|
function getContentTypePaths(contentType, basePath, createFolder = false, debug = false) {
|
|
226
|
-
const baseTypeSlug =
|
|
227
|
-
|
|
175
|
+
const baseTypeSlug = keyToSlug(contentType.baseType, {
|
|
176
|
+
defaultKey: 'global',
|
|
177
|
+
stripLeadingUnderscore: true
|
|
178
|
+
});
|
|
179
|
+
const typeSlug = keyToSlug(contentType.key);
|
|
180
|
+
const typePath = path.join(basePath, baseTypeSlug, typeSlug);
|
|
228
181
|
if (createFolder) {
|
|
229
182
|
if (!fs.existsSync(typePath)) {
|
|
230
183
|
fs.mkdirSync(typePath, { recursive: true });
|
|
@@ -235,10 +188,10 @@ function getContentTypePaths(contentType, basePath, createFolder = false, debug
|
|
|
235
188
|
if (!fs.statSync(typePath).isDirectory())
|
|
236
189
|
throw new Error(`The folder ${typePath} for Content-Type ${contentType.key} exists, but is not a directory!`);
|
|
237
190
|
}
|
|
238
|
-
const typeFile = path.join(typePath, `${
|
|
239
|
-
const fragmentFile = path.join(typePath, `${
|
|
240
|
-
const propertyFragmentFile = path.join(typePath, `${
|
|
241
|
-
const queryFile = path.join(typePath,
|
|
191
|
+
const typeFile = path.join(typePath, `${typeSlug}.opti-type.json`);
|
|
192
|
+
const fragmentFile = path.join(typePath, `${typeSlug}.${baseTypeSlug}.graphql`);
|
|
193
|
+
const propertyFragmentFile = path.join(typePath, `${typeSlug}.property.graphql`);
|
|
194
|
+
const queryFile = path.join(typePath, `${typeSlug}.query.graphql`);
|
|
242
195
|
const componentFile = path.join(typePath, `index.tsx`);
|
|
243
196
|
return {
|
|
244
197
|
type: contentType.key,
|
|
@@ -251,16 +204,187 @@ function getContentTypePaths(contentType, basePath, createFolder = false, debug
|
|
|
251
204
|
queryFile
|
|
252
205
|
};
|
|
253
206
|
}
|
|
207
|
+
function getStyleFilePathsSync(displayTemplate, contentBaseType = '_Component', basePath = './src/components/cms', createFolder = false) {
|
|
208
|
+
const displayTemplateType = getDisplayTemplateType(displayTemplate);
|
|
209
|
+
let target;
|
|
210
|
+
let groupPath;
|
|
211
|
+
switch (displayTemplateType) {
|
|
212
|
+
case 'base':
|
|
213
|
+
groupPath = path.join(keyToSlug(displayTemplate.baseType, { stripLeadingUnderscore: true }), 'styles');
|
|
214
|
+
target = displayTemplate.baseType;
|
|
215
|
+
break;
|
|
216
|
+
case 'node':
|
|
217
|
+
groupPath = path.join('nodes', keyToSlug(displayTemplate.nodeType));
|
|
218
|
+
target = displayTemplate.nodeType;
|
|
219
|
+
break;
|
|
220
|
+
case 'component':
|
|
221
|
+
{
|
|
222
|
+
const baseType = contentBaseType;
|
|
223
|
+
target = displayTemplate.contentType;
|
|
224
|
+
groupPath = path.join(keyToSlug(baseType, { stripLeadingUnderscore: true }), keyToSlug(displayTemplate.contentType, { stripLeadingGroup: true }));
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
default:
|
|
228
|
+
throw new Error(`Unsupported DisplayTemplate target for ${displayTemplate.key}`);
|
|
229
|
+
}
|
|
230
|
+
const displayTemplateKeySlug = keyToSlug(displayTemplate.key ?? 'displayTemplate', { stripLeadingGroup: true });
|
|
231
|
+
const groupFolder = path.join(basePath, groupPath);
|
|
232
|
+
const styleFolder = displayTemplateType === 'component' ? groupFolder : path.join(groupFolder, displayTemplateKeySlug);
|
|
233
|
+
if (createFolder && !fs.existsSync(styleFolder))
|
|
234
|
+
fs.mkdirSync(styleFolder, { recursive: true });
|
|
235
|
+
return {
|
|
236
|
+
styleFile: path.join(styleFolder, displayTemplateKeySlug + '.opti-style.json'),
|
|
237
|
+
helperFile: path.join(groupFolder, 'displayTemplates.ts'),
|
|
238
|
+
identifier: `${displayTemplateType}/${target}`,
|
|
239
|
+
helperFolder: groupFolder,
|
|
240
|
+
styleFolder
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
async function getStyleFilePaths(definition, opts) {
|
|
244
|
+
let defintionBaseType;
|
|
245
|
+
if (definition.contentType && !opts.contentBaseType && opts.client) {
|
|
246
|
+
const contentType = await opts.client.contentTypesGet({ path: { key: definition.contentType } }).catch(() => undefined);
|
|
247
|
+
defintionBaseType = contentType?.baseType;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
defintionBaseType = opts.contentBaseType;
|
|
251
|
+
}
|
|
252
|
+
return getStyleFilePathsSync(definition, defintionBaseType, opts.basePath, opts.createFolder);
|
|
253
|
+
}
|
|
254
254
|
/**
|
|
255
|
+
* Simple logic to create path slugs for storing ContentType related files on
|
|
256
|
+
* disk.
|
|
255
257
|
*
|
|
256
|
-
* @param
|
|
258
|
+
* @param typeKey
|
|
259
|
+
* @param stripLeadingUnderscore
|
|
257
260
|
* @returns
|
|
258
261
|
*/
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
function keyToSlug(typeKey, options = {}) {
|
|
263
|
+
// Destruct the configuration into the parts we need
|
|
264
|
+
const { stripLeadingGroup = true, defaultKey = 'unknown', stripLeadingUnderscore = false, ...slugifyBaseConfig } = options;
|
|
265
|
+
const slugifyConfig = {
|
|
266
|
+
...slugifyBaseConfig,
|
|
267
|
+
preserveLeadingUnderscore: !stripLeadingUnderscore
|
|
268
|
+
};
|
|
269
|
+
// First make sure we have a valid input type
|
|
270
|
+
if (!(typeof typeKey === 'string' || typeKey === undefined || typeKey === null))
|
|
271
|
+
throw new Error(`Invalid typeKey provided, expected an optional string, received a value of type ${typeof typeKey}`);
|
|
272
|
+
// Then, take the prefix out, if any and required
|
|
273
|
+
const toSlugify = stripLeadingGroup && typeKey?.indexOf(":") >= 0 ? typeKey.split(":", 2).at(1) : typeKey;
|
|
274
|
+
// Finally slugify the result
|
|
275
|
+
return slugify(toSlugify ?? defaultKey, slugifyConfig);
|
|
276
|
+
}
|
|
277
|
+
function getDisplayTemplateType(displayTemplate) {
|
|
278
|
+
const templateType = displayTemplate.baseType ? 'base' : displayTemplate.nodeType ? 'node' : displayTemplate.contentType ? 'component' : 'unknown';
|
|
279
|
+
return templateType;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Test if the value must be included in a set, by defining allowed values and blocked values. A value
|
|
284
|
+
* is considered be eligible to be included when it's both allowed an not disallowed.
|
|
285
|
+
*
|
|
286
|
+
* @param value The value to test
|
|
287
|
+
* @param allow The list of allowed values, when not provided, or and empty array, all
|
|
288
|
+
* values will be allowed.
|
|
289
|
+
* @param disallow The list of explicitly disallowed values, yielding an "all allowed but
|
|
290
|
+
* these" operation
|
|
291
|
+
* @param compareSlugified When set to `true`, and after test with `includes()` on the allow and
|
|
292
|
+
* disallow did not yield a match, will try to match by converting the
|
|
293
|
+
* values to string (using the `.toString()` method), then slugify them with
|
|
294
|
+
* the `keyToSlug` method and then comparing the outcomes.
|
|
295
|
+
* @param slugifyOptions The options to provide to the `keyToSlug` method, this will be ignored
|
|
296
|
+
* unless you set the `compareSlugified` parameter to `true`
|
|
297
|
+
* @returns If the value should be included in the result, based upon the `allow` and
|
|
298
|
+
* `disallow` configuration.
|
|
299
|
+
*
|
|
300
|
+
* @see {@link keyToSlug}
|
|
301
|
+
*/
|
|
302
|
+
function shouldInclude(value, allow, disallow, compareSlugified = false, slugifyOptions) {
|
|
303
|
+
// Do not include null or undefined values
|
|
304
|
+
if (value === null || value === undefined)
|
|
305
|
+
return false;
|
|
306
|
+
// Item is allowed when either allow is not set, an empty array or has the value
|
|
307
|
+
const isAllowed = !Array.isArray(allow) || allow.length === 0 || allow.includes(value) || (compareSlugified && allow.some(av => keyToSlug(av.toString(), slugifyOptions) === keyToSlug(value.toString(), slugifyOptions)));
|
|
308
|
+
// Item is disallowed when and the array is set and includes the value
|
|
309
|
+
const isDisallowed = Array.isArray(disallow) && (disallow.includes(value) || (compareSlugified && disallow.some(av => keyToSlug(av.toString(), slugifyOptions) === keyToSlug(value.toString(), slugifyOptions))));
|
|
310
|
+
// Return the outcome
|
|
311
|
+
return isAllowed && !isDisallowed;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Test if the provided content type is a contract
|
|
315
|
+
*
|
|
316
|
+
* @param contentType The ContentType to test
|
|
317
|
+
* @returns `true` if the ContentType is a contract, `false` otherwise
|
|
318
|
+
* @see {@link IntegrationApi.ContentType}
|
|
319
|
+
*/
|
|
320
|
+
function isContract(contentType) {
|
|
321
|
+
if (typeof contentType !== 'object' || contentType === null)
|
|
322
|
+
return false;
|
|
323
|
+
return contentType.isContract || contentType.source === 'globalcontract';
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Test if the provided content type comes from the Graph content source (e.g. external content)
|
|
327
|
+
*
|
|
328
|
+
* @param contentType The ContentType to test
|
|
329
|
+
* @returns `true` if the ContentType is a graph type, `false` otherwise
|
|
330
|
+
* @see {@link IntegrationApi.ContentType}
|
|
331
|
+
*/
|
|
332
|
+
function isGraphType(contentType) {
|
|
333
|
+
if (typeof contentType !== 'object' || contentType === null)
|
|
334
|
+
return false;
|
|
335
|
+
return (contentType.key && typeof (contentType.key) === 'string' && contentType.key.startsWith('graph:')) || contentType.source === 'graph';
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Test if the provided content type is a folder, which is a structural element for editors, but
|
|
339
|
+
* does not represent anything significant when working with ContentType
|
|
340
|
+
*
|
|
341
|
+
* @param contentType The ContentType to test
|
|
342
|
+
* @returns `true` if the ContentType is a folder, `false` otherwise
|
|
343
|
+
* @see {@link IntegrationApi.ContentType}
|
|
344
|
+
*/
|
|
345
|
+
function isFolder(contentType) {
|
|
346
|
+
if (typeof contentType !== 'object' || contentType === null)
|
|
347
|
+
return false;
|
|
348
|
+
return contentType.baseType === '_folder';
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Test if the provided content type is a system type
|
|
352
|
+
*
|
|
353
|
+
* @param contentType The ContentType to test
|
|
354
|
+
* @returns `true` if the ContentType is a system type, `false` otherwise
|
|
355
|
+
* @see {@link IntegrationApi.ContentType}
|
|
356
|
+
*/
|
|
357
|
+
function isSystemType(contentType) {
|
|
358
|
+
if (typeof contentType !== 'object' || contentType === null)
|
|
359
|
+
return false;
|
|
360
|
+
return contentType.source === 'system';
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Type guard that returns `true` when `toTest` is a non-empty array.
|
|
364
|
+
*
|
|
365
|
+
* @param toTest The value to inspect.
|
|
366
|
+
* @returns `true` when `toTest` is an `Array` with at least one element.
|
|
367
|
+
*/
|
|
368
|
+
function isNonEmptyArray(toTest) {
|
|
369
|
+
return Array.isArray(toTest) && toTest.length > 0;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Type guard that returns `true` when `toTest` is `null`, `undefined`, or an empty array.
|
|
373
|
+
*
|
|
374
|
+
* @param toTest The value to inspect.
|
|
375
|
+
* @returns `true` when `toTest` is `null` or an `Array` with no elements.
|
|
376
|
+
*/
|
|
377
|
+
function isEmptyArray(toTest) {
|
|
378
|
+
return toTest === null || !Array.isArray(toTest) || toTest.length === 0;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Type guard that returns `true` when `toTest` is neither `null` nor `undefined`.
|
|
382
|
+
*
|
|
383
|
+
* @param toTest The value to inspect.
|
|
384
|
+
* @returns `true` when `toTest` is a defined, non-null value of type `T`.
|
|
385
|
+
*/
|
|
386
|
+
function isDefined(toTest) {
|
|
387
|
+
return toTest !== null && toTest !== undefined;
|
|
264
388
|
}
|
|
265
389
|
|
|
266
390
|
const ContentTypesArgsDefaults = {
|
|
@@ -283,28 +407,41 @@ async function getContentTypes(client, args, pageSize = 25, allowSystem = false,
|
|
|
283
407
|
const allContentTypes = [];
|
|
284
408
|
const filteredContentTypes = [];
|
|
285
409
|
for await (const contentType of getAllContentTypes(client, cfg.debug, pageSize)) {
|
|
286
|
-
// Skip
|
|
287
|
-
if (!all && contentType
|
|
410
|
+
// Skip contracts by default as they're abstract classes and cannot be used to directly store data.
|
|
411
|
+
if (!all && isContract(contentType)) {
|
|
288
412
|
if (cfg.debug)
|
|
289
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it is a
|
|
413
|
+
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`));
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
// Skip content types mapped against Graph data, these should be used with their source type in Graph,
|
|
417
|
+
// not the reference in CMS
|
|
418
|
+
if (!all && isGraphType(contentType)) {
|
|
419
|
+
if (cfg.debug)
|
|
420
|
+
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`));
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
// Skip folder types as these are non-data carrying types in the instance.
|
|
424
|
+
if (!all && isFolder(contentType)) {
|
|
425
|
+
if (cfg.debug)
|
|
426
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it is a folder, use --all to include\n`));
|
|
290
427
|
continue;
|
|
291
428
|
}
|
|
292
429
|
// Build the unfiltered array
|
|
293
430
|
allContentTypes.push(contentType);
|
|
294
431
|
// Skip based upon base type filters
|
|
295
|
-
if (!shouldInclude(
|
|
432
|
+
if (!shouldInclude(contentType.baseType, baseTypes, excludeBaseTypes, true)) {
|
|
296
433
|
if (cfg.debug)
|
|
297
434
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it has a restricted base type: ${contentType.baseType}\n`));
|
|
298
435
|
continue;
|
|
299
436
|
}
|
|
300
437
|
// Skip based upon type filters
|
|
301
|
-
if (!shouldInclude(contentType.key, types, excludeTypes)) {
|
|
438
|
+
if (!shouldInclude(contentType.key, types, excludeTypes, true)) {
|
|
302
439
|
if (cfg.debug)
|
|
303
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it
|
|
440
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${contentType.key} as it is a restricted type (${types.join(', ')})(${excludeTypes.join(', ')})\n`));
|
|
304
441
|
continue;
|
|
305
442
|
}
|
|
306
443
|
// Skip based upon system filter
|
|
307
|
-
if (
|
|
444
|
+
if (!allowSystem && isSystemType(contentType)) {
|
|
308
445
|
if (cfg.debug)
|
|
309
446
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Content-Type ${contentType.key} due to it being a system type\n`));
|
|
310
447
|
continue;
|
|
@@ -322,13 +459,6 @@ async function getContentTypes(client, args, pageSize = 25, allowSystem = false,
|
|
|
322
459
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Applied content type filters, reduced from ${allContentTypes.length} to ${filteredContentTypes.length} items\n`));
|
|
323
460
|
return { all: allContentTypes, contentTypes: filteredContentTypes };
|
|
324
461
|
}
|
|
325
|
-
function shouldInclude(value, allow, disallow) {
|
|
326
|
-
// Item is allowed when either allow is not set, an empty array or has the value
|
|
327
|
-
const isAllowed = !Array.isArray(allow) || allow.length === 0 || allow.includes(value);
|
|
328
|
-
// Item is disallowed when and the array is set and includes the value
|
|
329
|
-
const isDisallowed = Array.isArray(disallow) && disallow.includes(value);
|
|
330
|
-
return isAllowed && !isDisallowed;
|
|
331
|
-
}
|
|
332
462
|
/**
|
|
333
463
|
* Retrieve all content types as an Async Generator, allowing processing of entries whilest they are being loaded from the CMS instance.
|
|
334
464
|
*
|
|
@@ -340,25 +470,28 @@ async function* getAllContentTypes(client, debug = false, pageSize = 25) {
|
|
|
340
470
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Content Types from Optimizely CMS\n`));
|
|
341
471
|
let requestPageSize = pageSize;
|
|
342
472
|
let requestPageIndex = 0;
|
|
343
|
-
let totalItemCount
|
|
344
|
-
let totalPages
|
|
473
|
+
let totalItemCount;
|
|
474
|
+
let totalPages;
|
|
345
475
|
do {
|
|
346
|
-
const resultsPage = await client.contentTypesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch((
|
|
476
|
+
const resultsPage = await client.contentTypesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch(() => {
|
|
347
477
|
return {
|
|
348
478
|
items: [],
|
|
349
479
|
totalItemCount: 0,
|
|
480
|
+
totalCount: 0,
|
|
350
481
|
pageIndex: requestPageIndex,
|
|
351
482
|
pageSize: requestPageSize
|
|
352
483
|
};
|
|
353
484
|
});
|
|
485
|
+
console.log(resultsPage);
|
|
354
486
|
// Calculate fields for next page
|
|
355
|
-
|
|
487
|
+
//@ts-expect-error There's a difference between the SaaS & PaaS API, hence we're ignoring the next line
|
|
488
|
+
totalItemCount = resultsPage.totalItemCount ?? resultsPage.totalCount ?? 0;
|
|
356
489
|
requestPageSize = resultsPage.pageSize;
|
|
357
490
|
requestPageIndex = resultsPage.pageIndex + 1;
|
|
358
491
|
totalPages = Math.ceil(totalItemCount / requestPageSize);
|
|
359
492
|
// Debug output
|
|
360
493
|
if (debug)
|
|
361
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched contentTypes page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page)\n`));
|
|
494
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched contentTypes page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page, ${totalItemCount} items available)\n`));
|
|
362
495
|
// Yield items
|
|
363
496
|
for (const contentType of (resultsPage.items ?? [])) {
|
|
364
497
|
yield contentType;
|
|
@@ -376,50 +509,60 @@ const stylesBuilder = yargs => {
|
|
|
376
509
|
return newArgs;
|
|
377
510
|
};
|
|
378
511
|
async function getStyles(client, args, pageSize = 25) {
|
|
379
|
-
|
|
380
|
-
return { all: [], styles: [] };
|
|
381
|
-
const { _config: cfg, excludeBaseTypes, excludeTypes, excludeNodeTypes, excludeTemplates, baseTypes, types, nodes, templates, templateTypes } = parseArgs(args);
|
|
512
|
+
const { _config: cfg, excludeBaseTypes: disallowBaseTypes, excludeTypes: disallowTypes, excludeNodeTypes: disallowNodeTypes, excludeTemplates: disallowTemplates, baseTypes: allowBaseTypes, types: allowTypes, nodes: allowNodeTypes, templates: allowTemplates, templateTypes: baseAllowTemplateTypes } = parseArgs(args);
|
|
382
513
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pulling Style-Definitions from Optimizely CMS\n`));
|
|
514
|
+
const allowTemplateTypes = [];
|
|
515
|
+
if (!Array.isArray(baseAllowTemplateTypes) || baseAllowTemplateTypes.length === 0) {
|
|
516
|
+
if (isNonEmptyArray(allowBaseTypes)) {
|
|
517
|
+
console.log('BT', allowBaseTypes);
|
|
518
|
+
if (cfg.debug)
|
|
519
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Base picking filter is active, adjusting template type filter to pick templates targeting a base type\n`));
|
|
520
|
+
allowTemplateTypes.push('base');
|
|
521
|
+
}
|
|
522
|
+
if (isNonEmptyArray(allowNodeTypes)) {
|
|
523
|
+
if (cfg.debug)
|
|
524
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Node type filter active, adjusting template type filter\n`));
|
|
525
|
+
allowTemplateTypes.push('node');
|
|
526
|
+
}
|
|
527
|
+
if (isNonEmptyArray(allowTypes)) {
|
|
528
|
+
if (cfg.debug)
|
|
529
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Component type filter active, adjusting template type filter\n`));
|
|
530
|
+
allowTemplateTypes.push('component');
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
allowTemplateTypes.push(...baseAllowTemplateTypes);
|
|
535
|
+
}
|
|
383
536
|
const allDisplayTemplates = [];
|
|
384
537
|
const filteredDisplayTemplates = [];
|
|
385
538
|
for await (const displayTemplate of getAllStyles(client, cfg.debug, pageSize)) {
|
|
386
539
|
allDisplayTemplates.push(displayTemplate);
|
|
387
|
-
const templateType = displayTemplate
|
|
388
|
-
if (
|
|
540
|
+
const templateType = getDisplayTemplateType(displayTemplate);
|
|
541
|
+
if (!shouldInclude(displayTemplate.key, allowTemplates, disallowTemplates)) {
|
|
389
542
|
if (cfg.debug)
|
|
390
543
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style defintion key filtering active\n`));
|
|
391
544
|
continue;
|
|
392
545
|
}
|
|
393
|
-
if (
|
|
546
|
+
if (!shouldInclude(templateType, allowTemplateTypes)) {
|
|
394
547
|
if (cfg.debug)
|
|
395
548
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style type filtering is active\n`));
|
|
396
549
|
continue;
|
|
397
550
|
}
|
|
398
|
-
if (
|
|
551
|
+
if (templateType === 'base' && !shouldInclude(displayTemplate.baseType, allowBaseTypes, disallowBaseTypes)) {
|
|
399
552
|
if (cfg.debug)
|
|
400
553
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is defined at base type level and base type filtering is active\n`));
|
|
401
554
|
continue;
|
|
402
555
|
}
|
|
403
|
-
if (
|
|
556
|
+
if (templateType === 'component' && !shouldInclude(displayTemplate.contentType, allowTypes, disallowTypes)) {
|
|
404
557
|
if (cfg.debug)
|
|
405
558
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is defined at component type level and component type filtering is active\n`));
|
|
406
559
|
continue;
|
|
407
560
|
}
|
|
408
|
-
if (templateType
|
|
409
|
-
if (cfg.debug)
|
|
410
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is targeting the ${templateType} level and component type selection is active\n`));
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
if (displayTemplate.nodeType && isExcluded(displayTemplate.nodeType, excludeNodeTypes, nodes)) {
|
|
561
|
+
if (templateType === 'node' && !shouldInclude(displayTemplate.nodeType, allowNodeTypes, disallowNodeTypes)) {
|
|
414
562
|
if (cfg.debug)
|
|
415
563
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is defined at node type level and node type filtering is active\n`));
|
|
416
564
|
continue;
|
|
417
565
|
}
|
|
418
|
-
if (templateType != 'node' && nodes.length > 0) {
|
|
419
|
-
if (cfg.debug)
|
|
420
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping Style-Defintion ${displayTemplate.key} - Style is targeting the ${templateType} level and node type selection is active\n`));
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
566
|
filteredDisplayTemplates.push(displayTemplate);
|
|
424
567
|
}
|
|
425
568
|
if (cfg.debug)
|
|
@@ -430,14 +573,12 @@ async function getStyles(client, args, pageSize = 25) {
|
|
|
430
573
|
};
|
|
431
574
|
}
|
|
432
575
|
async function* getAllStyles(client, debug = false, pageSize = 5) {
|
|
433
|
-
if (client.runtimeCmsVersion == OptiCmsVersion.CMS12)
|
|
434
|
-
return;
|
|
435
576
|
let requestPageSize = pageSize;
|
|
436
577
|
let requestPageIndex = 0;
|
|
437
|
-
let totalItemCount
|
|
438
|
-
let totalPages
|
|
578
|
+
let totalItemCount;
|
|
579
|
+
let totalPages;
|
|
439
580
|
do {
|
|
440
|
-
const resultsPage = await client.displayTemplatesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch((
|
|
581
|
+
const resultsPage = await client.displayTemplatesList({ query: { pageIndex: requestPageIndex, pageSize: requestPageSize } }).catch(() => {
|
|
441
582
|
return {
|
|
442
583
|
items: [],
|
|
443
584
|
totalItemCount: 0,
|
|
@@ -446,39 +587,298 @@ async function* getAllStyles(client, debug = false, pageSize = 5) {
|
|
|
446
587
|
};
|
|
447
588
|
});
|
|
448
589
|
// Calculate fields for next page
|
|
449
|
-
|
|
590
|
+
//@ts-expect-error Difference between PaaS & SaaS
|
|
591
|
+
totalItemCount = resultsPage.totalItemCount ?? resultsPage.totalCount ?? 0;
|
|
450
592
|
requestPageSize = resultsPage.pageSize;
|
|
451
593
|
requestPageIndex = resultsPage.pageIndex + 1;
|
|
452
594
|
totalPages = Math.ceil(totalItemCount / requestPageSize);
|
|
453
595
|
// Debug output
|
|
454
596
|
if (debug)
|
|
455
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched displayTemplates page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page)\n`));
|
|
597
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Fetched displayTemplates page ${requestPageIndex} of ${totalPages} (${requestPageSize} items per page, ${totalItemCount} items available)\n`));
|
|
456
598
|
// Yield items
|
|
457
599
|
for (const displayTemplate of (resultsPage.items ?? [])) {
|
|
458
600
|
yield displayTemplate;
|
|
459
601
|
}
|
|
460
602
|
} while (requestPageIndex < totalPages);
|
|
461
603
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
604
|
+
class TypeFilesList extends Map {
|
|
605
|
+
getDisplayTemplateByKey(displayTemplateKey) {
|
|
606
|
+
for (const groupKey of this.keys()) {
|
|
607
|
+
const templates = this.get(groupKey)?.templates || [];
|
|
608
|
+
const displayTemplate = templates.find(x => x.key === displayTemplateKey);
|
|
609
|
+
if (displayTemplate)
|
|
610
|
+
return displayTemplate.data;
|
|
611
|
+
}
|
|
612
|
+
return undefined;
|
|
613
|
+
}
|
|
614
|
+
getDisplayTemplatePathsByKey(displayTemplateKey) {
|
|
615
|
+
for (const identifier of this.keys()) {
|
|
616
|
+
const groupInfo = this.get(identifier);
|
|
617
|
+
const templates = groupInfo?.templates || [];
|
|
618
|
+
const helperFile = groupInfo?.filePath;
|
|
619
|
+
const helperFolder = groupInfo?.fileFolder;
|
|
620
|
+
const info = templates.find(x => x.key === displayTemplateKey);
|
|
621
|
+
if (info)
|
|
622
|
+
return {
|
|
623
|
+
styleFile: info.file,
|
|
624
|
+
styleFolder: info.folder,
|
|
625
|
+
identifier,
|
|
626
|
+
helperFile,
|
|
627
|
+
helperFolder
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function toTypeFilesList(displayTemplates, client, basePath) {
|
|
634
|
+
if (isNonEmptyArray(displayTemplates)) {
|
|
635
|
+
// Get all content types we need to get the type file lists
|
|
636
|
+
const targetContentTypes = (await Promise.allSettled(displayTemplates.map(async (displayTemplate) => {
|
|
637
|
+
if (!displayTemplate.contentType)
|
|
638
|
+
return undefined;
|
|
639
|
+
return client.contentTypesGet({ path: { key: displayTemplate.contentType } });
|
|
640
|
+
}))).map(x => x.status == 'fulfilled' ? x.value : undefined).filter(isDefined);
|
|
641
|
+
// Simple helper to get the base type from the list
|
|
642
|
+
function getBaseTypeOf(contentTypeKey) {
|
|
643
|
+
if (!contentTypeKey)
|
|
644
|
+
return undefined;
|
|
645
|
+
return targetContentTypes.find(x => x.key === contentTypeKey)?.baseType;
|
|
646
|
+
}
|
|
647
|
+
// Now reduce the list into the Map we need
|
|
648
|
+
return displayTemplates.reduce((aggregator, displayTemplate) => {
|
|
649
|
+
const contentTypeBaseType = getBaseTypeOf(displayTemplate.contentType);
|
|
650
|
+
const { identifier, helperFile, helperFolder, styleFile, styleFolder } = getStyleFilePathsSync(displayTemplate, contentTypeBaseType, basePath, true);
|
|
651
|
+
const info = aggregator.get(identifier) ?? { filePath: helperFile, fileFolder: helperFolder, templates: [] };
|
|
652
|
+
info.templates.push({ key: displayTemplate.key, file: styleFile, data: displayTemplate, folder: styleFolder });
|
|
653
|
+
aggregator.set(identifier, info);
|
|
654
|
+
return aggregator;
|
|
655
|
+
}, new TypeFilesList());
|
|
480
656
|
}
|
|
481
|
-
|
|
657
|
+
return new TypeFilesList();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function normalizeMergePatch(value, omitKeys, requiredKeys = [], requiredSource, atomicKeys = []) {
|
|
661
|
+
if (value === undefined || value === null)
|
|
662
|
+
return null;
|
|
663
|
+
if (Array.isArray(value))
|
|
664
|
+
return value.map(item => normalizeMergePatch(item, omitKeys));
|
|
665
|
+
if (typeof value !== 'object')
|
|
666
|
+
return value;
|
|
667
|
+
const output = Object.entries(value).reduce((acc, [key, entry]) => {
|
|
668
|
+
if (!omitKeys.includes(key)) {
|
|
669
|
+
if (atomicKeys.includes(key) && requiredSource !== undefined) {
|
|
670
|
+
// Include the full value from newValue verbatim — do not recurse into the diff
|
|
671
|
+
acc[key] = requiredSource[key];
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
acc[key] = normalizeMergePatch(entry, omitKeys);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return acc;
|
|
678
|
+
}, {});
|
|
679
|
+
const withRequiredKeys = requiredKeys.reduce((acc, key) => {
|
|
680
|
+
if (requiredSource && !Object.keys(acc).includes(key))
|
|
681
|
+
acc[key] = requiredSource[key];
|
|
682
|
+
return acc;
|
|
683
|
+
}, output);
|
|
684
|
+
return withRequiredKeys;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Computes a JSON Merge Patch (RFC 7396) that transforms `currentValue` into `newValue`.
|
|
688
|
+
*
|
|
689
|
+
* The resulting patch can be sent directly as the request body of an `application/merge-patch+json`
|
|
690
|
+
* request. Changed and added properties are included with their new value; removed properties are
|
|
691
|
+
* included with a `null` value (as required by RFC 7396); unchanged properties are omitted entirely.
|
|
692
|
+
*
|
|
693
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7396
|
|
694
|
+
*
|
|
695
|
+
* @example Basic usage
|
|
696
|
+
* ```ts
|
|
697
|
+
* const current = { name: 'Alice', age: 30, role: 'user' }
|
|
698
|
+
* const updated = { name: 'Alice', age: 31 }
|
|
699
|
+
*
|
|
700
|
+
* generatePatch(current, updated)
|
|
701
|
+
* // => { age: 31, role: null }
|
|
702
|
+
* // `age` is replaced, `role` is removed (null), `name` is unchanged (omitted)
|
|
703
|
+
* ```
|
|
704
|
+
*
|
|
705
|
+
* @example Excluding keys from the patch
|
|
706
|
+
* ```ts
|
|
707
|
+
* const current = { id: '123', name: 'Alice', version: 1 }
|
|
708
|
+
* const updated = { id: '123', name: 'Bob', version: 2 }
|
|
709
|
+
*
|
|
710
|
+
* generatePatch(current, updated, { readonlyFields: ['id', 'version'] })
|
|
711
|
+
* // => { name: 'Bob' }
|
|
712
|
+
* // `id` and `version` are excluded even though they are present in the diff
|
|
713
|
+
* ```
|
|
714
|
+
*
|
|
715
|
+
* @example Always include required fields
|
|
716
|
+
* ```ts
|
|
717
|
+
* const current = { id: '123', name: 'Alice', version: 1 }
|
|
718
|
+
* const updated = { id: '123', name: 'Alice', version: 2 }
|
|
719
|
+
*
|
|
720
|
+
* generatePatch(current, updated, { requiredFields: ['id'] })
|
|
721
|
+
* // => { id: '123', version: 2 }
|
|
722
|
+
* // `id` is included even though it did not change
|
|
723
|
+
* ```
|
|
724
|
+
*
|
|
725
|
+
* @example Nested objects
|
|
726
|
+
* ```ts
|
|
727
|
+
* const current = { address: { city: 'Amsterdam', zip: '1000AA' } }
|
|
728
|
+
* const updated = { address: { city: 'Utrecht' } }
|
|
729
|
+
*
|
|
730
|
+
* generatePatch(current, updated)
|
|
731
|
+
* // => { address: { city: 'Utrecht', zip: null } }
|
|
732
|
+
* ```
|
|
733
|
+
*
|
|
734
|
+
* @param currentValue - The current state of the resource.
|
|
735
|
+
* @param newValue - The desired state of the resource.
|
|
736
|
+
* @param options - Optional configuration.
|
|
737
|
+
* @param options.readonlyFields - Keys that should never appear in the generated patch,
|
|
738
|
+
* regardless of whether they changed.
|
|
739
|
+
* @param options.requiredFields - Keys that should always appear in the generated patch,
|
|
740
|
+
* using values from `newValue`.
|
|
741
|
+
* @param options.atomicFields - Keys whose values are always included verbatim when they
|
|
742
|
+
* change, rather than being recursively patched. Use for
|
|
743
|
+
* record- or dictionary-typed properties (e.g.
|
|
744
|
+
* `{ [key: string]: SomeObject }`) and arrays of objects.
|
|
745
|
+
* @returns A {@link MergePatch} object ready to be serialised as an `application/merge-patch+json` body.
|
|
746
|
+
*/
|
|
747
|
+
function generatePatch(currentValue, newValue, options = {}) {
|
|
748
|
+
const requiredKeys = options.requiredFields ?? [];
|
|
749
|
+
const omitKeys = (options.readonlyFields ?? []).filter(key => !requiredKeys.includes(key));
|
|
750
|
+
const atomicKeys = (options.atomicFields ?? []);
|
|
751
|
+
const patch = diff(currentValue, newValue);
|
|
752
|
+
return normalizeMergePatch(patch, omitKeys, requiredKeys, newValue, atomicKeys);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Extracts all field paths from a {@link MergePatch} as dot-separated strings.
|
|
756
|
+
* Nested objects are flattened recursively; leaf values (including `null`) produce a path entry.
|
|
757
|
+
*
|
|
758
|
+
* @example
|
|
759
|
+
* ```ts
|
|
760
|
+
* getPatchFields({ a: { b: 0 }, c: 'value' })
|
|
761
|
+
* // => ['a.b', 'c']
|
|
762
|
+
* ```
|
|
763
|
+
*
|
|
764
|
+
* @param patch - The merge patch to extract fields from.
|
|
765
|
+
* @param prefix - Internal prefix used during recursion; omit when calling directly.
|
|
766
|
+
* @returns An array of dot-separated field path strings.
|
|
767
|
+
*/
|
|
768
|
+
function getPatchFields(patch, prefix = '') {
|
|
769
|
+
return Object.entries(patch).flatMap(([key, value]) => {
|
|
770
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
771
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
772
|
+
? getPatchFields(value, path)
|
|
773
|
+
: [path];
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const StylesPushCommand = {
|
|
778
|
+
command: "styles:push",
|
|
779
|
+
describe: "Push Visual Builder style definitions into the CMS (create/replace)",
|
|
780
|
+
builder: (yargs) => {
|
|
781
|
+
yargs.option('excludeTemplates', { alias: 'e', description: "Exclude these templates", string: true, type: 'array', demandOption: false, default: [] });
|
|
782
|
+
yargs.option("templates", { alias: 't', description: "Select only these templates", string: true, type: 'array', demandOption: false, default: [] });
|
|
783
|
+
return yargs;
|
|
784
|
+
},
|
|
785
|
+
handler: async (args) => {
|
|
786
|
+
const { _config: cfg, excludeTemplates, templates, ...opts } = parseArgs(args);
|
|
787
|
+
const client = createCmsClient(args);
|
|
788
|
+
const { styles: displayTemplates } = await getStyles(client, {
|
|
789
|
+
all: false,
|
|
790
|
+
baseTypes: [],
|
|
791
|
+
excludeBaseTypes: [],
|
|
792
|
+
excludeNodeTypes: [],
|
|
793
|
+
excludeTemplates: [],
|
|
794
|
+
excludeTypes: [],
|
|
795
|
+
nodes: [],
|
|
796
|
+
templates: [],
|
|
797
|
+
templateTypes: [],
|
|
798
|
+
types: [],
|
|
799
|
+
...args
|
|
800
|
+
}, 50);
|
|
801
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Pushing (create/replace) DisplayStyles into Optimizely CMS\n`));
|
|
802
|
+
const styleDefinitionFiles = await glob("./**/*.opti-style.json", {
|
|
803
|
+
cwd: opts.components
|
|
804
|
+
});
|
|
805
|
+
const results = (await Promise.allSettled(styleDefinitionFiles.map(async (styleDefinitionFile) => {
|
|
806
|
+
const filePath = path.normalize(path.join(opts.components, styleDefinitionFile));
|
|
807
|
+
const styleDefinition = tryReadJsonFile(filePath, cfg.debug);
|
|
808
|
+
const styleKey = styleDefinition.key;
|
|
809
|
+
if (!styleKey) {
|
|
810
|
+
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`));
|
|
811
|
+
return undefined;
|
|
812
|
+
}
|
|
813
|
+
if (excludeTemplates.includes(styleKey))
|
|
814
|
+
return undefined; // Skip excluded styles
|
|
815
|
+
if (templates.length > 0 && !templates.includes(styleKey))
|
|
816
|
+
return undefined; // Only include defined styles, if any
|
|
817
|
+
if (cfg.debug)
|
|
818
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Pushing: ${styleKey}\n`));
|
|
819
|
+
// Try to fetch the current template
|
|
820
|
+
const currentTemplate = displayTemplates.find(dt => dt.key === styleKey);
|
|
821
|
+
// Create / Replace the current template
|
|
822
|
+
const newTemplate = await (currentTemplate ? (async () => {
|
|
823
|
+
const patch = generatePatch(currentTemplate, styleDefinition, {
|
|
824
|
+
readonlyFields: ['key', 'created', 'lastModified', 'createdBy', 'lastModifiedBy'],
|
|
825
|
+
atomicFields: ['settings']
|
|
826
|
+
});
|
|
827
|
+
if (!path || Object.entries(patch).length === 0)
|
|
828
|
+
return currentTemplate;
|
|
829
|
+
return client.displayTemplatesPatch({ path: { key: styleKey }, body: patch });
|
|
830
|
+
})() :
|
|
831
|
+
client.displayTemplatesCreate({ body: styleDefinition }));
|
|
832
|
+
const missedFields = newTemplate ? generatePatch(newTemplate, currentTemplate, {
|
|
833
|
+
readonlyFields: ['key', 'created', 'lastModified', 'createdBy', 'lastModifiedBy']
|
|
834
|
+
}) : undefined;
|
|
835
|
+
if (missedFields && Object.keys(missedFields).length > 0)
|
|
836
|
+
throw new Error(`The Display template ${styleKey} failed to update properties: ${getPatchFields(missedFields).join('; ')}`);
|
|
837
|
+
return newTemplate;
|
|
838
|
+
})));
|
|
839
|
+
const styles = new Table({
|
|
840
|
+
head: [
|
|
841
|
+
chalk.yellow(chalk.bold("Name")),
|
|
842
|
+
chalk.yellow(chalk.bold("Key")),
|
|
843
|
+
chalk.yellow(chalk.bold("Default")),
|
|
844
|
+
chalk.yellow(chalk.bold("Target"))
|
|
845
|
+
],
|
|
846
|
+
colWidths: [31, 20, 9, 20],
|
|
847
|
+
colAligns: ["left", "left", "center", "left"]
|
|
848
|
+
});
|
|
849
|
+
results.forEach(result => {
|
|
850
|
+
if (result.status === 'fulfilled') {
|
|
851
|
+
if (isDefined(result.value)) {
|
|
852
|
+
const tpl = result.value;
|
|
853
|
+
styles.push([
|
|
854
|
+
tpl.displayName,
|
|
855
|
+
tpl.key,
|
|
856
|
+
tpl.isDefault ? figures.tick : figures.cross,
|
|
857
|
+
tpl.contentType ? `${tpl.contentType} (C)` : tpl.baseType ? `${tpl.baseType} (B)` : `${tpl.nodeType} (N)`
|
|
858
|
+
]);
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
process.stderr.write(`DisplayTemplate pushed without errors, but no result was returned.\n`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
process.stderr.write(`Error processing DisplayTemplate: ${result.reason ?? "n"}\n`);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
process.stdout.write(styles.toString() + "\n");
|
|
869
|
+
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
function tryReadJsonFile(filePath, debug = false) {
|
|
873
|
+
try {
|
|
874
|
+
if (debug)
|
|
875
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Reading style definition from ${filePath}\n`));
|
|
876
|
+
return JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' }));
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
process.stderr.write(chalk.redBright(`${chalk.bold(figures.cross)} Error while reading ${filePath}\n`));
|
|
880
|
+
}
|
|
881
|
+
return undefined;
|
|
482
882
|
}
|
|
483
883
|
|
|
484
884
|
const StylesListCommand = {
|
|
@@ -486,10 +886,6 @@ const StylesListCommand = {
|
|
|
486
886
|
describe: "List Visual Builder style definitions from the CMS",
|
|
487
887
|
handler: async (args) => {
|
|
488
888
|
const client = createCmsClient(args);
|
|
489
|
-
if (client.runtimeCmsVersion == OptiCmsVersion.CMS12) {
|
|
490
|
-
process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
889
|
const { all: results } = await getStyles(client, { ...args, excludeBaseTypes: [], excludeNodeTypes: [], excludeTemplates: [], excludeTypes: [], baseTypes: [], nodes: [], templates: [], types: [], templateTypes: [], all: false });
|
|
494
890
|
const styles = new Table({
|
|
495
891
|
head: [
|
|
@@ -514,6 +910,14 @@ const StylesListCommand = {
|
|
|
514
910
|
}
|
|
515
911
|
};
|
|
516
912
|
|
|
913
|
+
function ucFirst(current) {
|
|
914
|
+
if (typeof current != 'string')
|
|
915
|
+
throw new Error("Only strings can be transformed");
|
|
916
|
+
if (current == "")
|
|
917
|
+
return current;
|
|
918
|
+
return current[0]?.toUpperCase() + current.substring(1);
|
|
919
|
+
}
|
|
920
|
+
|
|
517
921
|
const StylesPullCommand = {
|
|
518
922
|
command: "styles:pull",
|
|
519
923
|
describe: "Create Visual Builder style definitions from the CMS",
|
|
@@ -524,129 +928,64 @@ const StylesPullCommand = {
|
|
|
524
928
|
return newYargs;
|
|
525
929
|
},
|
|
526
930
|
handler: async (args) => {
|
|
527
|
-
const { _config: cfg, components: basePath, force
|
|
931
|
+
const { _config: cfg, components: basePath, force} = parseArgs(args);
|
|
528
932
|
const client = createCmsClient(args);
|
|
529
|
-
if (client.runtimeCmsVersion == OptiCmsVersion.CMS12) {
|
|
530
|
-
process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
933
|
const { styles: filteredResults } = await getStyles(client, args);
|
|
534
|
-
//#region Create & Write opti-style.json files
|
|
535
934
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Start creating .opti-style.json files\n`));
|
|
536
|
-
const
|
|
537
|
-
const updatedTemplates =
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
delete outputTemplate.lastModifiedBy;
|
|
546
|
-
if (outputTemplate.created)
|
|
547
|
-
delete outputTemplate.created;
|
|
548
|
-
if (outputTemplate.lastModified)
|
|
549
|
-
delete outputTemplate.lastModified;
|
|
550
|
-
// Write file to disk
|
|
551
|
-
if (fs.existsSync(filePath)) {
|
|
552
|
-
if (!force) {
|
|
553
|
-
if (cfg.debug)
|
|
554
|
-
process.stdout.write(chalk.gray(`${figures.cross} Skipping style file for ${displayTemplate.key} - File already exists\n`));
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
if (cfg.debug) {
|
|
558
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting style file for ${displayTemplate.key}\n`));
|
|
559
|
-
}
|
|
560
|
-
fs.writeFileSync(filePath, JSON.stringify(outputTemplate, undefined, 2));
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
else {
|
|
564
|
-
if (cfg.debug)
|
|
565
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating style file for ${displayTemplate.key} in ${filePath}\n`));
|
|
566
|
-
fs.writeFileSync(filePath, JSON.stringify(outputTemplate, undefined, 2));
|
|
935
|
+
const styleFiles = await toTypeFilesList(filteredResults, client, basePath);
|
|
936
|
+
const updatedTemplates = [];
|
|
937
|
+
for (const groupIdentifier of styleFiles.keys()) {
|
|
938
|
+
const displayTemplateGroup = styleFiles.get(groupIdentifier);
|
|
939
|
+
for (const { file: filePath, data: displayTemplate } of (displayTemplateGroup?.templates || [])) {
|
|
940
|
+
// Write JSON to disk
|
|
941
|
+
const updatedJson = await createDisplayTemplateFile(displayTemplate, filePath, force, cfg.debug);
|
|
942
|
+
if (updatedJson)
|
|
943
|
+
updatedTemplates.push(displayTemplate.key);
|
|
567
944
|
}
|
|
568
|
-
//
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
filePath: helperPath,
|
|
572
|
-
templates: []
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
typeFiles[targetType].templates.push({ key: displayTemplate.key, file: filePath, data: displayTemplate });
|
|
576
|
-
return displayTemplate.key;
|
|
577
|
-
}))).filter(x => x);
|
|
578
|
-
//#endregion
|
|
579
|
-
//#region Create needed definition files
|
|
580
|
-
if (definitions)
|
|
581
|
-
await createDisplayTemplateHelpers(typeFiles);
|
|
582
|
-
//#endregion
|
|
945
|
+
// Write template to disk
|
|
946
|
+
void await createDisplayTemplateHelper(displayTemplateGroup, groupIdentifier, force, cfg.debug);
|
|
947
|
+
}
|
|
583
948
|
process.stdout.write(chalk.green(chalk.bold(figures.tick + ` Created/updated style definitions for ${updatedTemplates.join(', ')}`)) + "\n");
|
|
584
949
|
}
|
|
585
950
|
};
|
|
586
|
-
function
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const baseTypeSlug = baseTypeToSlug(displayTemplate.baseType);
|
|
604
|
-
itemPath = path.join(basePath, baseTypeSlug, 'styles', displayTemplate.key);
|
|
605
|
-
typesPath = path.join(basePath, baseTypeSlug, 'styles');
|
|
606
|
-
targetType = targetPrefix + '/' + baseTypeSlug;
|
|
607
|
-
break;
|
|
951
|
+
async function createDisplayTemplateFile(displayTemplate, filePath, force = false, debug = false) {
|
|
952
|
+
let updated = false;
|
|
953
|
+
// Build local style data
|
|
954
|
+
const outputTemplate = { ...displayTemplate };
|
|
955
|
+
if (outputTemplate.createdBy)
|
|
956
|
+
delete outputTemplate.createdBy;
|
|
957
|
+
if (outputTemplate.lastModifiedBy)
|
|
958
|
+
delete outputTemplate.lastModifiedBy;
|
|
959
|
+
if (outputTemplate.created)
|
|
960
|
+
delete outputTemplate.created;
|
|
961
|
+
if (outputTemplate.lastModified)
|
|
962
|
+
delete outputTemplate.lastModified;
|
|
963
|
+
// Write file to disk
|
|
964
|
+
if (fs.existsSync(filePath)) {
|
|
965
|
+
if (!force) {
|
|
966
|
+
if (debug)
|
|
967
|
+
process.stdout.write(chalk.gray(`${figures.cross} Skipping style file for ${displayTemplate.key} - File already exists\n`));
|
|
608
968
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
break;
|
|
969
|
+
else {
|
|
970
|
+
if (debug) {
|
|
971
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting style file for ${displayTemplate.key}\n`));
|
|
972
|
+
}
|
|
973
|
+
fs.writeFileSync(filePath, JSON.stringify(outputTemplate, undefined, 2));
|
|
974
|
+
updated = true;
|
|
616
975
|
}
|
|
617
|
-
default:
|
|
618
|
-
throw new Error("Unsupported display template type");
|
|
619
976
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const helperFilePath = path.join(typesPath, 'displayTemplates.ts');
|
|
626
|
-
return { key: displayTemplate.key, itemPath, typesPath, targetType, styleFilePath, helperFilePath };
|
|
627
|
-
}
|
|
628
|
-
function baseTypeToSlug(baseType) {
|
|
629
|
-
return baseType.replace(/^\_*/, "").toLowerCase();
|
|
630
|
-
}
|
|
631
|
-
function getTemplateTarget(displayTemplate) {
|
|
632
|
-
if (displayTemplate.baseType && displayTemplate.baseType.length > 0)
|
|
633
|
-
return 'base';
|
|
634
|
-
if (displayTemplate.nodeType && displayTemplate.nodeType.length > 0)
|
|
635
|
-
return 'node';
|
|
636
|
-
if (displayTemplate.contentType && displayTemplate.contentType.length > 0)
|
|
637
|
-
return 'content';
|
|
638
|
-
return null;
|
|
639
|
-
}
|
|
640
|
-
async function createDisplayTemplateHelpers(typeFiles, force = false, debug = false) {
|
|
641
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Start creating displayTemplates.ts files\n`));
|
|
642
|
-
for (const targetId in typeFiles) {
|
|
643
|
-
await createDisplayTemplateHelper(typeFiles[targetId], targetId, force, debug);
|
|
977
|
+
else {
|
|
978
|
+
if (debug)
|
|
979
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating style file for ${displayTemplate.key} in ${filePath}\n`));
|
|
980
|
+
fs.writeFileSync(filePath, JSON.stringify(outputTemplate, undefined, 2));
|
|
981
|
+
updated = true;
|
|
644
982
|
}
|
|
983
|
+
return updated;
|
|
645
984
|
}
|
|
646
985
|
async function createDisplayTemplateHelper(typeFile, typeFileId, force = false, debug = false) {
|
|
647
986
|
const prefix = '//not-modified - Remove this line when making change to prevent it from being updated by the CLI tools';
|
|
648
987
|
const { filePath: typeFilePath, templates } = typeFile;
|
|
649
|
-
const shouldWrite = await
|
|
988
|
+
const shouldWrite = await fs$1.readFile(typeFilePath, { encoding: "utf-8" }).then(data => data.startsWith('//not-modified')).catch((e) => {
|
|
650
989
|
if (e?.code === 'ENOENT')
|
|
651
990
|
return true;
|
|
652
991
|
if (debug)
|
|
@@ -680,20 +1019,20 @@ async function createDisplayTemplateHelper(typeFile, typeFileId, force = false,
|
|
|
680
1019
|
typeContents.push(`export type ${displayTemplate.key}Props = LayoutProps<typeof ${displayTemplate.key}Styles>`);
|
|
681
1020
|
typeContents.push(`export type ${displayTemplate.key}Keys = LayoutPropsSettingKeys<${displayTemplate.key}Props>`);
|
|
682
1021
|
typeContents.push(`export type ${displayTemplate.key}Options<K extends ${displayTemplate.key}Keys> = LayoutPropsSettingValues<${displayTemplate.key}Props, K>`);
|
|
683
|
-
typeContents.push(`export type ${displayTemplate.key}ComponentProps<DT extends Record<string,
|
|
684
|
-
typeContents.push(`export type ${displayTemplate.key}Component<DT extends Record<string,
|
|
1022
|
+
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']`);
|
|
1023
|
+
typeContents.push(`export type ${displayTemplate.key}Component<DT extends Record<string, unknown> = Record<string, unknown>> = ComponentType<${displayTemplate.key}ComponentProps<DT>>`);
|
|
685
1024
|
typeContents.push('');
|
|
686
1025
|
props.push(`${displayTemplate.key}Props`);
|
|
687
1026
|
if (!typeId)
|
|
688
1027
|
typeId = displayTemplate.nodeType ?? displayTemplate.baseType ?? displayTemplate.contentType;
|
|
689
1028
|
});
|
|
690
1029
|
if (typeId) {
|
|
691
|
-
typeId = ucFirst
|
|
1030
|
+
typeId = ucFirst(typeId);
|
|
692
1031
|
typeContents.push(`export type ${typeId}LayoutProps = ${props.join(' | ')}
|
|
693
1032
|
export type ${typeId}LayoutKeys = LayoutPropsSettingKeys<${typeId}LayoutProps>
|
|
694
1033
|
export type ${typeId}LayoutOptions<K extends ${typeId}LayoutKeys> = LayoutPropsSettingValues<${typeId}LayoutProps,K>
|
|
695
|
-
export type ${typeId}ComponentProps<DT extends Record<string,
|
|
696
|
-
export type ${typeId}Component<DT extends Record<string,
|
|
1034
|
+
export type ${typeId}ComponentProps<DT extends Record<string, unknown> = Record<string, unknown>> = Omit<CmsComponentProps<DT, ${typeId}LayoutProps>,'children'> & JSX.IntrinsicElements['div']
|
|
1035
|
+
export type ${typeId}Component<DT extends Record<string, unknown> = Record<string, unknown>> = ComponentType<${typeId}ComponentProps<DT>>`);
|
|
697
1036
|
const defaultTemplate = templates.find(t => t.data.isDefault);
|
|
698
1037
|
if (defaultTemplate) {
|
|
699
1038
|
typeContents.push(`
|
|
@@ -710,7 +1049,7 @@ export function is${t.data.key}Props(props?: ${typeId}LayoutProps | null) : prop
|
|
|
710
1049
|
}`);
|
|
711
1050
|
});
|
|
712
1051
|
}
|
|
713
|
-
void await
|
|
1052
|
+
void await fs$1.writeFile(typeFilePath, prefix + "\n" + imports.join("\n") + "\n\n" + typeContents.join("\n"));
|
|
714
1053
|
return true;
|
|
715
1054
|
}
|
|
716
1055
|
|
|
@@ -724,10 +1063,6 @@ const StylesCreateCommand = {
|
|
|
724
1063
|
handler: async (args) => {
|
|
725
1064
|
const { components: basePath } = parseArgs(args);
|
|
726
1065
|
const client = createCmsClient(args);
|
|
727
|
-
if (client.runtimeCmsVersion == OptiCmsVersion.CMS12) {
|
|
728
|
-
process.stdout.write(chalk.gray(`${figures.cross} Styles are not supported on CMS12\n`));
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
1066
|
const allowedBaseTypes = ['section', 'component', 'experience'];
|
|
732
1067
|
// Prepare
|
|
733
1068
|
process.stdout.write(chalk.yellowBright(chalk.bold(`Reading current information from Optimizely CMS\n`)));
|
|
@@ -776,10 +1111,10 @@ const StylesCreateCommand = {
|
|
|
776
1111
|
definition[type] = typeId;
|
|
777
1112
|
definition['settings'] = {};
|
|
778
1113
|
const contentBaseType = type == "contentType" ? contentTypes.filter(x => x.key == typeId).map(x => x.baseType).at(0) : undefined;
|
|
779
|
-
const styleFilePath = await
|
|
780
|
-
if (!fs.existsSync(
|
|
781
|
-
fs.mkdirSync(
|
|
782
|
-
if (fs.existsSync(
|
|
1114
|
+
const { styleFile: styleFilePath, styleFolder: styleFileFolder } = await getStyleFilePaths(definition, { contentBaseType, client, basePath });
|
|
1115
|
+
if (!fs.existsSync(styleFileFolder))
|
|
1116
|
+
fs.mkdirSync(styleFileFolder, { recursive: true });
|
|
1117
|
+
if (fs.existsSync(styleFilePath)) {
|
|
783
1118
|
const overwrite = await confirm({ message: "The style definition file already exists, do you want to overwrite it?" });
|
|
784
1119
|
if (!overwrite) {
|
|
785
1120
|
process.stdout.write("\n");
|
|
@@ -788,8 +1123,8 @@ const StylesCreateCommand = {
|
|
|
788
1123
|
}
|
|
789
1124
|
}
|
|
790
1125
|
// @ToDo: Add properties
|
|
791
|
-
fs.writeFileSync(
|
|
792
|
-
process.stdout.write(chalk.yellowBright(chalk.bold(`\nWritten style defintion template to: ${path.normalize(
|
|
1126
|
+
fs.writeFileSync(styleFilePath, JSON.stringify(definition, undefined, 4));
|
|
1127
|
+
process.stdout.write(chalk.yellowBright(chalk.bold(`\nWritten style defintion template to: ${path.normalize(styleFilePath)}\n`)));
|
|
793
1128
|
if (await confirm({ message: "Do you want to publish this style into Optimizely CMS?" })) {
|
|
794
1129
|
const response = await client.displayTemplatesCreate({ body: definition }).catch(_ => undefined);
|
|
795
1130
|
if (!response) {
|
|
@@ -811,6 +1146,91 @@ const StylesCreateCommand = {
|
|
|
811
1146
|
}
|
|
812
1147
|
};
|
|
813
1148
|
|
|
1149
|
+
const StylesDeleteCommand = {
|
|
1150
|
+
command: "styles:delete",
|
|
1151
|
+
describe: "Remove Visual Builder style definitions from the CMS",
|
|
1152
|
+
builder: (yargs) => {
|
|
1153
|
+
const newYargs = stylesBuilder(yargs);
|
|
1154
|
+
newYargs.option('force', { alias: 'f', description: "Overwrite existing files", boolean: true, type: 'boolean', demandOption: false, default: false });
|
|
1155
|
+
newYargs.option('withStyleFile', { alias: 'w', description: "Delete the .opti-style.json file as weill", boolean: true, type: 'boolean', demandOption: false, default: true });
|
|
1156
|
+
newYargs.option("definitions", { alias: 'u', description: "Update typescript definitions", boolean: true, type: 'boolean', demandOption: false, default: true });
|
|
1157
|
+
return newYargs;
|
|
1158
|
+
},
|
|
1159
|
+
handler: async (args) => {
|
|
1160
|
+
const { components: basePath } = parseArgs(args);
|
|
1161
|
+
const client = createCmsClient(args);
|
|
1162
|
+
const { styles, all: allStyles } = await getStyles(client, args, 100);
|
|
1163
|
+
if (styles.findIndex(x => x.isDefault) >= 0)
|
|
1164
|
+
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`))));
|
|
1165
|
+
if (!args.force) {
|
|
1166
|
+
process.stdout.write(`This will remove the following display templates\n`);
|
|
1167
|
+
for (const style of styles) {
|
|
1168
|
+
process.stdout.write(` ${figures.arrowRight} ${style.displayName || style.key} [Key: ${style.key}; Default: ${style.isDefault ? 'Yes' : 'No'}]\n`);
|
|
1169
|
+
}
|
|
1170
|
+
process.stdout.write(`\nRun with -f to actually preform removal\n`);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const keysToDelete = styles.map(displayTemplate => displayTemplate.key);
|
|
1174
|
+
const styleHelpers = await toTypeFilesList(allStyles, client, basePath);
|
|
1175
|
+
for (const displayTemplate of styles) {
|
|
1176
|
+
const { styleFile: styleFilePath, styleFolder: itemPath, identifier: targetType, helperFolder: typesPath } = styleHelpers.getDisplayTemplatePathsByKey(displayTemplate.key);
|
|
1177
|
+
// Remove style file
|
|
1178
|
+
if (args.withStyleFile) {
|
|
1179
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing *.opti-style.json file for ${displayTemplate.displayName} [${displayTemplate.key}]\n`));
|
|
1180
|
+
await fs$1.rm(styleFilePath, { force: false, recursive: false, maxRetries: 1 }).catch((e) => {
|
|
1181
|
+
if (e?.code === 'ENOENT') // The file can't be deleted as it does not exist - that's the desired state
|
|
1182
|
+
return;
|
|
1183
|
+
throw e;
|
|
1184
|
+
});
|
|
1185
|
+
await fs$1.rmdir(itemPath, { recursive: false, maxRetries: 1 }).catch((e) => {
|
|
1186
|
+
if (e?.code === 'ENOTEMPTY') {
|
|
1187
|
+
process.stdout.write(chalk.yellowBright(`${figures.warning} The folder is not empty, please remove ${itemPath} manually if needed\n`));
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
else if (e?.code === 'ENOENT')
|
|
1191
|
+
return;
|
|
1192
|
+
else
|
|
1193
|
+
throw e;
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
// Remove/update typescript helper
|
|
1197
|
+
if (args.definitions && styleHelpers.has(targetType)) {
|
|
1198
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing/updating displayTemplates.ts file for ${displayTemplate.displayName} [${displayTemplate.key}]\n`));
|
|
1199
|
+
const remainingTemplates = styleHelpers[targetType].templates.filter(x => !keysToDelete.includes(x.key));
|
|
1200
|
+
if (remainingTemplates.length === 0) {
|
|
1201
|
+
await fs$1.rm(styleHelpers[targetType].filePath, { force: false, recursive: false, maxRetries: 1 }).catch((e) => {
|
|
1202
|
+
if (e?.code === 'ENOENT') // The file can't be deleted as it does not exist - that's the desired state
|
|
1203
|
+
return;
|
|
1204
|
+
throw e;
|
|
1205
|
+
});
|
|
1206
|
+
await fs$1.rmdir(typesPath, { recursive: false, maxRetries: 1 }).catch((e) => {
|
|
1207
|
+
if (e?.code === 'ENOTEMPTY') {
|
|
1208
|
+
process.stdout.write(chalk.yellowBright(`${figures.warning} The folder is not empty, please remove ${typesPath} manually if needed\n`));
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
else if (e?.code === 'ENOENT')
|
|
1212
|
+
return;
|
|
1213
|
+
else
|
|
1214
|
+
throw e;
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
else {
|
|
1218
|
+
const newEntry = {
|
|
1219
|
+
filePath: styleHelpers[targetType].filePath,
|
|
1220
|
+
templates: remainingTemplates
|
|
1221
|
+
};
|
|
1222
|
+
if (!await createDisplayTemplateHelper(newEntry, targetType, false, false))
|
|
1223
|
+
process.stdout.write(chalk.yellowBright(`${figures.warning} The display template was not updated, please update ${newEntry.filePath} manually`));
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
// Actually remove from CMS
|
|
1227
|
+
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Removing the display template ${displayTemplate.displayName} [${displayTemplate.key}] from Optimizely CMS\n`));
|
|
1228
|
+
void await client.displayTemplatesDelete({ path: { key: displayTemplate.key } });
|
|
1229
|
+
}
|
|
1230
|
+
process.stdout.write("\n" + chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
|
|
814
1234
|
const TypesPullCommand = {
|
|
815
1235
|
command: "types:pull",
|
|
816
1236
|
describe: "Pull content type definition files into the project",
|
|
@@ -820,13 +1240,13 @@ const TypesPullCommand = {
|
|
|
820
1240
|
return newArgs;
|
|
821
1241
|
},
|
|
822
1242
|
handler: async (args) => {
|
|
823
|
-
const { _config: { debug }, components: basePath, force } = parseArgs(args);
|
|
1243
|
+
const { _config: { debug }, components: basePath, path: projectPath, force } = parseArgs(args);
|
|
824
1244
|
const client = createCmsClient(args);
|
|
825
1245
|
const { contentTypes } = await getContentTypes(client, args);
|
|
826
1246
|
const updatedTypes = contentTypes.map(contentType => {
|
|
827
|
-
const {
|
|
828
|
-
if (!fs.existsSync(
|
|
829
|
-
fs.mkdirSync(
|
|
1247
|
+
const { path: path$1, typeFile } = getContentTypePaths(contentType, basePath);
|
|
1248
|
+
if (!fs.existsSync(path$1))
|
|
1249
|
+
fs.mkdirSync(path$1, { recursive: true });
|
|
830
1250
|
if (fs.existsSync(typeFile) && !force) {
|
|
831
1251
|
if (debug)
|
|
832
1252
|
process.stdout.write(chalk.yellow(`${figures.cross} Skipping type definition for ${contentType.displayName} (${contentType.key}) - File already exists\n`));
|
|
@@ -835,9 +1255,7 @@ const TypesPullCommand = {
|
|
|
835
1255
|
const outContentType = { ...contentType };
|
|
836
1256
|
if (outContentType.source || outContentType.source == "")
|
|
837
1257
|
delete outContentType.source;
|
|
838
|
-
|
|
839
|
-
//if (outContentType.usage) delete outContentType.usage
|
|
840
|
-
if (outContentType.lastModifiedBy)
|
|
1258
|
+
if (outContentType.lastModifiedBy || outContentType.lastModifiedBy == "")
|
|
841
1259
|
delete outContentType.lastModifiedBy;
|
|
842
1260
|
if (outContentType.lastModified)
|
|
843
1261
|
delete outContentType.lastModified;
|
|
@@ -858,16 +1276,13 @@ const TypesPullCommand = {
|
|
|
858
1276
|
}
|
|
859
1277
|
}
|
|
860
1278
|
if (debug)
|
|
861
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key})\n`));
|
|
1279
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Writing type definition for ${contentType.displayName} (${contentType.key}) into ${path.relative(projectPath, path$1)}\n`));
|
|
862
1280
|
fs.writeFileSync(typeFile, JSON.stringify(outContentType, undefined, 2));
|
|
863
1281
|
return contentType.key;
|
|
864
1282
|
}).filter(x => x);
|
|
865
1283
|
process.stdout.write(chalk.green(chalk.bold(`${figures.tick} Created/updated type definitions for ${updatedTypes.join(', ')}\n`)));
|
|
866
1284
|
}
|
|
867
1285
|
};
|
|
868
|
-
function isEmptyArray(toTest) {
|
|
869
|
-
return toTest === null || !Array.isArray(toTest) || toTest.length === 0;
|
|
870
|
-
}
|
|
871
1286
|
|
|
872
1287
|
const deepmerge$1 = createDeepMerge();
|
|
873
1288
|
async function loadSchema(client, schemaName) {
|
|
@@ -1169,14 +1584,6 @@ function getTypeFolder(list, type) {
|
|
|
1169
1584
|
return list.filter(x => x.type == type).at(0);
|
|
1170
1585
|
}
|
|
1171
1586
|
|
|
1172
|
-
function ucFirst(current) {
|
|
1173
|
-
if (typeof current != 'string')
|
|
1174
|
-
throw new Error("Only strings can be transformed");
|
|
1175
|
-
if (current == "")
|
|
1176
|
-
return current;
|
|
1177
|
-
return current[0]?.toUpperCase() + current.substring(1);
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
1587
|
const NextJsComponentsCommand = {
|
|
1181
1588
|
command: "nextjs:components",
|
|
1182
1589
|
describe: "Create the React Components for a Next.JS / Optimizely Graph structure",
|
|
@@ -1286,8 +1693,9 @@ ${varName}.getDataFragment = () => ['${contentType.key}Data', ${contentType.key}
|
|
|
1286
1693
|
|
|
1287
1694
|
export default ${varName}`,
|
|
1288
1695
|
// Default Template for all section types
|
|
1289
|
-
section: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
|
|
1290
|
-
import { ${contentType.key}
|
|
1696
|
+
section: (contentType, varName, displayTemplate, baseDisplayTemplate) => `import { OptimizelyComposition, isNode, CmsEditable, type CmsComponent } from "@remkoj/optimizely-cms-react/rsc";
|
|
1697
|
+
import { get${contentType.key}DataDocument, type get${contentType.key}DataQuery, SectionCompositionDataFragmentDoc } from "@/gql/graphql";
|
|
1698
|
+
import { getFragmentData } from "@/gql/fragment-masking";${displayTemplate ? `
|
|
1291
1699
|
import { ${displayTemplate} } from "./displayTemplates";` : ''}${baseDisplayTemplate ? `
|
|
1292
1700
|
import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
|
|
1293
1701
|
|
|
@@ -1296,19 +1704,25 @@ import { ${baseDisplayTemplate} } from "../styles/displayTemplates";` : ''}
|
|
|
1296
1704
|
* ---
|
|
1297
1705
|
* ${contentType.description}
|
|
1298
1706
|
*/
|
|
1299
|
-
export const ${varName} : CmsComponent
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
</
|
|
1707
|
+
export const ${varName} : CmsComponent<get${contentType.key}DataQuery${(displayTemplate || baseDisplayTemplate) ? ', ' + [displayTemplate, baseDisplayTemplate].filter(x => x).join(" | ") : ''}> = ({ data${(displayTemplate || baseDisplayTemplate) ? ', layoutProps' : ''}, editProps, children, ctx }) => {
|
|
1708
|
+
// If we're rendering stand-alone we'll get a composition back, with ourself included If we're
|
|
1709
|
+
// rendering as part of an experience, we'll get the data directly. So handle both cases.
|
|
1710
|
+
if (!data?._metadata && children) return children;
|
|
1711
|
+
|
|
1712
|
+
const componentName = '${contentType.displayName}'
|
|
1713
|
+
const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
|
|
1714
|
+
const composition = getFragmentData(SectionCompositionDataFragmentDoc, data)?.composition;
|
|
1715
|
+
return <CmsEditable className="w-full border-y border-y-solid border-y-slate-900 py-2 mb-4" {...editProps}>
|
|
1716
|
+
<div className="font-bold italic">{ componentName }</div>
|
|
1717
|
+
<div>{ componentInfo }</div>
|
|
1718
|
+
{ 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> }
|
|
1719
|
+
${(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 */}'}
|
|
1720
|
+
{children && <div>{ children }</div>}
|
|
1721
|
+
{composition && isNode(composition) && <OptimizelyComposition node={composition} ctx={ctx} />}
|
|
1722
|
+
</CmsEditable>
|
|
1309
1723
|
}
|
|
1310
1724
|
${varName}.displayName = "${contentType.displayName} (${ucFirst(contentType.baseType)}/${contentType.key})"
|
|
1311
|
-
${varName}.
|
|
1725
|
+
${varName}.getDataQuery = () => get${contentType.key}DataDocument
|
|
1312
1726
|
|
|
1313
1727
|
export default ${varName}`,
|
|
1314
1728
|
// Template for all page component types
|
|
@@ -1347,10 +1761,11 @@ ${varName}.getMetaData = async (contentLink, locale, client) => {
|
|
|
1347
1761
|
|
|
1348
1762
|
export default ${varName}`,
|
|
1349
1763
|
// Template for all experience component types
|
|
1350
|
-
experience: (contentType, varName, displayTemplate) => `import
|
|
1764
|
+
experience: (contentType, varName, displayTemplate) => `import 'server-only'
|
|
1765
|
+
import { type OptimizelyNextPage as CmsComponent } from "@remkoj/optimizely-cms-nextjs";
|
|
1351
1766
|
import { ExperienceDataFragmentDoc, get${contentType.key}DataDocument, type get${contentType.key}DataQuery } from '@/gql/graphql'
|
|
1352
1767
|
import { getFragmentData } from '@/gql/fragment-masking'
|
|
1353
|
-
import { OptimizelyComposition, isNode, CmsEditable } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
|
|
1768
|
+
import { OptimizelyComposition, isNode, CmsEditable, type ServerContext } from "@remkoj/optimizely-cms-react/rsc";${displayTemplate ? `
|
|
1354
1769
|
import { ${displayTemplate} } from "./displayTemplates";` : ''}
|
|
1355
1770
|
import { getSdk } from "@/gql/client"
|
|
1356
1771
|
|
|
@@ -1366,7 +1781,7 @@ import { getSdk } from "@/gql/client"
|
|
|
1366
1781
|
* [Documentation: Customizing queries](https://github.com/remkoj/optimizely-dxp-clients/blob/main/packages/optimizely-graph-functions/docs/customizing_queries.md)
|
|
1367
1782
|
*/
|
|
1368
1783
|
export const ${varName} : CmsComponent<get${contentType.key}DataQuery${displayTemplate ? ', ' + displayTemplate : ''}> = ({ data${displayTemplate ? ', layoutProps' : ''}, ctx }) => {
|
|
1369
|
-
if (ctx) ctx
|
|
1784
|
+
if (ctx) (ctx as ServerContext).setEditableContentIsExperience(true)
|
|
1370
1785
|
const composition = getFragmentData(ExperienceDataFragmentDoc, data).composition
|
|
1371
1786
|
const componentName = '${contentType.displayName}'
|
|
1372
1787
|
const componentInfo = '${contentType.description?.replaceAll("'", "\\'") ?? ''}'
|
|
@@ -1401,13 +1816,13 @@ const NextJsVisualBuilderCommand = {
|
|
|
1401
1816
|
const { styles } = await getStyles(createCmsClient(args), { ...args, excludeNodeTypes: [], excludeTemplates: [], nodes: [], templates: [], templateTypes: ['node', 'base'] });
|
|
1402
1817
|
// Process node styles
|
|
1403
1818
|
styles.filter(x => typeof (x.nodeType) == 'string' && x.nodeType.length > 0).map(styleDefintion => {
|
|
1404
|
-
const templatePath = path.join(basePath, 'nodes', styleDefintion.nodeType, styleDefintion.key);
|
|
1819
|
+
const templatePath = path.join(basePath, 'nodes', keyToSlug(styleDefintion.nodeType), keyToSlug(styleDefintion.key));
|
|
1405
1820
|
createSpecificNode(styleDefintion, templatePath, force, debug);
|
|
1406
1821
|
});
|
|
1407
1822
|
// Process base styles
|
|
1408
1823
|
styles.filter(x => typeof (x.baseType) == 'string' && x.baseType.length > 0).map(styleDefinition => {
|
|
1409
1824
|
const baseType = (styleDefinition.baseType ?? '').startsWith('_') ? (styleDefinition.baseType ?? '').substring(1) : styleDefinition.baseType;
|
|
1410
|
-
const templatePath = path.join(basePath, baseType, 'styles', styleDefinition.key);
|
|
1825
|
+
const templatePath = path.join(basePath, baseType, 'styles', keyToSlug(styleDefinition.key));
|
|
1411
1826
|
createSpecificNode(styleDefinition, templatePath, force, debug);
|
|
1412
1827
|
});
|
|
1413
1828
|
}
|
|
@@ -1417,14 +1832,14 @@ function createSpecificNode(template, templatePath, force = false, debug = false
|
|
|
1417
1832
|
if (fs.existsSync(nodeFile)) {
|
|
1418
1833
|
if (!force) {
|
|
1419
1834
|
if (debug)
|
|
1420
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${template.displayName} (${template.key})
|
|
1835
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Skipping ${template.displayName} (${template.key}) node - file already exists\n`));
|
|
1421
1836
|
return undefined;
|
|
1422
1837
|
}
|
|
1423
1838
|
if (debug)
|
|
1424
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${template.displayName} (${template.key})
|
|
1839
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Overwriting ${template.displayName} (${template.key}) node\n`));
|
|
1425
1840
|
}
|
|
1426
1841
|
else if (debug)
|
|
1427
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${template.displayName} (${template.key})
|
|
1842
|
+
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${template.displayName} (${template.key}) node\n`));
|
|
1428
1843
|
if (!fs.existsSync(templatePath))
|
|
1429
1844
|
fs.mkdirSync(templatePath, { recursive: true });
|
|
1430
1845
|
const baseType = template.nodeType ?? template.baseType ?? 'unknown';
|
|
@@ -1502,6 +1917,7 @@ function getDisplayTemplateInfo(template, typePath) {
|
|
|
1502
1917
|
|
|
1503
1918
|
const ROOT_FACTORY_KEY = ".";
|
|
1504
1919
|
const FACTORY_FILE_NAME = "index.ts";
|
|
1920
|
+
const reservedNames = ["loading.tsx", "loading.jsx", "suspense.tsx", "suspense.jsx"];
|
|
1505
1921
|
const NextJsFactoryCommand = {
|
|
1506
1922
|
command: "nextjs:factory",
|
|
1507
1923
|
describe: "Create the ComponentFactory for a Next.JS / Optimizely Graph structure",
|
|
@@ -1510,108 +1926,102 @@ const NextJsFactoryCommand = {
|
|
|
1510
1926
|
const { components: basePath, force, _config: { debug } } = parseArgs(args);
|
|
1511
1927
|
if (debug)
|
|
1512
1928
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Start generating component factories\n`));
|
|
1929
|
+
// Get & filter all component files
|
|
1930
|
+
const clientComponents = [];
|
|
1513
1931
|
const components = globSync(["./**/*.jsx", "./**/*.tsx"], {
|
|
1514
1932
|
cwd: basePath
|
|
1515
1933
|
}).map(p => p.split(path.sep)).filter(p => {
|
|
1934
|
+
// Get the filename
|
|
1935
|
+
const fileName = p.at(p.length - 1);
|
|
1936
|
+
if (!fileName)
|
|
1937
|
+
return false;
|
|
1516
1938
|
// Consider components in a file starting with "_" as a partial
|
|
1517
|
-
if (
|
|
1939
|
+
if (fileName.startsWith('_') == true)
|
|
1518
1940
|
return false;
|
|
1519
1941
|
// Consider components in a folder named "partials" as a partial
|
|
1520
|
-
if (p.
|
|
1942
|
+
if (p.some(folder => folder === 'partials'))
|
|
1521
1943
|
return false;
|
|
1522
1944
|
// Skip the special loader component
|
|
1523
|
-
if (
|
|
1945
|
+
if (reservedNames.includes(fileName))
|
|
1524
1946
|
return false;
|
|
1525
1947
|
// Check if the file has a default export
|
|
1526
1948
|
const fileBuffer = fs.readFileSync(path.join(basePath, p.join(path.sep)));
|
|
1527
1949
|
const hasDefaultExport = fileBuffer.includes('export default');
|
|
1950
|
+
if (fileBuffer.includes('use client'))
|
|
1951
|
+
clientComponents.push(p.join(path.sep));
|
|
1528
1952
|
if (!hasDefaultExport) {
|
|
1529
1953
|
process.stdout.write(chalk.redBright(`${figures.warning} No default export in ${p.join(path.sep)} - ignoring file\n`));
|
|
1530
1954
|
return false;
|
|
1531
1955
|
}
|
|
1532
1956
|
return true;
|
|
1533
1957
|
});
|
|
1958
|
+
// Report what we have found
|
|
1534
1959
|
if (debug)
|
|
1535
1960
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Identified ${components.length} components in ${basePath}\n`));
|
|
1961
|
+
// Build factory / component structure
|
|
1536
1962
|
const componentFactoryDefintions = new Map();
|
|
1537
1963
|
components.forEach(component => {
|
|
1964
|
+
const componentDir = path.dirname(path.posix.join(...component));
|
|
1965
|
+
// const componentFile = path.basename(path.posix.join(...component));
|
|
1966
|
+
// Determine component target
|
|
1967
|
+
const componentKey = getComponentKey(component, basePath);
|
|
1968
|
+
let componentVariant = (component.length > 1 ? component.at(component.length - 1) ?? 'default' : 'default').replace('index', 'default');
|
|
1969
|
+
componentVariant = path.basename(componentVariant, path.extname(componentVariant));
|
|
1970
|
+
// Get factory information
|
|
1538
1971
|
const factorySegments = component.length > 2 ? component.slice(0, -2) : [ROOT_FACTORY_KEY];
|
|
1539
|
-
const factoryKey =
|
|
1972
|
+
const factoryKey = path.posix.join(...factorySegments);
|
|
1540
1973
|
const factoryFile = path.join(factoryKey, FACTORY_FILE_NAME);
|
|
1541
|
-
//
|
|
1974
|
+
// Check dynamic & suspense
|
|
1975
|
+
const isClientComponent = clientComponents.includes(component.join(path.sep));
|
|
1976
|
+
const useDynamic = [
|
|
1977
|
+
path.join(basePath, componentDir, 'loading.tsx'),
|
|
1978
|
+
path.join(basePath, componentDir, 'loading.jsx')
|
|
1979
|
+
].some(fs.existsSync);
|
|
1980
|
+
const useSuspense = [
|
|
1981
|
+
path.join(basePath, componentDir, 'suspense.tsx'),
|
|
1982
|
+
path.join(basePath, componentDir, 'suspense.jsx')
|
|
1983
|
+
].some(fs.existsSync);
|
|
1984
|
+
// Prepare data
|
|
1985
|
+
const componentImport = component.length == 1 ?
|
|
1986
|
+
`.${path.posix.sep}${path.basename(component[0], path.extname(component[0]))}` :
|
|
1987
|
+
`.${path.posix.sep}${path.posix.relative(factoryKey, componentDir)}${componentVariant !== 'default' ? path.posix.sep + componentVariant : ''}`;
|
|
1988
|
+
const loaderImport = useDynamic ? componentImport + path.posix.sep + 'loading' : undefined;
|
|
1989
|
+
const suspenseImport = useSuspense ? componentImport + path.posix.sep + 'suspense' : undefined;
|
|
1990
|
+
const componentVariablesBase = componentKey + (componentVariant !== 'default' ? processName(componentVariant) : '');
|
|
1991
|
+
// Add to the factory
|
|
1542
1992
|
const factory = componentFactoryDefintions.get(factoryKey) || { file: factoryFile, entries: [], subfactories: [] };
|
|
1543
|
-
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'));
|
|
1544
|
-
if (useSuspense && debug)
|
|
1545
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Components in ${component.slice(0, -1).join(path.sep)} will use suspense\n`));
|
|
1546
|
-
const componentSegments = component.slice(-2).map(p => {
|
|
1547
|
-
const entry = p.substring(0, p.length - path.extname(p).length);
|
|
1548
|
-
return entry.toLowerCase() == "index" ? null : entry;
|
|
1549
|
-
}).filter(x => x);
|
|
1550
|
-
const componentImport = "./" + componentSegments.join('/');
|
|
1551
|
-
const loaderImport = useSuspense ? "./" + component.slice(-2).map((p, i, a) => {
|
|
1552
|
-
const entry = p.substring(0, p.length - path.extname(p).length);
|
|
1553
|
-
if (i == a.length - 1)
|
|
1554
|
-
return 'loading';
|
|
1555
|
-
return entry.toLowerCase() == "index" ? null : entry;
|
|
1556
|
-
}).join('/') : undefined;
|
|
1557
1993
|
factory.entries.push({
|
|
1994
|
+
key: componentKey,
|
|
1995
|
+
variant: componentVariant,
|
|
1558
1996
|
import: componentImport,
|
|
1559
|
-
variable:
|
|
1560
|
-
key: componentSegments.join('/') == "node" ? "Node" : componentSegments.join('/'),
|
|
1997
|
+
variable: componentVariablesBase + 'Component',
|
|
1561
1998
|
loaderImport,
|
|
1562
|
-
loaderVariable:
|
|
1999
|
+
loaderVariable: useDynamic ? componentVariablesBase + 'Loader' : undefined,
|
|
2000
|
+
suspenseImport,
|
|
2001
|
+
suspenseVariable: useSuspense ? componentVariablesBase + 'Placeholder' : undefined,
|
|
2002
|
+
isClient: isClientComponent
|
|
1563
2003
|
});
|
|
1564
2004
|
componentFactoryDefintions.set(factoryKey, factory);
|
|
1565
|
-
//
|
|
2005
|
+
// Add/update parent factories
|
|
1566
2006
|
const parentSegements = factoryKey == ROOT_FACTORY_KEY ? factorySegments.slice(0, -1) : [ROOT_FACTORY_KEY, ...factorySegments.slice(0, -1)];
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
prefix: processName(factorySegments.slice(-1).at(0)),
|
|
1571
|
-
variable: factorySegments.map(processName).join("") + "Factory"
|
|
1572
|
-
};
|
|
1573
|
-
for (let i = parentSegements.length; i > 0; i--) {
|
|
1574
|
-
const parentFactoryKey = parentSegements.slice(0, i).filter(x => x != ROOT_FACTORY_KEY).join(path.sep) || ROOT_FACTORY_KEY;
|
|
2007
|
+
parentSegements.forEach((_, idx, data) => {
|
|
2008
|
+
const parentFactoryKey = path.posix.join(...data.slice(0, idx + 1)) || ROOT_FACTORY_KEY;
|
|
2009
|
+
const childFactoryKey = factorySegments.slice(idx, idx + 1).at(0);
|
|
1575
2010
|
const parentFactory = componentFactoryDefintions.get(parentFactoryKey) ?? { file: path.join(parentFactoryKey, FACTORY_FILE_NAME), entries: [], subfactories: [] };
|
|
1576
|
-
if (!parentFactory.subfactories.some(x => x.key
|
|
1577
|
-
parentFactory.subfactories.push(
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
prefix: processName(parentFactoryKey.split(path.sep).slice(-1).at(0)),
|
|
1584
|
-
variable: parentFactoryKey.split(path.sep).map(processName).join("") + "Factory"
|
|
1585
|
-
};
|
|
2011
|
+
if (!parentFactory.subfactories.some(x => x.key === childFactoryKey)) {
|
|
2012
|
+
parentFactory.subfactories.push({
|
|
2013
|
+
key: childFactoryKey,
|
|
2014
|
+
import: './' + childFactoryKey,
|
|
2015
|
+
variable: processName(childFactoryKey) + 'Factory'
|
|
2016
|
+
});
|
|
2017
|
+
componentFactoryDefintions.set(parentFactoryKey, parentFactory);
|
|
1586
2018
|
}
|
|
1587
|
-
}
|
|
1588
|
-
});
|
|
1589
|
-
const mainFactory = componentFactoryDefintions.get(ROOT_FACTORY_KEY);
|
|
1590
|
-
if (mainFactory) {
|
|
1591
|
-
if (debug)
|
|
1592
|
-
process.stdout.write(chalk.gray(`${figures.arrowRight} Updating prefixes within RootFactory\n`));
|
|
1593
|
-
mainFactory.subfactories = mainFactory.subfactories.map(subFactory => {
|
|
1594
|
-
if (typeof (subFactory.prefix == 'string'))
|
|
1595
|
-
switch (subFactory.prefix) {
|
|
1596
|
-
case "Video":
|
|
1597
|
-
case "Image":
|
|
1598
|
-
subFactory.prefix = ["Media", subFactory.prefix, "Component"];
|
|
1599
|
-
break;
|
|
1600
|
-
case "Experience":
|
|
1601
|
-
subFactory.prefix = [subFactory.prefix, "Page"];
|
|
1602
|
-
break;
|
|
1603
|
-
case "Element":
|
|
1604
|
-
subFactory.prefix = ["Component"];
|
|
1605
|
-
break;
|
|
1606
|
-
case "Media":
|
|
1607
|
-
subFactory.prefix = [subFactory.prefix, "Component"];
|
|
1608
|
-
break;
|
|
1609
|
-
}
|
|
1610
|
-
return subFactory;
|
|
1611
2019
|
});
|
|
1612
|
-
}
|
|
2020
|
+
});
|
|
2021
|
+
// Report factory file count
|
|
1613
2022
|
if (debug)
|
|
1614
2023
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Finished preparing ${componentFactoryDefintions.size} factories, start writing\n`));
|
|
2024
|
+
// Iterate over the factories and create them
|
|
1615
2025
|
let updateCounter = 0;
|
|
1616
2026
|
for (const key of componentFactoryDefintions.keys()) {
|
|
1617
2027
|
const factory = componentFactoryDefintions.get(key);
|
|
@@ -1635,6 +2045,36 @@ const NextJsFactoryCommand = {
|
|
|
1635
2045
|
process.stdout.write("\n");
|
|
1636
2046
|
}
|
|
1637
2047
|
};
|
|
2048
|
+
/**
|
|
2049
|
+
* Get the component key that must be used for the variable in the factory
|
|
2050
|
+
* this looks first at the component-type file.
|
|
2051
|
+
*
|
|
2052
|
+
* @param component The file path broken into parts
|
|
2053
|
+
* @returns The key to use
|
|
2054
|
+
*/
|
|
2055
|
+
function getComponentKey(component, componentsRootDir) {
|
|
2056
|
+
// Prepare context
|
|
2057
|
+
//const componentFile = path.basename(path.join(...component));
|
|
2058
|
+
const componentDir = path.dirname(path.join(...component));
|
|
2059
|
+
// Calculate the fallback value
|
|
2060
|
+
const baseName = processName(component.length == 1 ? ucFirst(path.basename(component[0], path.extname(component[0]))) : component.at(component.length - 2));
|
|
2061
|
+
// Try to read the key from the opti-type.json file in the same folder
|
|
2062
|
+
const definitions = globSync("*.opti-type.json", { cwd: path.join(componentsRootDir, componentDir) });
|
|
2063
|
+
//console.log(definitions, componentDir);
|
|
2064
|
+
if (definitions.length == 1) {
|
|
2065
|
+
try {
|
|
2066
|
+
const data = JSON.parse(fs.readFileSync(path.join(componentsRootDir, componentDir, definitions[0])).toString('utf8'));
|
|
2067
|
+
//console.log('Updating to', data.key)
|
|
2068
|
+
if (data.key)
|
|
2069
|
+
return data.key;
|
|
2070
|
+
}
|
|
2071
|
+
catch {
|
|
2072
|
+
// Ignored on purpose
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
// Return the fallback value
|
|
2076
|
+
return baseName;
|
|
2077
|
+
}
|
|
1638
2078
|
function shouldWriteFactory(factoryFile, force = false, debug = false) {
|
|
1639
2079
|
if (!fs.existsSync(factoryFile)) {
|
|
1640
2080
|
process.stdout.write(chalk.green(`${figures.tick} Creating new factory file: ${factoryFile}\n`));
|
|
@@ -1656,63 +2096,68 @@ function shouldWriteFactory(factoryFile, force = false, debug = false) {
|
|
|
1656
2096
|
function processName(input) {
|
|
1657
2097
|
if (input == ROOT_FACTORY_KEY)
|
|
1658
2098
|
return "Cms";
|
|
1659
|
-
const nameSegements = input.split(/[
|
|
2099
|
+
const nameSegements = input.split(/[-_]/g);
|
|
1660
2100
|
return nameSegements.map(ucFirst).join('');
|
|
1661
2101
|
}
|
|
1662
2102
|
function generateFactory(factoryInfo, factoryKey) {
|
|
1663
|
-
|
|
1664
|
-
const factoryName = factoryKey.split(path.sep).map(processName).join("") + "Factory";
|
|
1665
|
-
|
|
1666
|
-
const
|
|
1667
|
-
const
|
|
2103
|
+
// Get the factory name
|
|
2104
|
+
const factoryName = factoryKey.split(path.posix.sep).map(processName).join("") + "Factory";
|
|
2105
|
+
// Get the components and sub-factories, sorted by key to minimize changes between runs
|
|
2106
|
+
const components = [...factoryInfo.entries].sort((a, b) => { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
|
|
2107
|
+
const subFactories = [...factoryInfo.subfactories].sort((a, b) => { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
|
|
2108
|
+
// Check if there's at least one component that uses next/dynamic
|
|
2109
|
+
const hasDynamic = factoryInfo.entries.some(x => x.loaderImport);
|
|
2110
|
+
const hasClientComponents = factoryInfo.entries.some(x => x.isClient);
|
|
2111
|
+
// The intro for the factory
|
|
2112
|
+
const factoryIntro = `// Auto generated dictionary
|
|
1668
2113
|
// @not-modified => When this line is removed, the "force" parameter of the CLI tool is required to overwrite this file
|
|
1669
|
-
import { type ComponentTypeDictionary } from
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
${
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
${
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
return
|
|
2114
|
+
import { type ComponentTypeDictionary } from '@remkoj/optimizely-cms-react';${hasDynamic || hasClientComponents ? `
|
|
2115
|
+
import dynamic from 'next/dynamic';` : ''}`;
|
|
2116
|
+
// The outry for the factory
|
|
2117
|
+
const factoryOutro = `// Export dictionary
|
|
2118
|
+
export default ${factoryName};`;
|
|
2119
|
+
// The imports of the factory
|
|
2120
|
+
const factoryImports = [...components, ...subFactories].map(x => {
|
|
2121
|
+
const imports = [];
|
|
2122
|
+
if (x.loaderImport)
|
|
2123
|
+
imports.push(`import ${x.loaderVariable} from '${x.loaderImport}';`);
|
|
2124
|
+
else if (!x.isClient)
|
|
2125
|
+
imports.push(`import ${x.variable} from '${x.import}';`);
|
|
2126
|
+
if (x.suspenseImport)
|
|
2127
|
+
imports.push(`import ${x.suspenseVariable} from '${x.suspenseImport}';`);
|
|
2128
|
+
return imports.length > 0 ? imports.join('\n') : undefined;
|
|
2129
|
+
}).filter(x => x).join('\n');
|
|
2130
|
+
// The dynamic imports of the factory
|
|
2131
|
+
const dynamicImports = hasDynamic ? `// Lazy load components that have a loading file, this only affects client components
|
|
2132
|
+
// See https://nextjs.org/docs/app/guides/lazy-loading#importing-server-components
|
|
2133
|
+
// for more details on how this affects server components in Next.js
|
|
2134
|
+
` + components.filter(x => x.loaderImport).map(x => {
|
|
2135
|
+
return `const ${x.variable} = dynamic(() => import('${x.import}'), {
|
|
2136
|
+
ssr: true,
|
|
2137
|
+
loading: ${x.loaderVariable}
|
|
2138
|
+
});`;
|
|
2139
|
+
}).join('\n') : undefined;
|
|
2140
|
+
// The client imports of the factory
|
|
2141
|
+
const clientComponents = hasClientComponents ? `// Lazy load client components even without loader specified
|
|
2142
|
+
` + components.filter(x => x.isClient && !x.loaderImport).map(x => {
|
|
2143
|
+
return `const ${x.variable} = dynamic(() => import('${x.import}'), { ssr: true });`;
|
|
2144
|
+
}).join('\n') : undefined;
|
|
2145
|
+
// The actual entries for the factory
|
|
2146
|
+
const factoryEntries = [...components.map(x => {
|
|
2147
|
+
return ` {
|
|
2148
|
+
type: '${x.key}',${x.variant && x.variant !== 'default' ? `
|
|
2149
|
+
variant: '${x.variant}',` : ''}
|
|
2150
|
+
component: ${x.variable}${x.suspenseVariable ? `,
|
|
2151
|
+
useSuspense: true,
|
|
2152
|
+
loader: ${x.suspenseVariable}` : ''}${x.isClient ? `,
|
|
2153
|
+
isClient: true` : ''}
|
|
2154
|
+
}`;
|
|
2155
|
+
}), ...subFactories.map(x => ` ...${x.variable}`)];
|
|
2156
|
+
// The body of the factory
|
|
2157
|
+
const factoryBody = `// Build dictionary
|
|
2158
|
+
export const ${factoryName} : ComponentTypeDictionary = [${factoryEntries.length > 0 ? '\n' + factoryEntries.join(',\n') + '\n' : ''}];`;
|
|
2159
|
+
// Combine everything into one string
|
|
2160
|
+
return [factoryIntro, factoryImports, dynamicImports, clientComponents, factoryBody, factoryOutro].filter(x => (x?.length || 0) > 0).join('\n\n') + '\n';
|
|
1716
2161
|
}
|
|
1717
2162
|
|
|
1718
2163
|
const NextJsCreateCommand = {
|
|
@@ -1752,16 +2197,23 @@ const NextJsFragmentsCommand = {
|
|
|
1752
2197
|
handler: async (args, opts) => {
|
|
1753
2198
|
// Prepare
|
|
1754
2199
|
const { loadedContentTypes, createdTypeFolders } = opts || {};
|
|
1755
|
-
const { components: basePath, _config: { debug }, force } = parseArgs(args);
|
|
2200
|
+
const { components: basePath, _config: { debug }, force, path: appPath } = parseArgs(args);
|
|
1756
2201
|
const client = createCmsClient(args);
|
|
2202
|
+
// Get content types
|
|
1757
2203
|
const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
|
|
2204
|
+
const allContentTypesMap = new Map();
|
|
2205
|
+
allContentTypes.forEach(x => {
|
|
2206
|
+
if (x.key)
|
|
2207
|
+
allContentTypesMap.set(x.key, x);
|
|
2208
|
+
});
|
|
2209
|
+
const generator = new DocumentGenerator(allContentTypesMap);
|
|
1758
2210
|
// Start process
|
|
1759
2211
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL fragments\n`));
|
|
1760
2212
|
const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
|
|
1761
|
-
const tracker = new
|
|
2213
|
+
const tracker = new PropertyCollisionTracker(appPath);
|
|
1762
2214
|
const dependencies = [];
|
|
1763
2215
|
const updatedTypes = contentTypes.map(contentType => {
|
|
1764
|
-
const { written, propertyTypes } = createComponentFragments(contentType, getTypeFolder(typeFolders, contentType.key), force, debug, tracker);
|
|
2216
|
+
const { written, propertyTypes } = createComponentFragments(contentType, getTypeFolder(typeFolders, contentType.key), force, debug, tracker, generator);
|
|
1765
2217
|
dependencies.push(...propertyTypes);
|
|
1766
2218
|
return written ? contentType.key : undefined;
|
|
1767
2219
|
}).filter(x => x);
|
|
@@ -1781,7 +2233,7 @@ const NextJsFragmentsCommand = {
|
|
|
1781
2233
|
typeFolders.push(tf);
|
|
1782
2234
|
}
|
|
1783
2235
|
return tf;
|
|
1784
|
-
}, force, debug);
|
|
2236
|
+
}, force, debug, tracker, generator);
|
|
1785
2237
|
// Report outcome
|
|
1786
2238
|
if (generatedProps.length > 0)
|
|
1787
2239
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
|
|
@@ -1791,7 +2243,7 @@ const NextJsFragmentsCommand = {
|
|
|
1791
2243
|
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
1792
2244
|
}
|
|
1793
2245
|
};
|
|
1794
|
-
function createComponentFragments(contentType, typePath, force, debug, propertyTracker = new Map()) {
|
|
2246
|
+
function createComponentFragments(contentType, typePath, force, debug, propertyTracker = new Map(), generator) {
|
|
1795
2247
|
const baseQueryFile = typePath.fragmentFile;
|
|
1796
2248
|
let writeFragment = true;
|
|
1797
2249
|
if (fs.existsSync(baseQueryFile)) {
|
|
@@ -1810,13 +2262,13 @@ function createComponentFragments(contentType, typePath, force, debug, propertyT
|
|
|
1810
2262
|
}
|
|
1811
2263
|
let written = false;
|
|
1812
2264
|
if (writeFragment) {
|
|
1813
|
-
const fragment =
|
|
2265
|
+
const fragment = generator.buildFragment(contentType, undefined, false, propertyTracker);
|
|
1814
2266
|
fs.writeFileSync(baseQueryFile, fragment);
|
|
1815
2267
|
written = true;
|
|
1816
2268
|
}
|
|
1817
|
-
return { written, propertyTypes:
|
|
2269
|
+
return { written, propertyTypes: DocumentGenerator.getReferencedPropertyComponents(contentType) };
|
|
1818
2270
|
}
|
|
1819
|
-
function createPropertyFragments(propertyTypesList, allContentTypes, selectTypeFolder, force = false, debug = false) {
|
|
2271
|
+
function createPropertyFragments(propertyTypesList, allContentTypes, selectTypeFolder, force = false, debug = false, propertyTracker = new Map(), generator) {
|
|
1820
2272
|
const returnValue = [];
|
|
1821
2273
|
for (const propertyContentTypeKey of propertyTypesList.filter((v, i, a) => a.indexOf(v, i + 1) <= i)) {
|
|
1822
2274
|
// Load the ContentType definition
|
|
@@ -1848,16 +2300,16 @@ function createPropertyFragments(propertyTypesList, allContentTypes, selectTypeF
|
|
|
1848
2300
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) property fragment\n`));
|
|
1849
2301
|
}
|
|
1850
2302
|
if (mustWrite) {
|
|
1851
|
-
const fragment =
|
|
2303
|
+
const fragment = generator.buildFragment(contentType, undefined, true, propertyTracker);
|
|
1852
2304
|
fs.writeFileSync(propertyFragmentFile, fragment);
|
|
1853
2305
|
returnValue.push(contentType.key);
|
|
1854
2306
|
}
|
|
1855
2307
|
// Recurse down for properties that we're not yet rendering
|
|
1856
|
-
const referencedPropertyTypes =
|
|
2308
|
+
const referencedPropertyTypes = DocumentGenerator.getReferencedPropertyComponents(contentType).filter(x => !propertyTypesList.includes(x));
|
|
1857
2309
|
if (referencedPropertyTypes.length > 0) {
|
|
1858
2310
|
if (debug)
|
|
1859
2311
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Component property ${propertyContentTypeKey} uses components a property, recursing down\n`));
|
|
1860
|
-
const additionalProperties = createPropertyFragments(referencedPropertyTypes, allContentTypes, selectTypeFolder, force, debug);
|
|
2312
|
+
const additionalProperties = createPropertyFragments(referencedPropertyTypes, allContentTypes, selectTypeFolder, force, debug, propertyTracker, generator);
|
|
1861
2313
|
returnValue.push(...additionalProperties);
|
|
1862
2314
|
}
|
|
1863
2315
|
}
|
|
@@ -1875,16 +2327,23 @@ const NextJsQueriesCommand = {
|
|
|
1875
2327
|
handler: async (args, opts) => {
|
|
1876
2328
|
// Prepare
|
|
1877
2329
|
const { loadedContentTypes, createdTypeFolders } = opts || {};
|
|
1878
|
-
const { components: basePath, _config: { debug }, force } = parseArgs(args);
|
|
2330
|
+
const { components: basePath, _config: { debug }, force, path: appPath } = parseArgs(args);
|
|
1879
2331
|
const client = createCmsClient(args);
|
|
1880
2332
|
const { contentTypes, all: allContentTypes } = loadedContentTypes ?? await getContentTypes(client, args);
|
|
2333
|
+
const allContentTypesMap = new Map();
|
|
2334
|
+
allContentTypes.forEach(x => {
|
|
2335
|
+
if (x.key)
|
|
2336
|
+
allContentTypesMap.set(x.key, x);
|
|
2337
|
+
});
|
|
2338
|
+
const generator = new DocumentGenerator(allContentTypesMap);
|
|
2339
|
+
const propertyTracker = new PropertyCollisionTracker(appPath);
|
|
1881
2340
|
// Start process
|
|
1882
2341
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Generating GraphQL queries\n`));
|
|
1883
2342
|
const dependencies = [];
|
|
1884
2343
|
const typeFolders = createdTypeFolders ?? createTypeFolders(contentTypes, basePath, debug);
|
|
1885
2344
|
const updatedTypes = contentTypes.map(contentType => {
|
|
1886
2345
|
const typePath = getTypeFolder(typeFolders, contentType.key);
|
|
1887
|
-
const { written, propertyTypes } = createGraphQueries(contentType, typePath, force, debug);
|
|
2346
|
+
const { written, propertyTypes } = createGraphQueries(contentType, typePath, force, debug, propertyTracker, generator);
|
|
1888
2347
|
dependencies.push(...propertyTypes);
|
|
1889
2348
|
return written ? contentType.key : undefined;
|
|
1890
2349
|
}).filter(x => x).flat();
|
|
@@ -1903,7 +2362,7 @@ const NextJsQueriesCommand = {
|
|
|
1903
2362
|
typeFolders.push(tf);
|
|
1904
2363
|
}
|
|
1905
2364
|
return tf;
|
|
1906
|
-
}, force, debug);
|
|
2365
|
+
}, force, debug, propertyTracker, generator);
|
|
1907
2366
|
// Report outcome
|
|
1908
2367
|
if (generatedProps.length > 0)
|
|
1909
2368
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Created/updated GraphQL property fragments for ${generatedProps.join(', ')}\n`));
|
|
@@ -1913,7 +2372,7 @@ const NextJsQueriesCommand = {
|
|
|
1913
2372
|
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
1914
2373
|
}
|
|
1915
2374
|
};
|
|
1916
|
-
function createGraphQueries(contentType, typePath, force, debug) {
|
|
2375
|
+
function createGraphQueries(contentType, typePath, force, debug, tracker, generator) {
|
|
1917
2376
|
const baseQueryFile = typePath.queryFile;
|
|
1918
2377
|
let mustWrite = true;
|
|
1919
2378
|
if (fs.existsSync(baseQueryFile)) {
|
|
@@ -1931,12 +2390,12 @@ function createGraphQueries(contentType, typePath, force, debug) {
|
|
|
1931
2390
|
process.stdout.write(chalk.gray(`${figures.arrowRight} Creating ${contentType.displayName} (${contentType.key}) base fragment\n`));
|
|
1932
2391
|
}
|
|
1933
2392
|
if (mustWrite) {
|
|
1934
|
-
const fragment =
|
|
2393
|
+
const fragment = generator.buildGetQuery(contentType, undefined, tracker);
|
|
1935
2394
|
fs.writeFileSync(baseQueryFile, fragment);
|
|
1936
2395
|
}
|
|
1937
2396
|
return {
|
|
1938
2397
|
written: mustWrite,
|
|
1939
|
-
propertyTypes:
|
|
2398
|
+
propertyTypes: DocumentGenerator.getReferencedPropertyComponents(contentType)
|
|
1940
2399
|
};
|
|
1941
2400
|
}
|
|
1942
2401
|
|
|
@@ -1958,12 +2417,12 @@ const SchemaDownloadCommand = {
|
|
|
1958
2417
|
const fullSchemaDir = path.normalize(path.join(projectPath, schemaDir));
|
|
1959
2418
|
const relativeSchemaDir = path.relative(projectPath, fullSchemaDir);
|
|
1960
2419
|
process.stdout.write(`\n${figures.arrowRight} Validating schema folder ${relativeSchemaDir}\n`);
|
|
1961
|
-
const schemaDirInfo = await
|
|
2420
|
+
const schemaDirInfo = await fs$1.stat(fullSchemaDir).catch((e) => { if (e.code == 'ENOENT')
|
|
1962
2421
|
return undefined;
|
|
1963
2422
|
else
|
|
1964
2423
|
throw e; });
|
|
1965
2424
|
if (!schemaDirInfo) {
|
|
1966
|
-
await
|
|
2425
|
+
await fs$1.mkdir(fullSchemaDir, { recursive: true });
|
|
1967
2426
|
}
|
|
1968
2427
|
else if (!schemaDirInfo.isDirectory()) {
|
|
1969
2428
|
process.stderr.write(chalk.redBright(chalk.bold(`${figures.cross} [Error] The schema directory exists, but is not a directory (${relativeSchemaDir})\n`)));
|
|
@@ -1976,7 +2435,7 @@ const SchemaDownloadCommand = {
|
|
|
1976
2435
|
const schemaFile = path.normalize(path.join(fullSchemaDir, `${schema.title.toLowerCase()}.schema.json`));
|
|
1977
2436
|
const relativePath = path.relative(projectPath, schemaFile);
|
|
1978
2437
|
try {
|
|
1979
|
-
await
|
|
2438
|
+
await fs$1.writeFile(schemaFile, JSON.stringify(schema.schema, undefined, 2), {
|
|
1980
2439
|
encoding: "utf-8",
|
|
1981
2440
|
flag: force ? 'w' : 'wx'
|
|
1982
2441
|
});
|
|
@@ -2216,7 +2675,293 @@ function writeFileAsync(path, data) {
|
|
|
2216
2675
|
});
|
|
2217
2676
|
}
|
|
2218
2677
|
|
|
2219
|
-
|
|
2678
|
+
const MigrateCommand = {
|
|
2679
|
+
command: "project:migrate",
|
|
2680
|
+
describe: "Automate the directory naming convention update",
|
|
2681
|
+
builder,
|
|
2682
|
+
async handler(args, opts) {
|
|
2683
|
+
const { components: componentsPath} = parseArgs(args);
|
|
2684
|
+
const files = await glob(['*/**/*'], { cwd: componentsPath });
|
|
2685
|
+
const operations = [];
|
|
2686
|
+
for (const file of files) {
|
|
2687
|
+
const fullPath = path.join(componentsPath, file);
|
|
2688
|
+
const pathInfo = await fs$1.stat(fullPath);
|
|
2689
|
+
if (pathInfo.isDirectory()) { // We only need to rename directories
|
|
2690
|
+
const dirName = path.basename(fullPath);
|
|
2691
|
+
const dirParent = path.dirname(fullPath);
|
|
2692
|
+
const newDirName = keyToSlug(dirName);
|
|
2693
|
+
// We're ignoring casing as changing that will confuse Git
|
|
2694
|
+
if (dirName.toLowerCase() !== newDirName.toLowerCase()) {
|
|
2695
|
+
process.stdout.write(`${figures.arrowRight} Updating ${file}\n`);
|
|
2696
|
+
const newFullPath = path.join(dirParent, newDirName);
|
|
2697
|
+
operations.push({ from: fullPath, to: newFullPath });
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
if (pathInfo.isFile() && (fullPath.endsWith('.opti-style.json') || fullPath.endsWith('.opti-type.json'))) {
|
|
2701
|
+
const fileName = path.basename(fullPath);
|
|
2702
|
+
const newFileName = keyToSlug(fileName, { preserveCharacters: ['.'] });
|
|
2703
|
+
if (fileName.toLowerCase() !== newFileName.toLowerCase()) {
|
|
2704
|
+
const filePath = path.dirname(fullPath);
|
|
2705
|
+
const newFullPath = path.join(filePath, newFileName);
|
|
2706
|
+
operations.unshift({ from: fullPath, to: newFullPath });
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
for (const operation of operations) {
|
|
2711
|
+
void await fs$1.rename(operation.from, operation.to);
|
|
2712
|
+
}
|
|
2713
|
+
NextJsFactoryCommand.handler(args);
|
|
2714
|
+
}
|
|
2715
|
+
};
|
|
2716
|
+
|
|
2717
|
+
const SECTION_START = '<!-- @remkoj/optimizely-packages:start -->';
|
|
2718
|
+
const SECTION_END = '<!-- @remkoj/optimizely-packages:end -->';
|
|
2719
|
+
const ProjectAiCommand = {
|
|
2720
|
+
command: 'project:ai',
|
|
2721
|
+
describe: 'Create or update AI assistant configuration files (AGENTS.md, CLAUDE.md, GitHub Copilot, Cursor) so installed @remkoj package documentation is available to the model',
|
|
2722
|
+
builder: (yargs) => {
|
|
2723
|
+
yargs.option('force', {
|
|
2724
|
+
alias: 'f',
|
|
2725
|
+
description: 'Overwrite existing files entirely instead of merging into existing content',
|
|
2726
|
+
boolean: true,
|
|
2727
|
+
type: 'boolean',
|
|
2728
|
+
demandOption: false,
|
|
2729
|
+
default: false,
|
|
2730
|
+
});
|
|
2731
|
+
yargs.option('tools', {
|
|
2732
|
+
alias: 't',
|
|
2733
|
+
description: 'AI tools to configure',
|
|
2734
|
+
array: true,
|
|
2735
|
+
type: 'string',
|
|
2736
|
+
choices: ['agents', 'claude', 'copilot', 'cursor'],
|
|
2737
|
+
demandOption: false,
|
|
2738
|
+
default: ['agents', 'claude', 'copilot', 'cursor'],
|
|
2739
|
+
});
|
|
2740
|
+
return yargs;
|
|
2741
|
+
},
|
|
2742
|
+
handler: async (args) => {
|
|
2743
|
+
const projectPath = path.isAbsolute(args.path)
|
|
2744
|
+
? args.path
|
|
2745
|
+
: path.normalize(path.join(process.cwd(), args.path));
|
|
2746
|
+
const force = args.force ?? false;
|
|
2747
|
+
const tools = args.tools ?? ['agents', 'claude', 'copilot', 'cursor'];
|
|
2748
|
+
// Discover installed @remkoj packages that include AGENTS.md
|
|
2749
|
+
process.stdout.write(`\n${figures.arrowRight} Scanning for @remkoj packages with AGENTS.md\n`);
|
|
2750
|
+
const packages = await findRemkojPackages(projectPath);
|
|
2751
|
+
if (packages.length === 0) {
|
|
2752
|
+
process.stderr.write(chalk.yellowBright(`\n${figures.warning} No @remkoj packages with AGENTS.md found in node_modules — run \`npm install\` first.\n`));
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
for (const pkg of packages) {
|
|
2756
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} ${chalk.bold(pkg.name)}\n`);
|
|
2757
|
+
}
|
|
2758
|
+
if (tools.includes('agents')) {
|
|
2759
|
+
process.stdout.write(`\n${figures.arrowRight} Updating AGENTS.md\n`);
|
|
2760
|
+
await updateWithSectionMarkers(path.join(projectPath, 'AGENTS.md'), buildAgentsMdSection(packages, projectPath), buildAgentsMdPreamble(), force, projectPath);
|
|
2761
|
+
}
|
|
2762
|
+
if (tools.includes('claude')) {
|
|
2763
|
+
process.stdout.write(`\n${figures.arrowRight} Writing CLAUDE.md\n`);
|
|
2764
|
+
await writeNewFileOnly(path.join(projectPath, 'CLAUDE.md'), `# CLAUDE.md\n\nSee [AGENTS.md](./AGENTS.md) for guidance on working with the Optimizely DXP packages in this project.\n`, force, projectPath);
|
|
2765
|
+
}
|
|
2766
|
+
if (tools.includes('copilot')) {
|
|
2767
|
+
process.stdout.write(`\n${figures.arrowRight} Updating GitHub Copilot instruction files\n`);
|
|
2768
|
+
await updateCopilotInstructions(projectPath, packages);
|
|
2769
|
+
}
|
|
2770
|
+
if (tools.includes('cursor')) {
|
|
2771
|
+
process.stdout.write(`\n${figures.arrowRight} Writing Cursor rules\n`);
|
|
2772
|
+
const rulesDir = path.join(projectPath, '.cursor', 'rules');
|
|
2773
|
+
await ensureDir(rulesDir);
|
|
2774
|
+
await writeCursorRules(path.join(rulesDir, 'optimizely-packages.mdc'), packages, projectPath);
|
|
2775
|
+
}
|
|
2776
|
+
process.stdout.write(`\n${chalk.greenBright(chalk.bold(figures.tick))} AI assistant configuration updated successfully\n`);
|
|
2777
|
+
},
|
|
2778
|
+
};
|
|
2779
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
2780
|
+
async function findRemkojPackages(projectPath) {
|
|
2781
|
+
const scopeDir = path.join(projectPath, 'node_modules', '@remkoj');
|
|
2782
|
+
const packages = [];
|
|
2783
|
+
try {
|
|
2784
|
+
const dirs = await fs$1.readdir(scopeDir);
|
|
2785
|
+
for (const dir of dirs) {
|
|
2786
|
+
const agentsPath = path.join(scopeDir, dir, 'AGENTS.md');
|
|
2787
|
+
try {
|
|
2788
|
+
await fs$1.access(agentsPath);
|
|
2789
|
+
packages.push({ name: `@remkoj/${dir}`, agentsPath });
|
|
2790
|
+
}
|
|
2791
|
+
catch {
|
|
2792
|
+
// Package has no AGENTS.md — skip silently
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
catch {
|
|
2797
|
+
// @remkoj scope directory not found in node_modules
|
|
2798
|
+
}
|
|
2799
|
+
return packages;
|
|
2800
|
+
}
|
|
2801
|
+
function wrapSection(content) {
|
|
2802
|
+
return `${SECTION_START}\n${content}\n${SECTION_END}`;
|
|
2803
|
+
}
|
|
2804
|
+
function buildReferenceList(packages, projectPath) {
|
|
2805
|
+
return packages
|
|
2806
|
+
.map((pkg) => {
|
|
2807
|
+
const rel = path.relative(projectPath, pkg.agentsPath).replace(/\\/g, '/');
|
|
2808
|
+
return `- [\`${pkg.name}\`](${rel})`;
|
|
2809
|
+
})
|
|
2810
|
+
.join('\n');
|
|
2811
|
+
}
|
|
2812
|
+
function buildAgentsMdPreamble() {
|
|
2813
|
+
return `# AI Agent Documentation — Optimizely DXP Packages
|
|
2814
|
+
|
|
2815
|
+
The section below is auto-generated by \`opti-cms project:ai\`. Re-run after installing or upgrading \`@remkoj\` packages.
|
|
2816
|
+
|
|
2817
|
+
`;
|
|
2818
|
+
}
|
|
2819
|
+
function buildAgentsMdSection(packages, projectPath) {
|
|
2820
|
+
return `The following \`@remkoj\` packages are installed. Read their \`AGENTS.md\` for documentation:\n\n${buildReferenceList(packages, projectPath)}`;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Creates or updates the auto-generated section delimited by SECTION_START/SECTION_END markers.
|
|
2824
|
+
*
|
|
2825
|
+
* - File absent: creates it with preamble + section.
|
|
2826
|
+
* - File present with markers: replaces the section in-place.
|
|
2827
|
+
* - File present without markers: appends the section (preserves existing content).
|
|
2828
|
+
* - force=true: always replaces the entire file with preamble + section.
|
|
2829
|
+
*/
|
|
2830
|
+
async function updateWithSectionMarkers(filePath, sectionContent, preamble, force, projectPath) {
|
|
2831
|
+
const rel = path.relative(projectPath, filePath);
|
|
2832
|
+
const section = wrapSection(sectionContent);
|
|
2833
|
+
let existing;
|
|
2834
|
+
try {
|
|
2835
|
+
existing = await fs$1.readFile(filePath, 'utf-8');
|
|
2836
|
+
}
|
|
2837
|
+
catch {
|
|
2838
|
+
// File does not exist — will be created
|
|
2839
|
+
}
|
|
2840
|
+
if (!existing || force) {
|
|
2841
|
+
await fs$1.writeFile(filePath, preamble + section + '\n', 'utf-8');
|
|
2842
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} ${existing ? 'Replaced' : 'Created'} ${rel}\n`);
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
if (existing.includes(SECTION_START) && existing.includes(SECTION_END)) {
|
|
2846
|
+
const before = existing.slice(0, existing.indexOf(SECTION_START));
|
|
2847
|
+
const after = existing.slice(existing.indexOf(SECTION_END) + SECTION_END.length);
|
|
2848
|
+
await fs$1.writeFile(filePath, before + section + after, 'utf-8');
|
|
2849
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} Updated section in ${rel}\n`);
|
|
2850
|
+
}
|
|
2851
|
+
else {
|
|
2852
|
+
await fs$1.writeFile(filePath, existing.trimEnd() + '\n\n' + section + '\n', 'utf-8');
|
|
2853
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} Appended section to ${rel}\n`);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
/**
|
|
2857
|
+
* Writes a file only when it does not already exist.
|
|
2858
|
+
* When force=true the file is always (re)written.
|
|
2859
|
+
*/
|
|
2860
|
+
async function writeNewFileOnly(filePath, content, force, projectPath) {
|
|
2861
|
+
const rel = path.relative(projectPath, filePath);
|
|
2862
|
+
let exists = false;
|
|
2863
|
+
try {
|
|
2864
|
+
await fs$1.access(filePath);
|
|
2865
|
+
exists = true;
|
|
2866
|
+
}
|
|
2867
|
+
catch {
|
|
2868
|
+
// File does not exist
|
|
2869
|
+
}
|
|
2870
|
+
if (exists && !force) {
|
|
2871
|
+
process.stdout.write(` ${chalk.yellowBright(figures.info)} ${rel} already exists — use --force to overwrite\n`);
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
await fs$1.writeFile(filePath, content, 'utf-8');
|
|
2875
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} ${exists ? 'Replaced' : 'Created'} ${rel}\n`);
|
|
2876
|
+
}
|
|
2877
|
+
/**
|
|
2878
|
+
* Writes one `.vscode/instructions/remkoj-<name>.instructions.md` file per package,
|
|
2879
|
+
* each containing a `#file:` reference to the package's AGENTS.md in node_modules.
|
|
2880
|
+
* VS Code Copilot resolves `#file:` references at query time, so content stays
|
|
2881
|
+
* current after a package upgrade without re-running this command.
|
|
2882
|
+
*
|
|
2883
|
+
* Also removes the deprecated `github.copilot.chat.codeGeneration.instructions`
|
|
2884
|
+
* key from `.vscode/settings.json` if it was written by a previous run.
|
|
2885
|
+
*/
|
|
2886
|
+
async function updateCopilotInstructions(projectPath, packages) {
|
|
2887
|
+
const instructionsDir = path.join(projectPath, '.vscode', 'instructions');
|
|
2888
|
+
await ensureDir(instructionsDir);
|
|
2889
|
+
// Remove stale remkoj instruction files from previous runs
|
|
2890
|
+
const FILE_PREFIX = 'remkoj-';
|
|
2891
|
+
try {
|
|
2892
|
+
const existing = await fs$1.readdir(instructionsDir);
|
|
2893
|
+
for (const f of existing) {
|
|
2894
|
+
if (f.startsWith(FILE_PREFIX) && f.endsWith('.instructions.md')) {
|
|
2895
|
+
await fs$1.unlink(path.join(instructionsDir, f));
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
catch {
|
|
2900
|
+
// Directory unreadable — ignore
|
|
2901
|
+
}
|
|
2902
|
+
// Write one instruction file per package using a #file: reference
|
|
2903
|
+
for (const pkg of packages) {
|
|
2904
|
+
const safeName = pkg.name.replace('@remkoj/', '');
|
|
2905
|
+
const filePath = path.join(instructionsDir, `${FILE_PREFIX}${safeName}.instructions.md`);
|
|
2906
|
+
const ref = path.relative(projectPath, pkg.agentsPath).replace(/\\/g, '/');
|
|
2907
|
+
const content = `---\napplyTo: "**"\n---\n\n#file:${ref}\n`;
|
|
2908
|
+
await fs$1.writeFile(filePath, content, 'utf-8');
|
|
2909
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} Wrote ${path.relative(projectPath, filePath)}\n`);
|
|
2910
|
+
}
|
|
2911
|
+
// Clean up the deprecated settings key written by previous versions of this command
|
|
2912
|
+
await removeDeprecatedCopilotSetting(projectPath);
|
|
2913
|
+
}
|
|
2914
|
+
async function removeDeprecatedCopilotSetting(projectPath) {
|
|
2915
|
+
const settingsFile = path.join(projectPath, '.vscode', 'settings.json');
|
|
2916
|
+
let settings;
|
|
2917
|
+
try {
|
|
2918
|
+
settings = JSON.parse(await fs$1.readFile(settingsFile, 'utf-8'));
|
|
2919
|
+
}
|
|
2920
|
+
catch {
|
|
2921
|
+
return; // File absent or invalid — nothing to clean up
|
|
2922
|
+
}
|
|
2923
|
+
const key = 'github.copilot.chat.codeGeneration.instructions';
|
|
2924
|
+
if (!(key in settings))
|
|
2925
|
+
return;
|
|
2926
|
+
const remaining = settings[key].filter((e) => !e.file?.startsWith('node_modules/@remkoj/'));
|
|
2927
|
+
if (remaining.length === 0) {
|
|
2928
|
+
delete settings[key];
|
|
2929
|
+
}
|
|
2930
|
+
else {
|
|
2931
|
+
settings[key] = remaining;
|
|
2932
|
+
}
|
|
2933
|
+
await fs$1.writeFile(settingsFile, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
2934
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} Removed deprecated setting from ${path.relative(projectPath, settingsFile)}\n`);
|
|
2935
|
+
}
|
|
2936
|
+
/** Writes the Cursor rules file with a reference list; always regenerated since it is fully derived from node_modules. */
|
|
2937
|
+
async function writeCursorRules(filePath, packages, projectPath) {
|
|
2938
|
+
const rel = path.relative(projectPath, filePath);
|
|
2939
|
+
let exists = false;
|
|
2940
|
+
try {
|
|
2941
|
+
await fs$1.access(filePath);
|
|
2942
|
+
exists = true;
|
|
2943
|
+
}
|
|
2944
|
+
catch {
|
|
2945
|
+
// File does not exist
|
|
2946
|
+
}
|
|
2947
|
+
const list = buildReferenceList(packages, projectPath);
|
|
2948
|
+
const content = [
|
|
2949
|
+
'---',
|
|
2950
|
+
'description: Documentation for @remkoj Optimizely DXP packages installed in this project',
|
|
2951
|
+
'alwaysApply: true',
|
|
2952
|
+
'---',
|
|
2953
|
+
'',
|
|
2954
|
+
wrapSection(`The following \`@remkoj\` packages are installed. Read their \`AGENTS.md\` for documentation:\n\n${list}`),
|
|
2955
|
+
'',
|
|
2956
|
+
].join('\n');
|
|
2957
|
+
await fs$1.writeFile(filePath, content, 'utf-8');
|
|
2958
|
+
process.stdout.write(` ${chalk.greenBright(figures.tick)} ${exists ? 'Updated' : 'Created'} ${rel}\n`);
|
|
2959
|
+
}
|
|
2960
|
+
async function ensureDir(dirPath) {
|
|
2961
|
+
await fs$1.mkdir(dirPath, { recursive: true });
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
var version = "6.0.0-rc.2";
|
|
2220
2965
|
var name = "opti-cms";
|
|
2221
2966
|
var APP = {
|
|
2222
2967
|
version: version,
|
|
@@ -2233,15 +2978,6 @@ const CmsVersionCommand = {
|
|
|
2233
2978
|
const client = createCmsClient(args);
|
|
2234
2979
|
if (client.debug)
|
|
2235
2980
|
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Reading version information Optimizely CMS\n`));
|
|
2236
|
-
const versionInfo = await client.getInstanceInfo().catch((error) => {
|
|
2237
|
-
switch (error.message) {
|
|
2238
|
-
case "fetch failed":
|
|
2239
|
-
throw new Error("Unable to connect to Optimizely CMS, please verify that the CMS URL is correct");
|
|
2240
|
-
default:
|
|
2241
|
-
throw error;
|
|
2242
|
-
}
|
|
2243
|
-
});
|
|
2244
|
-
const hasPreview2Info = versionInfo.results.preview2Data?.baseUrl ? true : false;
|
|
2245
2981
|
const info = new Table({
|
|
2246
2982
|
head: [
|
|
2247
2983
|
chalk.yellow(chalk.bold("Component")),
|
|
@@ -2250,18 +2986,10 @@ const CmsVersionCommand = {
|
|
|
2250
2986
|
colWidths: [30, 60],
|
|
2251
2987
|
colAligns: ["left", "left"]
|
|
2252
2988
|
});
|
|
2253
|
-
info.push(["Base URL",
|
|
2254
|
-
if (hasPreview2Info)
|
|
2255
|
-
info.push(["Base URL (Preview 2)", versionInfo.results.preview2Data?.baseUrl ?? 'n/a']);
|
|
2989
|
+
info.push(["Base URL", client.cmsUrl.href]);
|
|
2256
2990
|
info.push(["Client API", client.apiVersion]);
|
|
2257
|
-
info.push(["Service API", versionInfo.apiVersion]);
|
|
2258
|
-
if (hasPreview2Info)
|
|
2259
|
-
info.push(["Service API (Preview 2)", versionInfo.results.preview2Data?.apiVersion ?? 'n/a']);
|
|
2260
|
-
info.push(["CMS Build", versionInfo.cmsVersion]);
|
|
2261
|
-
info.push(["Service Build", versionInfo.serviceVersion]);
|
|
2262
2991
|
info.push(["SDK", APP.version]);
|
|
2263
2992
|
process.stdout.write(info.toString() + "\n");
|
|
2264
|
-
process.stdout.write(chalk.yellowBright(`${figures.arrowRight} Optimizely CMS Status: ${versionInfo.status}\n`));
|
|
2265
2993
|
process.stdout.write(chalk.green(chalk.bold(figures.tick + " Done")) + "\n");
|
|
2266
2994
|
}
|
|
2267
2995
|
};
|
|
@@ -2318,7 +3046,7 @@ const CmsResetCommand = {
|
|
|
2318
3046
|
});
|
|
2319
3047
|
if (!didRemoveCustomProperties) {
|
|
2320
3048
|
const ctUrl = new URL('/ui/EPiServer.Cms.UI.Admin/default#/ContentTypes', client.cmsUrl);
|
|
2321
|
-
process.stdout.write(`${chalk.redBright(figures.cross)} Custom properties on the
|
|
3049
|
+
process.stdout.write(`${chalk.redBright(figures.cross)} Custom properties on the "Blank Section" and "Blank Experience" Content Types must be removed manually, please remove and restart command.\n`);
|
|
2322
3050
|
process.stdout.write(` ${figures.arrowRight} Content Type manager: ${ctUrl}\n`);
|
|
2323
3051
|
process.exit(1);
|
|
2324
3052
|
}
|
|
@@ -2352,7 +3080,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
|
|
|
2352
3080
|
const children = await getContentItemChildren(client, key);
|
|
2353
3081
|
let hasError = false;
|
|
2354
3082
|
let removedCount = 0;
|
|
2355
|
-
for (
|
|
3083
|
+
for (const child of children.items)
|
|
2356
3084
|
if (child.key) {
|
|
2357
3085
|
removedCount = removedCount + await deleteContentItem(client, child.key).catch(e => {
|
|
2358
3086
|
console.error(e);
|
|
@@ -2360,7 +3088,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
|
|
|
2360
3088
|
return 0;
|
|
2361
3089
|
});
|
|
2362
3090
|
}
|
|
2363
|
-
for (
|
|
3091
|
+
for (const child of children.assets)
|
|
2364
3092
|
if (child.key) {
|
|
2365
3093
|
removedCount = removedCount + await deleteContentItem(client, child.key).catch(e => {
|
|
2366
3094
|
console.error(e);
|
|
@@ -2379,7 +3107,7 @@ async function deleteContentItem(client, key, onlyChildren = false) {
|
|
|
2379
3107
|
process.stderr.write(chalk.redBright(` ${figures.cross}The ContentItem ${key} is a reserved item, skipping deletion\n`));
|
|
2380
3108
|
return removedCount;
|
|
2381
3109
|
}
|
|
2382
|
-
const deleteResult = await client.
|
|
3110
|
+
const deleteResult = await client.contentDelete({ path: { key }, headers: { "cms-permanent-delete": true } }).then(r => r ? r.key : key).catch((e) => {
|
|
2383
3111
|
if (e.status == 404)
|
|
2384
3112
|
return key;
|
|
2385
3113
|
throw e;
|
|
@@ -2403,7 +3131,6 @@ async function resetSystemTypes(client) {
|
|
|
2403
3131
|
const newType = await client.contentTypesPatch({
|
|
2404
3132
|
path: { key: systemType },
|
|
2405
3133
|
body: {
|
|
2406
|
-
key: systemType,
|
|
2407
3134
|
properties: {}
|
|
2408
3135
|
}
|
|
2409
3136
|
}).catch((e) => e.status == 404 ? undefined : null);
|
|
@@ -2415,7 +3142,7 @@ async function resetSystemTypes(client) {
|
|
|
2415
3142
|
async function removeContentTypes(client) {
|
|
2416
3143
|
const contentTypes = await getAllTypes(client);
|
|
2417
3144
|
let removedCount = 0;
|
|
2418
|
-
for (
|
|
3145
|
+
for (const contentType of contentTypes.filter(item => item.source == '' && !reservedTypes.includes(item.key))) {
|
|
2419
3146
|
if (client.debug)
|
|
2420
3147
|
process.stdout.write(` ${chalk.blueBright(figures.arrowRight)} Removing content type ${contentType.displayName} (${contentType.key})\n`);
|
|
2421
3148
|
const result = await client.contentTypesDelete({ path: { key: contentType.key } }).catch((e) => {
|
|
@@ -2456,7 +3183,7 @@ async function getAllTemplates(client, batchSize = 100) {
|
|
|
2456
3183
|
}
|
|
2457
3184
|
throw e;
|
|
2458
3185
|
});
|
|
2459
|
-
const totalItemCount = items.totalItemCount ?? items.items?.length ?? 0;
|
|
3186
|
+
const totalItemCount = items.totalCount ?? items.totalItemCount ?? items.items?.length ?? 0;
|
|
2460
3187
|
const pageSize = items.pageSize ?? items.items?.length ?? 0;
|
|
2461
3188
|
const actualItems = items.items ?? [];
|
|
2462
3189
|
const pageCount = Math.ceil(totalItemCount / pageSize);
|
|
@@ -2486,7 +3213,7 @@ async function getAllTypes(client, batchSize = 100) {
|
|
|
2486
3213
|
}
|
|
2487
3214
|
throw e;
|
|
2488
3215
|
});
|
|
2489
|
-
const totalItemCount = items.totalItemCount ?? items.items?.length ?? 0;
|
|
3216
|
+
const totalItemCount = items.totalCount ?? items.totalItemCount ?? items.items?.length ?? 0;
|
|
2490
3217
|
const pageSize = items.pageSize ?? items.items?.length ?? 0;
|
|
2491
3218
|
const actualItems = items.items ?? [];
|
|
2492
3219
|
const pageCount = Math.ceil(totalItemCount / pageSize);
|
|
@@ -2506,7 +3233,7 @@ async function getAllTypes(client, batchSize = 100) {
|
|
|
2506
3233
|
return actualItems;
|
|
2507
3234
|
}
|
|
2508
3235
|
async function getAllAssets(client, parentKey, batchSize = 100) {
|
|
2509
|
-
const items = await client.
|
|
3236
|
+
const items = await client.contentListAssets({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
|
|
2510
3237
|
if (e.status == 404) {
|
|
2511
3238
|
return {
|
|
2512
3239
|
items: [],
|
|
@@ -2516,12 +3243,12 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
|
|
|
2516
3243
|
}
|
|
2517
3244
|
throw e;
|
|
2518
3245
|
});
|
|
2519
|
-
const totalItemCount = items.totalItemCount ?? items.items?.length ?? 0;
|
|
3246
|
+
const totalItemCount = items.totalCount ?? items.totalItemCount ?? items.items?.length ?? 0;
|
|
2520
3247
|
const pageSize = items.pageSize ?? items.items?.length ?? 0;
|
|
2521
3248
|
const actualItems = items.items ?? [];
|
|
2522
3249
|
const pageCount = Math.ceil(totalItemCount / pageSize);
|
|
2523
3250
|
for (let pageNr = 1; pageNr < pageCount; pageNr++) {
|
|
2524
|
-
const pageData = await client.
|
|
3251
|
+
const pageData = await client.contentListAssets({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
|
|
2525
3252
|
if (e.status == 404) {
|
|
2526
3253
|
return {
|
|
2527
3254
|
items: [],
|
|
@@ -2536,7 +3263,7 @@ async function getAllAssets(client, parentKey, batchSize = 100) {
|
|
|
2536
3263
|
return actualItems;
|
|
2537
3264
|
}
|
|
2538
3265
|
async function getAllItems(client, parentKey, batchSize = 100) {
|
|
2539
|
-
const items = await client.
|
|
3266
|
+
const items = await client.contentListItems({ path: { key: parentKey }, query: { pageIndex: 0, pageSize: batchSize } }).catch((e) => {
|
|
2540
3267
|
if (e.status == 404) {
|
|
2541
3268
|
return {
|
|
2542
3269
|
items: [],
|
|
@@ -2546,12 +3273,12 @@ async function getAllItems(client, parentKey, batchSize = 100) {
|
|
|
2546
3273
|
}
|
|
2547
3274
|
throw e;
|
|
2548
3275
|
});
|
|
2549
|
-
const totalItemCount = items.totalItemCount ?? items.items?.length ?? 0;
|
|
3276
|
+
const totalItemCount = items.totalCount ?? items.totalItemCount ?? items.items?.length ?? 0;
|
|
2550
3277
|
const pageSize = items.pageSize ?? items.items?.length ?? 0;
|
|
2551
3278
|
const actualItems = items.items ?? [];
|
|
2552
3279
|
const pageCount = Math.ceil(totalItemCount / pageSize);
|
|
2553
3280
|
for (let pageNr = 1; pageNr < pageCount; pageNr++) {
|
|
2554
|
-
const pageData = await client.
|
|
3281
|
+
const pageData = await client.contentListItems({ path: { key: parentKey }, query: { pageIndex: pageNr, pageSize } }).catch((e) => {
|
|
2555
3282
|
if (e.status == 404) {
|
|
2556
3283
|
return {
|
|
2557
3284
|
items: [],
|
|
@@ -2584,20 +3311,24 @@ const commands = [
|
|
|
2584
3311
|
StylesListCommand,
|
|
2585
3312
|
StylesPullCommand,
|
|
2586
3313
|
StylesPushCommand,
|
|
3314
|
+
StylesDeleteCommand,
|
|
2587
3315
|
TypesPullCommand,
|
|
2588
|
-
TypesPushCommand
|
|
3316
|
+
TypesPushCommand,
|
|
3317
|
+
MigrateCommand,
|
|
3318
|
+
ProjectAiCommand,
|
|
2589
3319
|
];
|
|
2590
3320
|
|
|
2591
3321
|
async function main() {
|
|
2592
|
-
const
|
|
2593
|
-
const
|
|
3322
|
+
const projectDir = getProjectDir();
|
|
3323
|
+
const envFiles = prepare(projectDir);
|
|
3324
|
+
const app = createOptiCmsApp(APP.name, APP.version, undefined, envFiles, projectDir);
|
|
2594
3325
|
app.command(commands);
|
|
2595
3326
|
try {
|
|
2596
3327
|
await app.parse(process.argv.slice(2));
|
|
2597
3328
|
}
|
|
2598
3329
|
catch {
|
|
2599
3330
|
//We're ignoring error here, as yargs will already generate the "nice output" for it
|
|
2600
|
-
//console.log('Caught error')
|
|
3331
|
+
//console.log ('Caught error')
|
|
2601
3332
|
}
|
|
2602
3333
|
}
|
|
2603
3334
|
main();
|