@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 +7 -0
- package/package.json +1 -1
- package/src/index.d.ts +20 -1
- package/src/index.js +59 -5
- package/src/mappers/base-mapper.js +12 -0
- package/src/mappers/headings-mapper.js +16 -1
- package/src/mappers/mapper-registry.js +4 -0
- package/src/mappers/prerender-mapper.js +78 -0
- package/src/mappers/toc-mapper.js +116 -0
- package/test/index.test.js +243 -0
- package/test/mappers/generic-mapper.test.js +6 -0
- package/test/mappers/headings-mapper.test.js +154 -3
- package/test/mappers/prerender-mapper.test.js +216 -0
- package/test/mappers/toc-mapper.test.js +616 -0
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
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|