@tokenoftrust/cli 2.4.11 → 2.4.13
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/package.json +1 -1
- package/src/commands/ship.mjs +11 -2
- package/src/declared-config.d.mts +7 -0
- package/src/declared-config.mjs +115 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.13",
|
|
4
4
|
"description": "Token of Trust developer CLI — clone a tenant store, run it locally with save→reload, and submit it for preview. Installs the `tot` command.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Token of Trust",
|
package/src/commands/ship.mjs
CHANGED
|
@@ -309,6 +309,8 @@ export function refusalNextStep(reason) {
|
|
|
309
309
|
return "upgrade the storefront subscription to enable go-live";
|
|
310
310
|
case "golive_unconfirmed":
|
|
311
311
|
return "re-run `tot ship` and confirm the plan";
|
|
312
|
+
case "compliance_review":
|
|
313
|
+
return 'approve the warnings with `tot ship --accept-quality-findings "<reason>"`, or fix them in `.tot/config.json` and publish again';
|
|
312
314
|
default:
|
|
313
315
|
return "re-run `tot ship`";
|
|
314
316
|
}
|
|
@@ -580,7 +582,11 @@ export async function runShip(
|
|
|
580
582
|
acceptRes = await fetchImpl(`${base}/api/changes/accept-issue`, {
|
|
581
583
|
method: "POST",
|
|
582
584
|
headers: { "content-type": "application/json", ...authHeaders },
|
|
583
|
-
body: JSON.stringify({
|
|
585
|
+
body: JSON.stringify({
|
|
586
|
+
operation: "publication",
|
|
587
|
+
reason: acceptQualityReason,
|
|
588
|
+
...(plan.ok && plan.artifactDigest ? { revision: plan.artifactDigest } : {}),
|
|
589
|
+
}),
|
|
584
590
|
});
|
|
585
591
|
} catch (e) {
|
|
586
592
|
console.error(
|
|
@@ -598,7 +604,10 @@ export async function runShip(
|
|
|
598
604
|
);
|
|
599
605
|
return 1;
|
|
600
606
|
}
|
|
601
|
-
|
|
607
|
+
const accepted = acceptData.accepted ?? {};
|
|
608
|
+
const count = Array.isArray(accepted.findingIds) ? accepted.findingIds.length : 0;
|
|
609
|
+
const what = accepted.problemCode === "compliance-review" ? "compliance warning(s)" : "quality finding(s)";
|
|
610
|
+
console.log(` ✓ ${count ? `${count} ` : ""}${what} accepted for this revision; continuing the publish.`);
|
|
602
611
|
}
|
|
603
612
|
|
|
604
613
|
// Each approved space goes live through its own reviewed transition, under the confirm above.
|
|
@@ -11,6 +11,13 @@ export const TOT_CONFIG_KEYS: string[];
|
|
|
11
11
|
export const COMPLIANCE_KEYS: string[];
|
|
12
12
|
export const FEATURE_KEYS: string[];
|
|
13
13
|
export const SITE_TYPES: string[];
|
|
14
|
+
export const SEO_KEYS: readonly string[];
|
|
15
|
+
export const STRUCTURED_DATA_KEYS: readonly string[];
|
|
16
|
+
export const STRUCTURED_DATA_PAGE_TYPES: readonly string[];
|
|
17
|
+
export const STRUCTURED_DATA_NODE_TOKENS: readonly string[];
|
|
18
|
+
export const STRUCTURED_DATA_NAMED_TYPES: readonly string[];
|
|
19
|
+
export const STRUCTURED_DATA_NAME_SOURCES: readonly string[];
|
|
20
|
+
export function seoDeclarationProblems(seo: unknown): string[];
|
|
14
21
|
export const COMPLIANCE_NOTICES: Record<string, string>;
|
|
15
22
|
export const MANDATED_NOTICE_KEYS: string[];
|
|
16
23
|
export function nearestKey(key: string, known: readonly string[]): string | null;
|
package/src/declared-config.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
export const TOT_CONFIG_KEYS = [
|
|
19
19
|
"$schema", "tenant", "scope", "mappings", "hostPlatform", "sample", "seed",
|
|
20
|
-
"displayName", "siteType", "compliance", "capabilities", "features", "embeds",
|
|
20
|
+
"displayName", "siteType", "compliance", "capabilities", "features", "embeds", "seo",
|
|
21
21
|
];
|
|
22
22
|
// Keys other tooling has written that the platform does NOT read, with the key it does.
|
|
23
23
|
const RENAMED_CONFIG_KEYS = { appDomain: "scope", tenantId: "tenant" };
|
|
@@ -30,6 +30,115 @@ export const FEATURE_KEYS = [
|
|
|
30
30
|
];
|
|
31
31
|
export const SITE_TYPES = ["commerce", "marketing"];
|
|
32
32
|
|
|
33
|
+
/** Every `seo` key (mirrors packages/public-runtime/src/declared-tenant-config.mjs). */
|
|
34
|
+
export const SEO_KEYS = Object.freeze(["trailingSlash", "structuredData"]);
|
|
35
|
+
|
|
36
|
+
/** Every `seo.structuredData` key. */
|
|
37
|
+
export const STRUCTURED_DATA_KEYS = Object.freeze(["graphs", "names", "reviews"]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The page types a structured-data profile declares a graph for. `productGroup` is a product
|
|
41
|
+
* with more than one variant; without its own graph it renders the `product` graph.
|
|
42
|
+
* `collection` covers every archive: a catalog collection, the collections index, and the
|
|
43
|
+
* blog index and its category/tag/author archives.
|
|
44
|
+
*/
|
|
45
|
+
export const STRUCTURED_DATA_PAGE_TYPES = Object.freeze([
|
|
46
|
+
"home", "product", "productGroup", "collection", "post", "page",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The JSON-LD nodes a profile may list, as the `@type` each emits (a multi-type node joins its
|
|
51
|
+
* sorted types with `+`, the key a parity snapshot records). `Person:author` is the page's
|
|
52
|
+
* author; every other Organization/Person token is the site entity.
|
|
53
|
+
*/
|
|
54
|
+
export const STRUCTURED_DATA_NODE_TOKENS = Object.freeze([
|
|
55
|
+
"Organization", "Person", "Organization+Person",
|
|
56
|
+
"WebSite", "ImageObject",
|
|
57
|
+
"WebPage", "ItemPage", "CollectionPage", "AboutPage", "ContactPage",
|
|
58
|
+
"Person:author",
|
|
59
|
+
"Article", "BlogPosting",
|
|
60
|
+
"Product", "ProductGroup",
|
|
61
|
+
"FAQPage", "BreadcrumbList", "ItemList",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
/** Node types whose `name` a profile may source, and the sources. */
|
|
65
|
+
export const STRUCTURED_DATA_NAMED_TYPES = Object.freeze(["Product", "ProductGroup", "Article", "BlogPosting"]);
|
|
66
|
+
export const STRUCTURED_DATA_NAME_SOURCES = Object.freeze(["documentTitle", "title"]);
|
|
67
|
+
|
|
68
|
+
const isPlainObject = (value) =>
|
|
69
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* What is wrong with a declared `seo` value, one sentence per problem; `[]` when it is valid.
|
|
73
|
+
* A profile that names a node or page type the renderer does not build is refused whole: a
|
|
74
|
+
* partly-applied profile emits a graph nobody declared.
|
|
75
|
+
*
|
|
76
|
+
* @param {unknown} seo
|
|
77
|
+
* @returns {string[]}
|
|
78
|
+
*/
|
|
79
|
+
export function seoDeclarationProblems(seo) {
|
|
80
|
+
if (!isPlainObject(seo)) return ["`seo` must be an object"];
|
|
81
|
+
const problems = [];
|
|
82
|
+
const source = /** @type {Record<string, unknown>} */ (seo);
|
|
83
|
+
for (const key of Object.keys(source)) {
|
|
84
|
+
if (!SEO_KEYS.includes(key)) problems.push(`\`seo.${key}\` is not a seo key (${SEO_KEYS.join(", ")})`);
|
|
85
|
+
}
|
|
86
|
+
if (source.trailingSlash !== undefined && typeof source.trailingSlash !== "boolean") {
|
|
87
|
+
problems.push("`seo.trailingSlash` must be true or false");
|
|
88
|
+
}
|
|
89
|
+
const sd = source.structuredData;
|
|
90
|
+
if (sd === undefined) return problems;
|
|
91
|
+
if (!isPlainObject(sd)) return [...problems, "`seo.structuredData` must be an object"];
|
|
92
|
+
const data = /** @type {Record<string, unknown>} */ (sd);
|
|
93
|
+
for (const key of Object.keys(data)) {
|
|
94
|
+
if (!STRUCTURED_DATA_KEYS.includes(key)) {
|
|
95
|
+
problems.push(`\`seo.structuredData.${key}\` is not a structuredData key (${STRUCTURED_DATA_KEYS.join(", ")})`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!isPlainObject(data.graphs) || Object.keys(/** @type {object} */ (data.graphs)).length === 0) {
|
|
99
|
+
problems.push("`seo.structuredData.graphs` must map at least one page type to its node list");
|
|
100
|
+
} else {
|
|
101
|
+
for (const [pageType, tokens] of Object.entries(/** @type {Record<string, unknown>} */ (data.graphs))) {
|
|
102
|
+
const at = `seo.structuredData.graphs.${pageType}`;
|
|
103
|
+
if (!STRUCTURED_DATA_PAGE_TYPES.includes(pageType)) {
|
|
104
|
+
problems.push(`\`${at}\`: unknown page type (${STRUCTURED_DATA_PAGE_TYPES.join(", ")})`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!Array.isArray(tokens) || tokens.length === 0) {
|
|
108
|
+
problems.push(`\`${at}\` must be a non-empty array of node types`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
for (const token of tokens) {
|
|
113
|
+
if (typeof token !== "string" || !STRUCTURED_DATA_NODE_TOKENS.includes(token)) {
|
|
114
|
+
problems.push(`\`${at}\`: ${JSON.stringify(token)} is not a node type the renderer builds (${STRUCTURED_DATA_NODE_TOKENS.join(", ")})`);
|
|
115
|
+
} else if (seen.has(token)) {
|
|
116
|
+
problems.push(`\`${at}\` lists ${token} twice`);
|
|
117
|
+
}
|
|
118
|
+
seen.add(token);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (data.names !== undefined) {
|
|
123
|
+
if (!isPlainObject(data.names)) {
|
|
124
|
+
problems.push("`seo.structuredData.names` must map a node type to documentTitle or title");
|
|
125
|
+
} else {
|
|
126
|
+
for (const [type, from] of Object.entries(/** @type {Record<string, unknown>} */ (data.names))) {
|
|
127
|
+
if (!STRUCTURED_DATA_NAMED_TYPES.includes(type)) {
|
|
128
|
+
problems.push(`\`seo.structuredData.names.${type}\`: only ${STRUCTURED_DATA_NAMED_TYPES.join(", ")} take a declared name`);
|
|
129
|
+
} else if (typeof from !== "string" || !STRUCTURED_DATA_NAME_SOURCES.includes(from)) {
|
|
130
|
+
problems.push(`\`seo.structuredData.names.${type}\` must be one of ${STRUCTURED_DATA_NAME_SOURCES.join(", ")}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (data.reviews !== undefined && typeof data.reviews !== "boolean") {
|
|
136
|
+
problems.push("`seo.structuredData.reviews` must be true or false");
|
|
137
|
+
}
|
|
138
|
+
return problems;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
33
142
|
/** What each compliance key puts on the page — the thing that is missing when it is. */
|
|
34
143
|
export const COMPLIANCE_NOTICES = {
|
|
35
144
|
ruleProfile: "checkout compliance rules",
|
|
@@ -361,6 +470,11 @@ export function validateDeclaredConfig(config, file = ".tot/config.json") {
|
|
|
361
470
|
&& !(Array.isArray(config.embeds) && config.embeds.every((/** @type {unknown} */ s) => typeof s === "string"))) {
|
|
362
471
|
out.push(mk(ERROR, "config-embeds", file, '`embeds` must be an array of embed slugs, e.g. ["youtube"]'));
|
|
363
472
|
}
|
|
473
|
+
if (config.seo !== undefined) {
|
|
474
|
+
for (const problem of seoDeclarationProblems(config.seo)) {
|
|
475
|
+
out.push(mk(ERROR, "config-seo", `${file} seo`, `${problem} — the whole \`seo\` declaration is ignored until it is fixed`));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
364
478
|
return out;
|
|
365
479
|
}
|
|
366
480
|
|