@taprootio/site-authoring 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -29
- package/examples/riverbend-wellness/nav.json +3 -6
- package/examples/riverbend-wellness/pages/visit.md +1 -1
- package/package.json +2 -2
- package/src/api.js +118 -31
- package/src/changelog.js +48 -0
- package/src/changelog.json +83 -0
- package/src/cli-release.js +9 -1
- package/src/cli.js +129 -14
- package/src/constants.js +19 -0
- package/src/errors.js +3 -0
- package/src/footer-contract.js +5 -3
- package/src/forms-workspace.js +22 -0
- package/src/heading-outline.js +76 -0
- package/src/index.js +1 -0
- package/src/output.js +9 -0
- package/src/page-metadata.js +2 -1
- package/src/page-structure-checks.js +356 -0
- package/src/presentation-reference.js +41 -6
- package/src/problems.js +6 -1
- package/src/redirects-contract.js +50 -140
- package/src/reference-help.js +77 -33
- package/src/reserved-paths.js +4 -10
- package/src/setting-reference.js +438 -0
- package/src/settings-catalog.js +2 -3
- package/src/site-check-findings.js +188 -0
- package/src/site-check-rules.json +35 -0
- package/src/site-checks.js +353 -0
- package/src/staging-check.js +48 -9
- package/src/staging-served.js +320 -0
- package/src/theme-projection.js +28 -55
- package/src/theme-validation.js +2 -1
- package/src/transport.js +13 -0
- package/src/typed-page-reference.js +128 -4
- package/src/verbs/changelog.js +62 -0
- package/src/verbs/deploy.js +38 -10
- package/src/verbs/index.js +3 -0
- package/src/verbs/nav-push.js +49 -1
- package/src/verbs/pages-push.js +17 -5
- package/src/verbs/plan.js +2 -1
- package/src/verbs/pull.js +5 -1
- package/src/verbs/redirects-check.js +4 -1
- package/src/verbs/staging-review.js +1 -1
- package/src/verbs/status.js +35 -28
- package/src/verbs/validate.js +24 -0
- package/src/workspace-check.js +26 -1
- package/src/workspace.js +5 -0
package/src/cli-release.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
changelogCommandSince,
|
|
2
3
|
CLI_UPGRADE_COMMAND,
|
|
3
4
|
CLI_UPGRADE_REFUSAL_FIELD,
|
|
4
5
|
CLI_VERSION,
|
|
@@ -131,6 +132,11 @@ export function isBehindLatest(latestVersion, currentVersion = CLI_VERSION) {
|
|
|
131
132
|
* `api.request_rejected`.
|
|
132
133
|
*/
|
|
133
134
|
class CliOutdatedError extends SiteAuthoringError {
|
|
135
|
+
constructor(code, message, options, latestVersion) {
|
|
136
|
+
super(code, message, options);
|
|
137
|
+
this.latestVersion = latestVersion;
|
|
138
|
+
}
|
|
139
|
+
|
|
134
140
|
refusalKind() {
|
|
135
141
|
return REFUSAL_CLI_OUTDATED;
|
|
136
142
|
}
|
|
@@ -146,7 +152,9 @@ export function cliOutdatedError(latestVersion) {
|
|
|
146
152
|
`This CLI is version ${CLI_VERSION}, and the last Taproot sign-in exchange reported ${latestVersion} as the `
|
|
147
153
|
+ "latest published release. Taproot accepts only the latest, so every online verb is refused until this "
|
|
148
154
|
+ `package is upgraded. Upgrade with: ${CLI_UPGRADE_COMMAND}. If ${latestVersion} was published in the last `
|
|
149
|
-
+ "few minutes, npm may not serve it yet — wait a moment and retry the upgrade."
|
|
155
|
+
+ "few minutes, npm may not serve it yet — wait a moment and retry the upgrade. Once upgraded, run "
|
|
156
|
+
+ `'${changelogCommandSince(CLI_VERSION)}' to see what changed.`,
|
|
150
157
|
{ field: CLI_UPGRADE_REFUSAL_FIELD },
|
|
158
|
+
latestVersion,
|
|
151
159
|
);
|
|
152
160
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeStagingReviewPath } from "./api.js";
|
|
2
|
+
import { parseCliVersion } from "./cli-release.js";
|
|
1
3
|
import { CAPABILITY_CONTENT, CAPABILITY_DEPLOYMENTS, CAPABILITY_DESIGN } from "./capabilities.js";
|
|
2
4
|
import { capabilitiesForSurface } from "./surface.js";
|
|
3
5
|
import {
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
VERB_DELIVERY_CHECK,
|
|
23
25
|
VERB_DEPLOY,
|
|
24
26
|
VERB_STAGING_REVIEW,
|
|
27
|
+
VERB_CHANGELOG,
|
|
25
28
|
VERB_ENV,
|
|
26
29
|
VERB_FOOTER_PUSH,
|
|
27
30
|
VERB_FORMS_PULL,
|
|
@@ -69,6 +72,7 @@ import {
|
|
|
69
72
|
getDesignBlueprint,
|
|
70
73
|
getFooterReference,
|
|
71
74
|
getPageTypeReference,
|
|
75
|
+
getSettingsReference,
|
|
72
76
|
getThemeReference,
|
|
73
77
|
getWorkflowReference,
|
|
74
78
|
listComponentTypeReferences,
|
|
@@ -118,7 +122,18 @@ const VERBS = Object.freeze([
|
|
|
118
122
|
+ "rather than one per run. A pulled workspace is checked offline against the pages (generated ones included, "
|
|
119
123
|
+ "so navigation to a section or tag page resolves), images, and baselines its manifests recorded at the pull. "
|
|
120
124
|
+ "A link whose address is only http(s):// plus its own text (left by older editor autolinking) is reported in "
|
|
121
|
-
+ "linkWarnings without refusing; pages push reports the same for the pages it sends.
|
|
125
|
+
+ "linkWarnings without refusing; pages push reports the same for the pages it sends. "
|
|
126
|
+
+ "The site checks that can run offline are reported in findings (a rule id, severity, file, and the value "
|
|
127
|
+
+ "found, such as link.page_missing for a link to a page the workspace does not have) and never refuse a "
|
|
128
|
+
+ "warning. For a free-form page they include the page-structure rules: seo.h1_missing when no heading, banner "
|
|
129
|
+
+ "title or hero title is the page's h1, seo.h1_demoted when a # heading is published as an h2 because the "
|
|
130
|
+
+ "banner's overlay text, a hero title or an earlier # heading already is the h1 (detail names which), "
|
|
131
|
+
+ "seo.heading_renumbered and seo.heading_before_title for headings published at another level, "
|
|
132
|
+
+ "a11y.image_alt_missing and a11y.link_text_empty; for any page seo.title_long and seo.description_missing. "
|
|
133
|
+
+ "siteChecks.checkedInEditor names the rules only the page editor evaluates (a cover, a description field, or a description's length, which descriptionWarnings reports here); "
|
|
134
|
+
+ "siteChecks.checkedBySite names the rules that need the site's own records (images, forms), which "
|
|
135
|
+
+ "'status' reports under siteHealth. A redirect source the site would refuse names its rule in "
|
|
136
|
+
+ "error.problems[].rule. This proves local structure and semantics only. It does not prove "
|
|
122
137
|
+ "authorization, live site ownership, concurrency, persisted round trips, or rendering: 'plan' checks a "
|
|
123
138
|
+ "workspace against the live site, and 'pages push --dry-run' does that for pages alone. "
|
|
124
139
|
+ `See '${CLI_BINARY_NAME} help fixture' for the fixture manifest contract and for the path of the complete `
|
|
@@ -200,6 +215,21 @@ const VERBS = Object.freeze([
|
|
|
200
215
|
+ "its value. The sign-in secret is never printed; the credential is named by id and display prefix, which are "
|
|
201
216
|
+ "what an owner needs to revoke it.",
|
|
202
217
|
},
|
|
218
|
+
{
|
|
219
|
+
name: VERB_CHANGELOG,
|
|
220
|
+
tokens: ["changelog"],
|
|
221
|
+
summary: "List what changed in each CLI release, offline.",
|
|
222
|
+
credentialFree: true,
|
|
223
|
+
configFree: true,
|
|
224
|
+
offline: true,
|
|
225
|
+
sinceOption: true,
|
|
226
|
+
note: "Only the latest CLI is accepted, so an upgrade can be forced at any time. After one, run "
|
|
227
|
+
+ "'changelog --since <the version you upgraded from>' to see every release's note and the behavior it changed, "
|
|
228
|
+
+ "and which help topics (help <topic>) to read again. With no --since it prints the installed release's entry. "
|
|
229
|
+
+ "It reads the package's own record, needs no sign-in or site, and still runs when this CLI is outdated. "
|
|
230
|
+
+ "The result lists entries newest first, each { version, note, behavior, help }; complete is false when "
|
|
231
|
+
+ "--since starts before the oldest release the record covers. 'status' reports the installed release's note.",
|
|
232
|
+
},
|
|
203
233
|
{
|
|
204
234
|
name: VERB_ENV,
|
|
205
235
|
tokens: ["env"],
|
|
@@ -308,7 +338,7 @@ const VERBS = Object.freeze([
|
|
|
308
338
|
note:
|
|
309
339
|
"Positional page paths narrow the push to those pages; with none, every workspace page is validated and sent. "
|
|
310
340
|
+ "A page names a form by key (formKey); the push looks each key up on the site, sends the form's id, and "
|
|
311
|
-
+ "refuses a key the site does not have or has archived (pages.form_unknown, pages.form_archived) before writing. "
|
|
341
|
+
+ "refuses a key the site does not have or has archived (pages.form_unknown, pages.form_archived) before writing, and says whether the key's forms/<key>.json is in the workspace. validate refuses the same key offline (rule form.key_unknown) when the workspace has a forms/ directory and no file in it is for that key. "
|
|
312
342
|
+ "A link in a page being sent whose address is only http(s):// plus its own text is reported in linkWarnings "
|
|
313
343
|
+ "(content.link_autolinked) without refusing. "
|
|
314
344
|
+ "A page is created without an author unless its source names one ('author:' in the front matter, or 'pages "
|
|
@@ -440,9 +470,12 @@ const VERBS = Object.freeze([
|
|
|
440
470
|
capabilities: [CAPABILITY_CONTENT, CAPABILITY_DEPLOYMENTS],
|
|
441
471
|
tokens: ["redirects", "check"],
|
|
442
472
|
summary: "Check the current redirect map through the authenticated staging edge.",
|
|
473
|
+
handoffOptions: true,
|
|
443
474
|
note: "Reports each entry's path, HTTP status and Location in redirects.items, and redirects.verified, without "
|
|
444
475
|
+ "following redirects. A redirect is live only with the release that carries it, so this reads the staged release. "
|
|
445
|
-
+ "The
|
|
476
|
+
+ "The check authenticates with its own handoff and never reports it. The result carries no review URL unless "
|
|
477
|
+
+ "you pass --handoff, which adds a fresh single-use one in stagingPreview.url (keep it private); "
|
|
478
|
+
+ "'staging review' mints one at any time.",
|
|
446
479
|
},
|
|
447
480
|
{
|
|
448
481
|
name: VERB_REDIRECTS_PULL,
|
|
@@ -589,11 +622,19 @@ const VERBS = Object.freeze([
|
|
|
589
622
|
tokens: ["deploy"],
|
|
590
623
|
summary: "Deploy to staging, or promote staging to production.",
|
|
591
624
|
target: true,
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
+ "
|
|
625
|
+
handoffOptions: true,
|
|
626
|
+
note: "deploy --staging does not report a review URL, because the URL is a live single-use credential and this "
|
|
627
|
+
+ "JSON is often saved to logs. Run 'staging review' to mint one, or pass --handoff to get a fresh "
|
|
628
|
+
+ "single-use one in stagingPreview.url (on a managed Docs site too, where it stages settings only): open it "
|
|
629
|
+
+ "once, then switch the site's theme toggle to review both schemes before deploy --production. Without "
|
|
630
|
+
+ "--handoff, stagingPreview.reason is staging.handoff_not_requested and stagingPreview.recovery names the "
|
|
631
|
+
+ "command. If a requested handoff could not be minted, stagingPreview.reason names why and "
|
|
595
632
|
+ "stagingPreview.recovery the next step; 'staging review' mints another. On a Standard site the result also "
|
|
596
|
-
+ "carries the automatic redirect check: routeCheck and redirects (see 'help redirects')."
|
|
633
|
+
+ "carries the automatic redirect check: routeCheck and redirects (see 'help redirects'). The edge can keep "
|
|
634
|
+
+ `serving the previous release for up to ${LIMITS.stagingPropagationCeilingSeconds} seconds after a deployment completes, so a Standard site's `
|
|
635
|
+
+ "deploy --staging waits until staging serves the new release and reports stagingServed: state served, "
|
|
636
|
+
+ "propagating (still the previous release at the bound) or unverified (the change cannot be attributed to this deployment, or the last read did not name the previous release), "
|
|
637
|
+
+ "with the time to wait or the seconds waited.",
|
|
597
638
|
},
|
|
598
639
|
{
|
|
599
640
|
name: VERB_PREVIEW_PAGE,
|
|
@@ -653,20 +694,23 @@ const VERBS = Object.freeze([
|
|
|
653
694
|
capabilities: [CAPABILITY_DEPLOYMENTS],
|
|
654
695
|
tokens: ["staging", "review"],
|
|
655
696
|
summary: "Mint a fresh single-use review handoff for the site's staging host.",
|
|
697
|
+
reviewPath: true,
|
|
656
698
|
note: "The handoff URL is reported only in the final JSON as stagingPreview.url, is single-use, and expires two "
|
|
657
|
-
+ "minutes after it is minted; keep it private.
|
|
658
|
-
+ "
|
|
699
|
+
+ "minutes after it is minted; keep it private. --path <page> opens the review on that page instead of the "
|
|
700
|
+
+ "home page: a page path as 'pull' records it ('about', '/posts/one'), optionally with a query. Open it once, then use the site's theme toggle to review both "
|
|
701
|
+
+ "schemes. deploy --staging and redirects check report no review URL unless you pass --handoff, so this is "
|
|
702
|
+
+ "the usual way to get one, on a managed Docs site too.",
|
|
659
703
|
},
|
|
660
704
|
{
|
|
661
705
|
name: VERB_STATUS,
|
|
662
706
|
surface: SURFACE_DOCS_PRESENTATION,
|
|
663
707
|
// Readiness and the deployment log are Deployments; the image list is
|
|
664
|
-
// Content, as is the
|
|
708
|
+
// Content, as is the site-health read.
|
|
665
709
|
capabilities: [CAPABILITY_CONTENT, CAPABILITY_DEPLOYMENTS],
|
|
666
710
|
tokens: ["status"],
|
|
667
711
|
summary:
|
|
668
|
-
"Report the platform authoring switch, CLI release, deployments, readiness, image processing, and
|
|
669
|
-
note: "
|
|
712
|
+
"Report the platform authoring switch, CLI release, deployments, readiness, image processing, and site health.",
|
|
713
|
+
note: "Site health lists the site checks' findings (a rule id, severity, page, and the value found) for the latest editable body of each page."
|
|
670
714
|
+ " Content permission is required; a refused or failed read fails status rather than reporting a clean site.",
|
|
671
715
|
},
|
|
672
716
|
]);
|
|
@@ -733,7 +777,8 @@ credentials, and it expires 24 hours after it is issued.
|
|
|
733
777
|
|
|
734
778
|
Verbs write one JSON result to stdout and human progress to stderr. validate,
|
|
735
779
|
help, whoami, and env are offline and read-only. login, logout, sites, use,
|
|
736
|
-
whoami, and env need no configuration and no site.
|
|
780
|
+
whoami, and env need no configuration and no site. So does changelog, which
|
|
781
|
+
also still runs when this CLI is outdated. The offline help family is
|
|
737
782
|
human-readable by default; add --json for stable reference data. Exit codes: 0
|
|
738
783
|
success, 1 failure, 2 usage fault.
|
|
739
784
|
|
|
@@ -817,6 +862,18 @@ function verbHelp(verb) {
|
|
|
817
862
|
--production Promote the completed staging deployment to production.
|
|
818
863
|
--allow-failed-preview Explicitly override the matching candidate's failed preview.`
|
|
819
864
|
: "";
|
|
865
|
+
const handoffOption = verb.handoffOptions
|
|
866
|
+
? `\n --handoff Also mint a fresh single-use review URL and report it in stagingPreview.url.
|
|
867
|
+
--no-handoff Report no review URL (the default); accepted so scripts can say so explicitly.`
|
|
868
|
+
: "";
|
|
869
|
+
const sinceOption = verb.sinceOption
|
|
870
|
+
? "\n --since <version> List the releases after this one, up to the installed release."
|
|
871
|
+
: "";
|
|
872
|
+
const reviewPathOption = verb.reviewPath
|
|
873
|
+
? "\n --path <page> Open the review on this page instead of the home page, such as 'about' or '/posts/one'."
|
|
874
|
+
: verb.handoffOptions
|
|
875
|
+
? "\n --review-path <page> With --handoff, open the review URL on this page instead of the home page."
|
|
876
|
+
: "";
|
|
820
877
|
const metaOptions = verb.metaOptions
|
|
821
878
|
? `\n --title <text> The page's title.
|
|
822
879
|
--description <text> The page's description for search results and link previews; '' clears it.
|
|
@@ -907,7 +964,7 @@ function verbHelp(verb) {
|
|
|
907
964
|
${verb.summary}
|
|
908
965
|
${boundary}
|
|
909
966
|
|
|
910
|
-
${options}${targetOption}${deliveryTargetOption}${metaOptions}${authorOptions}${jsonOption}${dryRunOption}${planOption}${nameOption}${note}
|
|
967
|
+
${options}${targetOption}${handoffOption}${reviewPathOption}${sinceOption}${deliveryTargetOption}${metaOptions}${authorOptions}${jsonOption}${dryRunOption}${planOption}${nameOption}${note}
|
|
911
968
|
`;
|
|
912
969
|
}
|
|
913
970
|
|
|
@@ -968,6 +1025,7 @@ function parseReferenceArguments(arguments_) {
|
|
|
968
1025
|
|| topic === "preview"
|
|
969
1026
|
|| topic === "theme"
|
|
970
1027
|
|| topic === "appearance"
|
|
1028
|
+
|| topic === "settings"
|
|
971
1029
|
|| topic === "footer"
|
|
972
1030
|
|| topic === "fixture"
|
|
973
1031
|
|| topic === "redirects"
|
|
@@ -1070,6 +1128,8 @@ function referenceResult(parsed) {
|
|
|
1070
1128
|
return { ...result, topic: "presentation", referenceKind: "theme", reference: getThemeReference() };
|
|
1071
1129
|
case "appearance":
|
|
1072
1130
|
return { ...result, topic: "presentation", referenceKind: "appearance", reference: getAppearanceReference() };
|
|
1131
|
+
case "settings":
|
|
1132
|
+
return { ...result, topic: "presentation", referenceKind: "settings", reference: getSettingsReference() };
|
|
1073
1133
|
case "footer":
|
|
1074
1134
|
return { ...result, topic: "presentation", referenceKind: "footer", reference: getFooterReference() };
|
|
1075
1135
|
default:
|
|
@@ -1249,6 +1309,9 @@ function parseArguments(arguments_) {
|
|
|
1249
1309
|
let propagationWaitSeconds;
|
|
1250
1310
|
let browser;
|
|
1251
1311
|
let allowFailedPreview = false;
|
|
1312
|
+
let handoff;
|
|
1313
|
+
let reviewPath;
|
|
1314
|
+
let since;
|
|
1252
1315
|
let dryRun = false;
|
|
1253
1316
|
let json = false;
|
|
1254
1317
|
let keyName;
|
|
@@ -1314,6 +1377,42 @@ function parseArguments(arguments_) {
|
|
|
1314
1377
|
allowFailedPreview = true;
|
|
1315
1378
|
continue;
|
|
1316
1379
|
}
|
|
1380
|
+
if (verb.handoffOptions && (argument === "--handoff" || argument === "--no-handoff")) {
|
|
1381
|
+
if (handoff !== undefined) {
|
|
1382
|
+
throw usageError(
|
|
1383
|
+
handoff === (argument === "--handoff") ? "cli.duplicate_option" : "cli.handoff_conflict",
|
|
1384
|
+
handoff === (argument === "--handoff")
|
|
1385
|
+
? `${argument} may be supplied only once.`
|
|
1386
|
+
: "--handoff and --no-handoff cannot be used together.",
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
handoff = argument === "--handoff";
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
if ((verb.reviewPath && argument === "--path") || (verb.handoffOptions && argument === "--review-path")) {
|
|
1393
|
+
if (reviewPath !== undefined) {
|
|
1394
|
+
throw usageError("cli.duplicate_option", `${argument} may be supplied only once.`);
|
|
1395
|
+
}
|
|
1396
|
+
// An option-like value would otherwise become the page '/--json' and mint a handoff to a 404.
|
|
1397
|
+
const candidate = rest[index + 1];
|
|
1398
|
+
reviewPath = normalizeStagingReviewPath(typeof candidate === "string" && candidate.startsWith("--") ? "" : candidate);
|
|
1399
|
+
index += 1;
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
if (verb.sinceOption && argument === "--since") {
|
|
1403
|
+
if (since !== undefined) throw usageError("cli.duplicate_option", "--since may be supplied only once.");
|
|
1404
|
+
const candidate = rest[index + 1];
|
|
1405
|
+
if (parseCliVersion(candidate) === undefined) {
|
|
1406
|
+
throw usageError(
|
|
1407
|
+
"changelog.since_invalid",
|
|
1408
|
+
"--since requires a version such as 0.20.0 (the version you upgraded from).",
|
|
1409
|
+
{ field: "since" },
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
since = candidate;
|
|
1413
|
+
index += 1;
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1317
1416
|
if (verb.dryRun && argument === "--dry-run") {
|
|
1318
1417
|
if (dryRun) throw usageError("cli.duplicate_option", "--dry-run may be supplied only once.");
|
|
1319
1418
|
dryRun = true;
|
|
@@ -1394,6 +1493,16 @@ function parseArguments(arguments_) {
|
|
|
1394
1493
|
`${verb.tokens.join(" ")} requires exactly one of --staging or --production.`,
|
|
1395
1494
|
);
|
|
1396
1495
|
}
|
|
1496
|
+
if (verb.handoffOptions && reviewPath !== undefined && handoff !== true) {
|
|
1497
|
+
throw usageError(
|
|
1498
|
+
"cli.review_path_requires_handoff",
|
|
1499
|
+
"--review-path chooses the page a --handoff review URL opens on; pass --handoff with it, or use 'staging review --path'.",
|
|
1500
|
+
{ field: "reviewPath" },
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
if (handoff === true && verb.target && deployTarget === DEPLOY_TARGET_PRODUCTION) {
|
|
1504
|
+
throw usageError("cli.handoff_target", "--handoff applies only to deploy --staging.", { field: "handoff" });
|
|
1505
|
+
}
|
|
1397
1506
|
if (verb.positionals === "fixturePath" && (positionals.length > 1 || (init && positionals.length !== 1))) {
|
|
1398
1507
|
throw usageError(
|
|
1399
1508
|
"validate.fixture_path_invalid",
|
|
@@ -1507,6 +1616,9 @@ function parseArguments(arguments_) {
|
|
|
1507
1616
|
quiet,
|
|
1508
1617
|
deployTarget,
|
|
1509
1618
|
allowFailedPreview,
|
|
1619
|
+
...(handoff === undefined ? {} : { handoff }),
|
|
1620
|
+
...(reviewPath === undefined ? {} : { reviewPath }),
|
|
1621
|
+
...(since === undefined ? {} : { since }),
|
|
1510
1622
|
...(deliveryUrl === undefined ? {} : { deliveryUrl }),
|
|
1511
1623
|
...(propagationWaitSeconds === undefined ? {} : { propagationWaitSeconds }),
|
|
1512
1624
|
...(browser === undefined ? {} : { browser }),
|
|
@@ -1599,6 +1711,9 @@ export async function runCli({
|
|
|
1599
1711
|
configPath: parsed.configPath,
|
|
1600
1712
|
deployTarget: parsed.deployTarget,
|
|
1601
1713
|
allowFailedPreview: parsed.allowFailedPreview,
|
|
1714
|
+
...(parsed.handoff === undefined ? {} : { handoff: parsed.handoff }),
|
|
1715
|
+
...(parsed.reviewPath === undefined ? {} : { reviewPath: parsed.reviewPath }),
|
|
1716
|
+
...(parsed.since === undefined ? {} : { since: parsed.since }),
|
|
1602
1717
|
init: parsed.init,
|
|
1603
1718
|
quiet: parsed.quiet,
|
|
1604
1719
|
dryRun: parsed.dryRun,
|
package/src/constants.js
CHANGED
|
@@ -106,6 +106,12 @@ export const VERB_STAGING_REVIEW = "staging review";
|
|
|
106
106
|
export const VERB_SITES = "sites";
|
|
107
107
|
export const VERB_USE = "use";
|
|
108
108
|
export const VERB_WHOAMI = "whoami";
|
|
109
|
+
export const VERB_CHANGELOG = "changelog";
|
|
110
|
+
|
|
111
|
+
/** The command that lists what changed since a version, for the CLI to name after an upgrade (TR01225). */
|
|
112
|
+
export function changelogCommandSince(version) {
|
|
113
|
+
return `${CLI_BINARY_NAME} ${VERB_CHANGELOG} --since ${version}`;
|
|
114
|
+
}
|
|
109
115
|
export const VERB_PLAN = "plan";
|
|
110
116
|
export const VERB_APPLY = "apply";
|
|
111
117
|
export const VERB_PLACES_SEARCH = "places search";
|
|
@@ -256,6 +262,19 @@ export const LIMITS = Object.freeze({
|
|
|
256
262
|
deploymentMilliseconds: 30 * 60_000,
|
|
257
263
|
previewMilliseconds: 5 * 60_000,
|
|
258
264
|
pollIntervalMilliseconds: 2_000,
|
|
265
|
+
// How long after a staging deployment completes the edge can still serve the
|
|
266
|
+
// previous release (TR01236): up to a minute for Workers KV to propagate the
|
|
267
|
+
// route, plus the Worker's 30-second KV read cache and 30-second isolate cache
|
|
268
|
+
// (docs/operations/published-site-worker-runbook.md, "Route resolution
|
|
269
|
+
// freshness budget"). The wait is longer than the ceiling so one slow read does
|
|
270
|
+
// not turn a propagating release into a false warning.
|
|
271
|
+
stagingPropagationCeilingSeconds: 120,
|
|
272
|
+
stagingServedMilliseconds: 180_000,
|
|
273
|
+
stagingServedPollMilliseconds: 3_000,
|
|
274
|
+
// Two reads in a row, because each read lands on one cached copy of the route.
|
|
275
|
+
stagingServedConfirmations: 2,
|
|
276
|
+
// The review notice sits at the end of the page, so the whole document is read.
|
|
277
|
+
stagingServedPageBytes: 4 * 1024 * 1024,
|
|
259
278
|
requestAttempts: 4,
|
|
260
279
|
uploadAttempts: 4,
|
|
261
280
|
diagnosticScalars: 2_000,
|
package/src/errors.js
CHANGED
|
@@ -130,6 +130,8 @@ export class SiteAuthoringError extends Error {
|
|
|
130
130
|
this.previewDiagnostic = normalizePreviewDiagnostic(options.previewDiagnostic);
|
|
131
131
|
this.field = typeof options.field === "string" ? sanitizeDiagnostic(options.field, "") : undefined;
|
|
132
132
|
this.status = typeof options.status === "string" ? sanitizeDiagnostic(options.status, "") : undefined;
|
|
133
|
+
// The site check rule (shared/site-check-rules.json) this refusal comes from, when it comes from one.
|
|
134
|
+
this.rule = typeof options.rule === "string" && /^[a-z0-9_.]+$/u.test(options.rule) ? options.rule : undefined;
|
|
133
135
|
this.alternatives = Array.isArray(options.alternatives)
|
|
134
136
|
? options.alternatives
|
|
135
137
|
.filter((value) => typeof value === "string")
|
|
@@ -191,6 +193,7 @@ export class SiteAuthoringError extends Error {
|
|
|
191
193
|
...(typeof problem.file === "string" ? { file: sanitizeDiagnostic(problem.file, "") } : {}),
|
|
192
194
|
code: /^[a-z0-9_.-]+$/u.test(problem.code) ? problem.code : "site.failed",
|
|
193
195
|
...(typeof problem.field === "string" ? { field: sanitizeDiagnostic(problem.field, "") } : {}),
|
|
196
|
+
...(typeof problem.rule === "string" && /^[a-z0-9_.]+$/u.test(problem.rule) ? { rule: problem.rule } : {}),
|
|
194
197
|
message: sanitizeDiagnostic(problem.message),
|
|
195
198
|
}));
|
|
196
199
|
this.problemCount = list.length;
|
package/src/footer-contract.js
CHANGED
|
@@ -278,11 +278,13 @@ function richText(value, path, maximumLength, maximumParagraphs, knownPageResour
|
|
|
278
278
|
if (text.length === 0) continue;
|
|
279
279
|
totalLength += text.length;
|
|
280
280
|
totalRuns += 1;
|
|
281
|
+
// A style flag that is off is left out, so a run carries only what it sets and a hand-written
|
|
282
|
+
// file matches what pull writes (TR01232).
|
|
281
283
|
normalizedRuns.push({
|
|
282
284
|
text,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
285
|
+
...(boolean(run.bold, false, `${runPath}.bold`) ? { bold: true } : {}),
|
|
286
|
+
...(boolean(run.italic, false, `${runPath}.italic`) ? { italic: true } : {}),
|
|
287
|
+
...(boolean(run.underline, false, `${runPath}.underline`) ? { underline: true } : {}),
|
|
286
288
|
...(run.link === undefined || run.link === null
|
|
287
289
|
? {}
|
|
288
290
|
: { link: inlineLink(run.link, `${runPath}.link`, knownPageResourceIds, tolerateStoredValues) }),
|
package/src/forms-workspace.js
CHANGED
|
@@ -53,6 +53,28 @@ export async function readWorkspaceForms(workspaceDir, keys, validationOptions =
|
|
|
53
53
|
return forms;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The keys of the form files the workspace holds, read from file names alone (TR01081), or undefined
|
|
58
|
+
* when the folder cannot be read (a name outside the safe grammar, a link, too many files): the
|
|
59
|
+
* answer is only a hint for a refusal, so a workspace that cannot say changes nothing, and
|
|
60
|
+
* `forms validate` reports the folder's problem. A file whose name is not a form key is left out.
|
|
61
|
+
*/
|
|
62
|
+
export async function workspaceFormKeys(workspaceDir) {
|
|
63
|
+
let files;
|
|
64
|
+
try {
|
|
65
|
+
files = await walkWorkspaceFiles(workspaceDir, FORMS_DIRECTORY, [FORM_FILE_EXTENSION]);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error instanceof SiteAuthoringError) return undefined;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
const keys = new Set();
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
const key = file.slice(FORMS_DIRECTORY.length + 1, -FORM_FILE_EXTENSION.length);
|
|
73
|
+
if (!key.includes("/") && isFormKey(key)) keys.add(key);
|
|
74
|
+
}
|
|
75
|
+
return keys;
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
/** A form name as the site compares it: trimmed, ignoring case (TR01216). */
|
|
57
79
|
export function comparableFormName(name) {
|
|
58
80
|
return String(name).trim().toLowerCase();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Generated from shared/heading-outline.ts by scripts/sync-field-validation.mjs. Do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* How a published page numbers its headings (TR00929, TR00946, TR00947).
|
|
4
|
+
*
|
|
5
|
+
* The generator renders a page with this state machine, and the page-structure
|
|
6
|
+
* checks in `page-structure-checks.ts` replay the same machine over a document
|
|
7
|
+
* to tell an author which element claims the page's `<h1>` and which headings
|
|
8
|
+
* the publisher renumbers. One owner keeps what the warning says in step with
|
|
9
|
+
* what ships. The generator's `component-renderer.ts` and the site-authoring
|
|
10
|
+
* package import copies of this file; `node scripts/sync-field-validation.mjs`
|
|
11
|
+
* writes them, so edit only this file.
|
|
12
|
+
*
|
|
13
|
+
* It is dependency-free so every consumer can carry a copy.
|
|
14
|
+
*/
|
|
15
|
+
export function createHeadingOutlineState(options = {}) {
|
|
16
|
+
const titleOpen = options.titleOpen === true;
|
|
17
|
+
const parentLevel = options.parentLevel ?? 1;
|
|
18
|
+
return { level: parentLevel, titleOpen, titleClaimed: !titleOpen, parentLevel, open: [] };
|
|
19
|
+
}
|
|
20
|
+
// Not `Array.prototype.at`: the editor's TypeScript library predates it.
|
|
21
|
+
function lastOpen(state) {
|
|
22
|
+
return state.open[state.open.length - 1];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The level to render an author heading at (TR00946). It closes the open
|
|
26
|
+
* headings at its authored level or deeper, then sits one level below the
|
|
27
|
+
* heading still open, so an h3 straight after the page's h1 becomes an h2,
|
|
28
|
+
* and so do the h3s after it until the author's own h2. A free-form page's
|
|
29
|
+
* first author h1 is its title; any other h1 is demoted.
|
|
30
|
+
*/
|
|
31
|
+
export function nextHeadingLevel(state, authored) {
|
|
32
|
+
if (!state)
|
|
33
|
+
return authored;
|
|
34
|
+
if (authored === 1 && !state.titleClaimed) {
|
|
35
|
+
recordFixedHeading(state, 1);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
while ((lastOpen(state)?.authored ?? 0) >= authored)
|
|
39
|
+
state.open.pop();
|
|
40
|
+
const level = Math.min(6, (lastOpen(state)?.level ?? state.parentLevel) + 1);
|
|
41
|
+
state.open.push({ authored, level });
|
|
42
|
+
state.level = level;
|
|
43
|
+
return level;
|
|
44
|
+
}
|
|
45
|
+
/** A heading whose level the page fixes, such as a hero title or CTA heading. */
|
|
46
|
+
export function recordFixedHeading(state, level) {
|
|
47
|
+
if (level === 1)
|
|
48
|
+
state.titleClaimed = true;
|
|
49
|
+
while ((lastOpen(state)?.level ?? 0) >= level)
|
|
50
|
+
state.open.pop();
|
|
51
|
+
state.open.push({ authored: level, level });
|
|
52
|
+
state.level = level;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether an Image Banner is the page's title: it opens a free-form page,
|
|
56
|
+
* shows an image, and carries overlay text. Without an image the overlay is
|
|
57
|
+
* not drawn, so the banner carries no heading.
|
|
58
|
+
*/
|
|
59
|
+
export function bannerOpensPage(state, data) {
|
|
60
|
+
return state?.titleOpen === true
|
|
61
|
+
&& Boolean(data.image?.src)
|
|
62
|
+
&& typeof data.overlayText === "string"
|
|
63
|
+
&& data.overlayText.trim() !== "";
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The level of the heading a hero or CTA component fixes on the page, or
|
|
67
|
+
* undefined when the component carries none. A hero title is always an h1;
|
|
68
|
+
* a CTA heading is an h2.
|
|
69
|
+
*/
|
|
70
|
+
export function componentFixedHeadingLevel(type, data) {
|
|
71
|
+
if (type === "hero-section" && data.title)
|
|
72
|
+
return 1;
|
|
73
|
+
if (type === "cta" && data.heading)
|
|
74
|
+
return 2;
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
package/src/index.js
CHANGED
package/src/output.js
CHANGED
|
@@ -3,12 +3,14 @@ import { constants as fsConstants } from "node:fs";
|
|
|
3
3
|
import { lstat, open } from "node:fs/promises";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
changelogCommandSince,
|
|
6
7
|
CLI_BINARY_NAME,
|
|
7
8
|
CLI_NAME,
|
|
8
9
|
CLI_VERSION,
|
|
9
10
|
GITHUB_OUTPUT_KEY_PREFIX,
|
|
10
11
|
GITHUB_OUTPUT_RESULT_KEY,
|
|
11
12
|
LIMITS,
|
|
13
|
+
REFUSAL_CLI_OUTDATED,
|
|
12
14
|
REFUSAL_UNCLASSIFIED,
|
|
13
15
|
RESULT_SCHEMA_VERSION,
|
|
14
16
|
} from "./constants.js";
|
|
@@ -37,6 +39,7 @@ export function failureResult(error) {
|
|
|
37
39
|
error: { code: error.code },
|
|
38
40
|
};
|
|
39
41
|
if (error.field) result.error.field = error.field;
|
|
42
|
+
if (error.rule) result.error.rule = error.rule;
|
|
40
43
|
if (error.details?.length) result.error.details = error.details;
|
|
41
44
|
if (error.status) result.error.status = error.status;
|
|
42
45
|
if (Array.isArray(error.alternatives) && error.alternatives.length > 0) {
|
|
@@ -67,6 +70,12 @@ export function failureResult(error) {
|
|
|
67
70
|
if (previewRecovery) result.error.preview = previewRecovery;
|
|
68
71
|
const refusal = typeof error.refusalKind === "function" ? error.refusalKind() : undefined;
|
|
69
72
|
if (typeof refusal === "string" && refusal !== REFUSAL_UNCLASSIFIED) result.error.refusal = refusal;
|
|
73
|
+
if (refusal === REFUSAL_CLI_OUTDATED) {
|
|
74
|
+
const latestVersion = typeof error.latestCliVersion === "function" ? error.latestCliVersion() : error.latestVersion;
|
|
75
|
+
if (typeof latestVersion === "string") result.error.latestVersion = latestVersion;
|
|
76
|
+
// What to run once the upgrade is done, so the agent need not diff the help topics.
|
|
77
|
+
result.error.changelogCommand = changelogCommandSince(CLI_VERSION);
|
|
78
|
+
}
|
|
70
79
|
return result;
|
|
71
80
|
}
|
|
72
81
|
|
package/src/page-metadata.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SiteAuthoringError } from "./errors.js";
|
|
2
|
+
import { DESCRIPTION_WARN_LENGTH } from "./page-structure-checks.js";
|
|
2
3
|
import { boundedByBytes } from "./session.js";
|
|
3
4
|
import { displayPagePath } from "./workspace.js";
|
|
4
5
|
|
|
@@ -7,7 +8,7 @@ import { displayPagePath } from "./workspace.js";
|
|
|
7
8
|
* 160 characters search engines cut it off, so it is warned about; past 1000 it
|
|
8
9
|
* is refused, here and by the API (TR01194).
|
|
9
10
|
*/
|
|
10
|
-
export
|
|
11
|
+
export { DESCRIPTION_WARN_LENGTH };
|
|
11
12
|
export const DESCRIPTION_MAXIMUM_LENGTH = 1000;
|
|
12
13
|
export const PAGE_DESCRIPTION_LONG = "pages.description_long";
|
|
13
14
|
|