@adobe/spacecat-shared-tokowaka-client 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [@adobe/spacecat-shared-tokowaka-client-v1.3.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.3.0...@adobe/spacecat-shared-tokowaka-client-v1.3.1) (2025-12-10)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * heading and toc mappers ([#1230](https://github.com/adobe/spacecat-shared/issues/1230)) ([f767fb3](https://github.com/adobe/spacecat-shared/commit/f767fb319c97ed64848a1c5fc5913693a52290d9))
7
+
1
8
  # [@adobe/spacecat-shared-tokowaka-client-v1.3.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.2.4...@adobe/spacecat-shared-tokowaka-client-v1.3.0) (2025-12-04)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-tokowaka-client",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Tokowaka Client for SpaceCat - Edge optimization config management",
5
5
  "type": "module",
6
6
  "engines": {
package/src/index.d.ts CHANGED
@@ -34,7 +34,9 @@ export const TARGET_USER_AGENTS_CATEGORIES: {
34
34
 
35
35
  export interface TokowakaMetaconfig {
36
36
  siteId: string;
37
- prerender: boolean;
37
+ prerender?: {
38
+ allowList?: string[];
39
+ } | boolean;
38
40
  }
39
41
 
40
42
  export interface TokowakaConfig {
@@ -231,6 +233,23 @@ export class FaqMapper extends BaseOpportunityMapper {
231
233
  ): TokawakaPatch[];
232
234
  }
233
235
 
236
+ /**
237
+ * Table of Contents (TOC) opportunity mapper
238
+ * Handles conversion of TOC suggestions to Tokowaka patches with HAST format
239
+ */
240
+ export class TocMapper extends BaseOpportunityMapper {
241
+ constructor(log: any);
242
+
243
+ getOpportunityType(): string;
244
+ requiresPrerender(): boolean;
245
+ suggestionsToPatches(
246
+ urlPath: string,
247
+ suggestions: Suggestion[],
248
+ opportunityId: string
249
+ ): TokawakaPatch[];
250
+ canDeploy(suggestion: Suggestion): { eligible: boolean; reason?: string };
251
+ }
252
+
234
253
  /**
235
254
  * Registry for opportunity mappers
236
255
  */
package/src/index.js CHANGED
@@ -122,7 +122,8 @@ class TokowakaClient {
122
122
  opportunity.getId(),
123
123
  );
124
124
 
125
- if (patches.length === 0) {
125
+ // Check if configs without patches are allowed (e.g., prerender-only)
126
+ if (patches.length === 0 && !mapper.allowConfigsWithoutPatch()) {
126
127
  return null;
127
128
  }
128
129
 
@@ -445,7 +446,16 @@ class TokowakaClient {
445
446
  // Generate configuration for this URL with eligible suggestions only
446
447
  const newConfig = this.generateConfig(fullUrl, opportunity, urlSuggestions);
447
448
 
448
- if (!newConfig || !newConfig.patches || newConfig.patches.length === 0) {
449
+ if (!newConfig) {
450
+ this.log.warn(`No config generated for URL: ${fullUrl}`);
451
+ // eslint-disable-next-line no-continue
452
+ continue;
453
+ }
454
+
455
+ // Check if mapper allows configs without patches (e.g., prerender-only config)
456
+ const allowsNoPatch = mapper.allowConfigsWithoutPatch() && newConfig.patches.length === 0;
457
+
458
+ if (!allowsNoPatch && (!newConfig.patches || newConfig.patches.length === 0)) {
449
459
  this.log.warn(`No eligible suggestions to deploy for URL: ${fullUrl}`);
450
460
  // eslint-disable-next-line no-continue
451
461
  continue;
@@ -534,7 +544,7 @@ class TokowakaClient {
534
544
  // eslint-disable-next-line no-await-in-loop
535
545
  const existingConfig = await this.fetchConfig(fullUrl);
536
546
 
537
- if (!existingConfig || !existingConfig.patches) {
547
+ if (!existingConfig) {
538
548
  this.log.warn(`No existing configuration found for URL: ${fullUrl}`);
539
549
  // eslint-disable-next-line no-continue
540
550
  continue;
@@ -543,6 +553,40 @@ class TokowakaClient {
543
553
  // Extract suggestion IDs to remove for this URL
544
554
  const suggestionIdsToRemove = urlSuggestions.map((s) => s.getId());
545
555
 
556
+ // For prerender opportunities, disable prerender flag
557
+ if (opportunityType === 'prerender') {
558
+ this.log.info(`Rolling back prerender config for URL: ${fullUrl}`);
559
+
560
+ // Set prerender to false (keep other patches if they exist)
561
+ const updatedConfig = {
562
+ ...existingConfig,
563
+ prerender: false,
564
+ };
565
+
566
+ // Upload updated config to S3 for this URL
567
+ // eslint-disable-next-line no-await-in-loop
568
+ const s3Path = await this.uploadConfig(fullUrl, updatedConfig);
569
+ s3Paths.push(s3Path);
570
+
571
+ // Invalidate CDN cache
572
+ // eslint-disable-next-line no-await-in-loop
573
+ const cdnInvalidationResult = await this.invalidateCdnCache(
574
+ fullUrl,
575
+ this.env.TOKOWAKA_CDN_PROVIDER,
576
+ );
577
+ cdnInvalidations.push(cdnInvalidationResult);
578
+
579
+ totalRemovedCount += 1; // Count as 1 rollback
580
+ // eslint-disable-next-line no-continue
581
+ continue;
582
+ }
583
+
584
+ if (!existingConfig.patches) {
585
+ this.log.info(`No patches found in configuration for URL: ${fullUrl}`);
586
+ // eslint-disable-next-line no-continue
587
+ continue;
588
+ }
589
+
546
590
  // Use mapper to remove patches
547
591
  const updatedConfig = mapper.rollbackPatches(
548
592
  existingConfig,
@@ -551,7 +595,7 @@ class TokowakaClient {
551
595
  );
552
596
 
553
597
  if (updatedConfig.removedCount === 0) {
554
- this.log.warn(`No patches found for URL: ${fullUrl}`);
598
+ this.log.warn(`No patches found for suggestions at URL: ${fullUrl}`);
555
599
  // eslint-disable-next-line no-continue
556
600
  continue;
557
601
  }
@@ -661,7 +705,17 @@ class TokowakaClient {
661
705
  this.log.debug(`Generating preview Tokowaka config for opportunity ${opportunity.getId()}`);
662
706
  const newConfig = this.generateConfig(previewUrl, opportunity, eligibleSuggestions);
663
707
 
664
- if (!newConfig || !newConfig.patches || newConfig.patches.length === 0) {
708
+ if (!newConfig) {
709
+ this.log.warn('No config generated for preview');
710
+ return {
711
+ config: null,
712
+ succeededSuggestions: [],
713
+ failedSuggestions: suggestions,
714
+ };
715
+ }
716
+
717
+ /* c8 ignore next 9 */
718
+ if (newConfig.patches.length === 0 && !mapper.allowConfigsWithoutPatch()) {
665
719
  this.log.warn('No eligible suggestions to preview');
666
720
  return {
667
721
  config: null,
@@ -68,6 +68,18 @@ export default class BaseOpportunityMapper {
68
68
  throw new Error('canDeploy() must be implemented by subclass');
69
69
  }
70
70
 
71
+ /**
72
+ * Determines if configurations without patches are allowed for this opportunity type
73
+ * By default, configurations must have at least one patch to be valid
74
+ * Override this method in subclasses if configs without patches are acceptable
75
+ * (e.g., prerender-only configs that just enable prerendering)
76
+ * @returns {boolean} - True if configs without patches are allowed
77
+ */
78
+ // eslint-disable-next-line class-methods-use-this
79
+ allowConfigsWithoutPatch() {
80
+ return false;
81
+ }
82
+
71
83
  /**
72
84
  * Helper method to create base patch structure
73
85
  * @protected
@@ -85,7 +85,7 @@ export default class HeadingsMapper extends BaseOpportunityMapper {
85
85
  const checkType = data?.checkType;
86
86
 
87
87
  // Check if checkType is eligible
88
- const eligibleCheckTypes = ['heading-empty', 'heading-missing-h1', 'heading-h1-length'];
88
+ const eligibleCheckTypes = ['heading-empty', 'heading-missing-h1', 'heading-h1-length', 'heading-order-invalid'];
89
89
  if (!eligibleCheckTypes.includes(checkType)) {
90
90
  return {
91
91
  eligible: false,
@@ -127,6 +127,21 @@ export default class HeadingsMapper extends BaseOpportunityMapper {
127
127
  }
128
128
  }
129
129
 
130
+ if (checkType === 'heading-order-invalid') {
131
+ if (data.transformRules?.action !== 'replaceWith') {
132
+ return {
133
+ eligible: false,
134
+ reason: `transformRules.action must be replaceWith for ${checkType}`,
135
+ };
136
+ }
137
+ if (data.transformRules?.valueFormat !== 'hast') {
138
+ return {
139
+ eligible: false,
140
+ reason: `transformRules.valueFormat must be hast for ${checkType}`,
141
+ };
142
+ }
143
+ }
144
+
130
145
  return { eligible: true };
131
146
  }
132
147
  }
@@ -14,7 +14,9 @@ import HeadingsMapper from './headings-mapper.js';
14
14
  import ContentSummarizationMapper from './content-summarization-mapper.js';
15
15
  import FaqMapper from './faq-mapper.js';
16
16
  import ReadabilityMapper from './readability-mapper.js';
17
+ import TocMapper from './toc-mapper.js';
17
18
  import GenericMapper from './generic-mapper.js';
19
+ import PrerenderMapper from './prerender-mapper.js';
18
20
 
19
21
  /**
20
22
  * Registry for opportunity mappers
@@ -37,7 +39,9 @@ export default class MapperRegistry {
37
39
  ContentSummarizationMapper,
38
40
  FaqMapper,
39
41
  ReadabilityMapper,
42
+ TocMapper,
40
43
  GenericMapper,
44
+ PrerenderMapper,
41
45
  // more mappers here
42
46
  ];
43
47
 
@@ -0,0 +1,78 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { hasText } from '@adobe/spacecat-shared-utils';
14
+ import BaseOpportunityMapper from './base-mapper.js';
15
+
16
+ /**
17
+ * Prerender mapper for prerender opportunities
18
+ * Handles prerender suggestions - these don't generate patches, just enable prerendering
19
+ */
20
+ export default class PrerenderMapper extends BaseOpportunityMapper {
21
+ constructor(log) {
22
+ super(log);
23
+ this.opportunityType = 'prerender';
24
+ this.prerenderRequired = true;
25
+ }
26
+
27
+ getOpportunityType() {
28
+ return this.opportunityType;
29
+ }
30
+
31
+ requiresPrerender() {
32
+ return this.prerenderRequired;
33
+ }
34
+
35
+ /**
36
+ * Prerender allows configurations without patches
37
+ * Prerender-only configs just enable prerendering without DOM modifications
38
+ * @returns {boolean} - True, prerender allows configs without patches
39
+ */
40
+ // eslint-disable-next-line class-methods-use-this
41
+ allowConfigsWithoutPatch() {
42
+ return true;
43
+ }
44
+
45
+ /**
46
+ * Converts suggestions to Tokowaka patches
47
+ * For prerender, we don't generate patches - just mark prerender as required
48
+ * @param {string} urlPath - URL path for the suggestions
49
+ * @param {Array} suggestions - Array of suggestion entities for the same URL
50
+ * @param {string} opportunityId - Opportunity ID
51
+ * @returns {Array} - Empty array (prerender doesn't use patches)
52
+ */
53
+ // eslint-disable-next-line class-methods-use-this, no-unused-vars
54
+ suggestionsToPatches(urlPath, suggestions, opportunityId) {
55
+ // Prerender suggestions don't generate patches
56
+ // They just enable prerendering for the URL
57
+ // Return empty array so no patches are created
58
+ return [];
59
+ }
60
+
61
+ /**
62
+ * Checks if a prerender suggestion can be deployed
63
+ * @param {Object} suggestion - Suggestion object
64
+ * @returns {Object} { eligible: boolean, reason?: string }
65
+ */
66
+ // eslint-disable-next-line class-methods-use-this
67
+ canDeploy(suggestion) {
68
+ const data = suggestion.getData();
69
+
70
+ // Validate URL exists
71
+ if (!hasText(data?.url)) {
72
+ return { eligible: false, reason: 'url is required' };
73
+ }
74
+
75
+ // All prerender suggestions with valid URLs are eligible
76
+ return { eligible: true };
77
+ }
78
+ }
@@ -0,0 +1,116 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { hasText } from '@adobe/spacecat-shared-utils';
14
+ import { TARGET_USER_AGENTS_CATEGORIES } from '../constants.js';
15
+ import BaseOpportunityMapper from './base-mapper.js';
16
+
17
+ /**
18
+ * Mapper for Table of Contents (TOC) opportunity
19
+ * Handles conversion of TOC suggestions to Tokowaka patches
20
+ */
21
+ export default class TocMapper extends BaseOpportunityMapper {
22
+ constructor(log) {
23
+ super(log);
24
+ this.opportunityType = 'toc';
25
+ this.prerenderRequired = true;
26
+ }
27
+
28
+ getOpportunityType() {
29
+ return this.opportunityType;
30
+ }
31
+
32
+ requiresPrerender() {
33
+ return this.prerenderRequired;
34
+ }
35
+
36
+ /**
37
+ * Converts suggestions to Tokowaka patches
38
+ * @param {string} urlPath - URL path for the suggestions
39
+ * @param {Array} suggestions - Array of suggestion entities for the same URL
40
+ * @param {string} opportunityId - Opportunity ID
41
+ * @returns {Array} - Array of Tokowaka patch objects
42
+ */
43
+ suggestionsToPatches(urlPath, suggestions, opportunityId) {
44
+ const patches = [];
45
+
46
+ suggestions.forEach((suggestion) => {
47
+ const eligibility = this.canDeploy(suggestion);
48
+ if (!eligibility.eligible) {
49
+ this.log.warn(`TOC suggestion ${suggestion.getId()} cannot be deployed: ${eligibility.reason}`);
50
+ return;
51
+ }
52
+
53
+ const data = suggestion.getData();
54
+ const { transformRules } = data;
55
+
56
+ const patch = {
57
+ ...this.createBasePatch(suggestion, opportunityId),
58
+ op: transformRules.action,
59
+ selector: transformRules.selector,
60
+ value: transformRules.value,
61
+ valueFormat: 'hast',
62
+ target: TARGET_USER_AGENTS_CATEGORIES.AI_BOTS,
63
+ };
64
+
65
+ patches.push(patch);
66
+ });
67
+
68
+ return patches;
69
+ }
70
+
71
+ /**
72
+ * Checks if a TOC suggestion can be deployed
73
+ * @param {Object} suggestion - Suggestion object
74
+ * @returns {Object} { eligible: boolean, reason?: string }
75
+ */
76
+ // eslint-disable-next-line class-methods-use-this
77
+ canDeploy(suggestion) {
78
+ const data = suggestion.getData();
79
+ const checkType = data?.checkType;
80
+
81
+ // Check if checkType is eligible
82
+ if (checkType !== 'toc') {
83
+ return {
84
+ eligible: false,
85
+ reason: `Only toc checkType can be deployed. This suggestion has checkType: ${checkType}`,
86
+ };
87
+ }
88
+
89
+ // Validate required fields
90
+ if (!hasText(data.transformRules?.selector)) {
91
+ return { eligible: false, reason: 'transformRules.selector is required' };
92
+ }
93
+
94
+ if (!data.transformRules?.value) {
95
+ return { eligible: false, reason: 'transformRules.value is required' };
96
+ }
97
+
98
+ if (data.transformRules?.valueFormat !== 'hast') {
99
+ return {
100
+ eligible: false,
101
+ reason: 'transformRules.valueFormat must be hast for toc',
102
+ };
103
+ }
104
+
105
+ // Validate action
106
+ const validActions = ['insertBefore', 'insertAfter'];
107
+ if (!validActions.includes(data.transformRules?.action)) {
108
+ return {
109
+ eligible: false,
110
+ reason: `transformRules.action must be one of ${validActions.join(', ')} for toc`,
111
+ };
112
+ }
113
+
114
+ return { eligible: true };
115
+ }
116
+ }