@taprootio/site-authoring 0.26.0 → 0.27.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/package.json +1 -1
- package/src/changelog.json +25 -0
- package/src/field-schema.json +11 -0
- package/src/field-validation.js +17 -1
- package/src/forms-contract.js +15 -1
- package/src/reference-help.js +9 -2
- package/src/setting-reference.js +18 -0
- package/src/settings-catalog.js +4 -1
- package/src/site-checks.js +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"description": "Taproot site authoring CLI: sign in once in the browser, then pull, validate, push, preview, approve, and deploy a site from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|
package/src/changelog.json
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"entries": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.27.1",
|
|
5
|
+
"note": "The site checks refuse a page or redirect source named like a search-engine key file, which the platform now publishes at the site root.",
|
|
6
|
+
"behavior": [
|
|
7
|
+
"A redirect source whose first segment is 32 lowercase hexadecimal characters followed by .txt (any capitals, with or without a path beneath it) is refused with redirect.source_generated_file, like sitemap.xml and robots.txt. Taproot writes a file with that name at the root of a site that tells search engines about its releases, so nothing else can sit there. No command changed.",
|
|
8
|
+
"The help reference version is 45."
|
|
9
|
+
],
|
|
10
|
+
"help": [
|
|
11
|
+
"redirects"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.27.0",
|
|
16
|
+
"note": "Embedded forms inherit configurable background, border and shadow defaults from the site, with per-form overrides.",
|
|
17
|
+
"behavior": [
|
|
18
|
+
"Form definitions accept sparse presentation.background, presentation.border and presentation.shadow boolean overrides. Omitted values inherit the site; explicit false disables the decoration. An empty presentation object is omitted by the form editor.",
|
|
19
|
+
"Site publishing preferences expose formBoxBackground, formBoxBorder and formBoxShadow. Background and shadow default on; border defaults off. Stage publishing preferences and the form version to apply appearance changes.",
|
|
20
|
+
"Forms pull and push preserve appearance overrides, including explicit false.",
|
|
21
|
+
"The help reference version is 44."
|
|
22
|
+
],
|
|
23
|
+
"help": [
|
|
24
|
+
"forms",
|
|
25
|
+
"settings"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
3
28
|
{
|
|
4
29
|
"version": "0.26.0",
|
|
5
30
|
"note": "marketing pull and marketing push keep a site's campaigns and placements as files, and link build prints a tracked address, so an agent can run a site's attribution over the CLI.",
|
package/src/field-schema.json
CHANGED
|
@@ -22,6 +22,17 @@
|
|
|
22
22
|
"type": "string",
|
|
23
23
|
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
|
24
24
|
},
|
|
25
|
+
"presentation": {
|
|
26
|
+
"description": "Sparse embedded-form appearance choices. Omitted properties inherit the site's staged defaults; explicit false disables a choice. Omit the object when all choices inherit.",
|
|
27
|
+
"type": "object",
|
|
28
|
+
"minProperties": 1,
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"properties": {
|
|
31
|
+
"background": { "type": "boolean" },
|
|
32
|
+
"border": { "type": "boolean" },
|
|
33
|
+
"shadow": { "type": "boolean" }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
25
36
|
"submit_label": {
|
|
26
37
|
"description": "The text on the submit button. Omit it for Taproot's default. Must contain more than whitespace; the validator also rejects a blank label, which a schema cannot express.",
|
|
27
38
|
"type": "string",
|
package/src/field-validation.js
CHANGED
|
@@ -106,7 +106,7 @@ export function contactFieldId(definition) {
|
|
|
106
106
|
const emails = definition.fields.filter((field) => field.type === "email");
|
|
107
107
|
return emails.length === 1 ? emails[0].id : undefined;
|
|
108
108
|
}
|
|
109
|
-
const DEFINITION_KEYS = ["fields", "contact_field", "submit_label", "after_submit", "newsletter_audience_id"];
|
|
109
|
+
const DEFINITION_KEYS = ["fields", "contact_field", "submit_label", "after_submit", "newsletter_audience_id", "presentation"];
|
|
110
110
|
const BASE_FIELD_KEYS = ["id", "type", "label", "hint"];
|
|
111
111
|
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
112
112
|
const isInteger = (value) => typeof value === "number" && Number.isInteger(value);
|
|
@@ -427,6 +427,22 @@ export function validateDefinition(definition) {
|
|
|
427
427
|
}
|
|
428
428
|
/** The button text and the after-submit choice, which sit beside the fields in a definition. */
|
|
429
429
|
function validateFormSettings(definition, errors) {
|
|
430
|
+
if ("presentation" in definition) {
|
|
431
|
+
const presentation = definition.presentation;
|
|
432
|
+
if (!isPlainObject(presentation) || Object.keys(presentation).length === 0) {
|
|
433
|
+
errors.push({ path: "presentation", code: "invalid_presentation" });
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
for (const key of sortedKeys(presentation)) {
|
|
437
|
+
if (key !== "background" && key !== "border" && key !== "shadow") {
|
|
438
|
+
errors.push({ path: `presentation.${key}`, code: "unknown_property" });
|
|
439
|
+
}
|
|
440
|
+
else if (typeof presentation[key] !== "boolean") {
|
|
441
|
+
errors.push({ path: `presentation.${key}`, code: "invalid_presentation_value" });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
430
446
|
const audience = definition.newsletter_audience_id;
|
|
431
447
|
if (audience !== undefined && (typeof audience !== "string" || !isResourceId(audience))) {
|
|
432
448
|
errors.push({ path: "newsletter_audience_id", code: "invalid_newsletter_audience" });
|
package/src/forms-contract.js
CHANGED
|
@@ -6,7 +6,7 @@ import { contactFieldId, validateDefinition } from "./field-validation.js";
|
|
|
6
6
|
*
|
|
7
7
|
* A form is one JSON file, `forms/<key>.json`, holding what an author decides:
|
|
8
8
|
* its key, name, sink, retention, owner-notification preference and definition. The definition holds the fields
|
|
9
|
-
* and the form's own settings (`contact_field`, `submit_label`, `after_submit`),
|
|
9
|
+
* and the form's own settings (`contact_field`, `submit_label`, `after_submit`, `presentation`),
|
|
10
10
|
* exactly as the site stores it, so one validator reads one shape. The
|
|
11
11
|
* definition's own rules belong to `shared/field-validation.ts` and are run
|
|
12
12
|
* here through the generated copy, so this file never restates them. The one
|
|
@@ -289,6 +289,20 @@ export function projectFormForWorkspace(form, pathById) {
|
|
|
289
289
|
if (form.definition.after_submit !== undefined) {
|
|
290
290
|
definition.after_submit = canonicalAfterSubmit(form.definition.after_submit, pathById);
|
|
291
291
|
}
|
|
292
|
+
if (form.definition.newsletter_audience_id !== undefined) {
|
|
293
|
+
definition.newsletter_audience_id = form.definition.newsletter_audience_id;
|
|
294
|
+
}
|
|
295
|
+
if (form.definition.presentation !== undefined) {
|
|
296
|
+
const presentation = {};
|
|
297
|
+
for (const name of ["background", "border", "shadow", ...Object.keys(form.definition.presentation).sort()]) {
|
|
298
|
+
if (form.definition.presentation[name] !== undefined) presentation[name] = form.definition.presentation[name];
|
|
299
|
+
}
|
|
300
|
+
definition.presentation = presentation;
|
|
301
|
+
}
|
|
302
|
+
// Preserve future properties on pull so an older workspace cannot silently erase them.
|
|
303
|
+
for (const name of Object.keys(form.definition).sort()) {
|
|
304
|
+
if (name !== "contact_field" && definition[name] === undefined) definition[name] = form.definition[name];
|
|
305
|
+
}
|
|
292
306
|
return {
|
|
293
307
|
key: form.key,
|
|
294
308
|
...projectFormSettings(form),
|
package/src/reference-help.js
CHANGED
|
@@ -95,7 +95,7 @@ import {
|
|
|
95
95
|
|
|
96
96
|
export { getAppearanceReference, getFooterReference, getSettingsReference, getThemeReference };
|
|
97
97
|
|
|
98
|
-
export const REFERENCE_VERSION =
|
|
98
|
+
export const REFERENCE_VERSION = 45;
|
|
99
99
|
|
|
100
100
|
// The date-limit examples `help forms` prints; tests run them through the form validator.
|
|
101
101
|
export const FORM_DATE_WINDOW_FIELD = Object.freeze({
|
|
@@ -786,7 +786,9 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
786
786
|
+ "check does not see may belong to a release the edge has not switched to yet; deploy --staging waits for "
|
|
787
787
|
+ "staging to serve the new release and reports stagingServed.",
|
|
788
788
|
"A source cannot name a file the site generates: '/index.html' or any page's own 'index.html', '/404.html', "
|
|
789
|
-
+ "'/sitemap.xml', '/robots.txt', '/favicon.ico',
|
|
789
|
+
+ "'/sitemap.xml', '/robots.txt', '/favicon.ico', a path under the sitemap or robots names, or the "
|
|
790
|
+
+ "search-engine key file Taproot publishes when a site tells search engines about its releases (32 "
|
|
791
|
+
+ "lowercase hexadecimal characters followed by '.txt', in any capitals). Nothing under "
|
|
790
792
|
+ "'/taproot/<version>/' (for example '/taproot/5.0.62/manifest.json') can be a source either: the platform "
|
|
791
793
|
+ "serves the published-site runtime there before any redirect. The platform "
|
|
792
794
|
+ "already redirects every '…/index.html' address to its directory, so no entry is needed for one. A source "
|
|
@@ -883,6 +885,11 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
883
885
|
+ "the visitor saw it, so a response shows what each box agreed to.",
|
|
884
886
|
"Editing a definition appends a version on the site; versions are never changed. A page keeps the form "
|
|
885
887
|
+ "version it was published with until the next deploy, and a rollback restores the form it shipped with.",
|
|
888
|
+
"definition.presentation optionally overrides the site's form appearance: background, border and shadow are "
|
|
889
|
+
+ "optional booleans. Omit a property to inherit the corresponding site preference (formBoxBackground, "
|
|
890
|
+
+ "formBoxBorder, formBoxShadow); false turns that choice off. Omit presentation when all three inherit: an empty "
|
|
891
|
+
+ "object is refused. Site defaults are background on, border off and shadow on. Background can be off while border "
|
|
892
|
+
+ "and shadow remain on. The release freezes both the form choices and the site defaults.",
|
|
886
893
|
"Both verbs write the file in one canonical key order, so a pulled file pushed back unchanged reports "
|
|
887
894
|
+ "'unchanged' and leaves the bytes as they were. Write your own files in that order and a push rewrites "
|
|
888
895
|
+ "nothing you did not change.",
|
package/src/setting-reference.js
CHANGED
|
@@ -272,6 +272,24 @@ const PREFERENCES_ENTRIES = {
|
|
|
272
272
|
+ "a deployment.",
|
|
273
273
|
false,
|
|
274
274
|
),
|
|
275
|
+
formBoxBackground: entry(
|
|
276
|
+
`${FEATURES} > Form appearance > Background`,
|
|
277
|
+
"Whether embedded forms have a box background by default. A form's presentation.background overrides this choice.",
|
|
278
|
+
"Embedded forms, including validation and confirmation. A change applies after deployment.",
|
|
279
|
+
true,
|
|
280
|
+
),
|
|
281
|
+
formBoxBorder: entry(
|
|
282
|
+
`${FEATURES} > Form appearance > Border`,
|
|
283
|
+
"Whether embedded forms have a border by default. A form's presentation.border overrides this choice.",
|
|
284
|
+
"Embedded forms. Independent of background and shadow; applies after deployment.",
|
|
285
|
+
false,
|
|
286
|
+
),
|
|
287
|
+
formBoxShadow: entry(
|
|
288
|
+
`${FEATURES} > Form appearance > Shadow`,
|
|
289
|
+
"Whether embedded forms have a shadow by default. A form's presentation.shadow overrides this choice.",
|
|
290
|
+
"Embedded forms. Independent of background and border; applies after deployment.",
|
|
291
|
+
true,
|
|
292
|
+
),
|
|
275
293
|
tiptapImageMaxHeightVh: entry(
|
|
276
294
|
`${FEATURES} > Image height`,
|
|
277
295
|
"The tallest an image in a page body may be, as a percent of the screen height (45, 60, 75 or 90).",
|
package/src/settings-catalog.js
CHANGED
|
@@ -130,6 +130,9 @@ export const SETTINGS_GROUPS = Object.freeze([
|
|
|
130
130
|
// Asks readers before the visit cookie is set (TR01263). Off is what an omitted value means, so a snapshot of
|
|
131
131
|
// an untouched site reads the same as the site behaves.
|
|
132
132
|
{ name: "analyticsConsentPrompt", wireType: "boolean" },
|
|
133
|
+
{ name: "formBoxBackground", wireType: "boolean", zero: true },
|
|
134
|
+
{ name: "formBoxBorder", wireType: "boolean" },
|
|
135
|
+
{ name: "formBoxShadow", wireType: "boolean", zero: true },
|
|
133
136
|
{ name: "tiptapImageMaxHeightVh", wireType: "int" },
|
|
134
137
|
{ name: "tiptapImageMaxImagesPerRow", wireType: "int" },
|
|
135
138
|
{ name: "tiptapImageBorderWidth", wireType: "int" },
|
|
@@ -146,7 +149,7 @@ export const CANDIDATE_SELECTABLE_SETTINGS_TYPES = Object.freeze(
|
|
|
146
149
|
);
|
|
147
150
|
|
|
148
151
|
function zeroValue(field) {
|
|
149
|
-
if (field.wireType === "boolean") return false;
|
|
152
|
+
if (field.wireType === "boolean") return field.zero ?? false;
|
|
150
153
|
if (field.wireType === "int" || field.wireType === "double") return 0;
|
|
151
154
|
if (field.wireType === "theme" || field.wireType === "document") return field.zero ?? {};
|
|
152
155
|
return field.zero ?? "";
|
package/src/site-checks.js
CHANGED
|
@@ -280,6 +280,8 @@ export function normalizeRedirectPath(value) {
|
|
|
280
280
|
return trimmed === "" ? "/" : trimmed;
|
|
281
281
|
}
|
|
282
282
|
const GENERATED_ROOT_FILES = ["404.html", "sitemap.xml", "robots.txt", "favicon.ico"];
|
|
283
|
+
// The IndexNow key file (TR01304) is named by a pattern, so it cannot join the list above.
|
|
284
|
+
const INDEX_NOW_KEY_FILE = /^[a-f0-9]{32}\.txt$/u;
|
|
283
285
|
const INTEGRATION_DATA_ROOT = "integrations";
|
|
284
286
|
const MAX_MARKER_SEGMENT_BYTES = 200;
|
|
285
287
|
const MAX_OUTPUT_PATH_LENGTH = 1_024;
|
|
@@ -317,8 +319,9 @@ export function redirectSourceRule(edgePath) {
|
|
|
317
319
|
return refuse(RULE_REDIRECT_SOURCE_INTEGRATION_DATA);
|
|
318
320
|
if (isRuntimeMirrorPath(decoded))
|
|
319
321
|
return refuse(RULE_REDIRECT_SOURCE_RUNTIME_MIRROR);
|
|
320
|
-
if (GENERATED_ROOT_FILES.includes(first.toLowerCase()))
|
|
322
|
+
if (GENERATED_ROOT_FILES.includes(first.toLowerCase()) || INDEX_NOW_KEY_FILE.test(first.toLowerCase())) {
|
|
321
323
|
return refuse(RULE_REDIRECT_SOURCE_GENERATED_FILE);
|
|
324
|
+
}
|
|
322
325
|
if (!isStorable(decoded))
|
|
323
326
|
return refuse(RULE_REDIRECT_SOURCE_NOT_STORABLE);
|
|
324
327
|
return undefined;
|