@taprootio/site-authoring 0.1.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/LICENSE +15 -0
- package/README.md +239 -0
- package/bin/taproot-site.js +5 -0
- package/package.json +40 -0
- package/src/api.js +1507 -0
- package/src/appearance-contract.js +311 -0
- package/src/atomic-file.js +136 -0
- package/src/bounded-file.js +59 -0
- package/src/capabilities.js +29 -0
- package/src/cli.js +872 -0
- package/src/config.js +412 -0
- package/src/constants.js +173 -0
- package/src/content/components.js +870 -0
- package/src/content/free-form-section-registry.json +524 -0
- package/src/content/free-form-sections.js +582 -0
- package/src/content/index.js +29 -0
- package/src/content/inline-facts.js +97 -0
- package/src/content/markdown.js +1625 -0
- package/src/content/tiptap-prosemirror.ts +1266 -0
- package/src/content/validate-document.js +1079 -0
- package/src/content/vocabulary.js +397 -0
- package/src/credentials.js +544 -0
- package/src/errors.js +133 -0
- package/src/footer-contract.js +687 -0
- package/src/footer-draft-hash.js +203 -0
- package/src/footer-workspace.js +136 -0
- package/src/image-metadata.js +169 -0
- package/src/index.js +16 -0
- package/src/json-guard.js +111 -0
- package/src/output.js +157 -0
- package/src/presentation-reference.js +600 -0
- package/src/reference-help.js +927 -0
- package/src/session.js +316 -0
- package/src/settings-catalog.js +187 -0
- package/src/settings.js +157 -0
- package/src/theme-validation.js +344 -0
- package/src/transport.js +777 -0
- package/src/verbs/approve.js +178 -0
- package/src/verbs/deploy.js +327 -0
- package/src/verbs/env.js +89 -0
- package/src/verbs/footer-push.js +115 -0
- package/src/verbs/index.js +125 -0
- package/src/verbs/login.js +529 -0
- package/src/verbs/logout.js +39 -0
- package/src/verbs/media-upload.js +319 -0
- package/src/verbs/nav-push.js +333 -0
- package/src/verbs/pages-push.js +990 -0
- package/src/verbs/preview-page.js +107 -0
- package/src/verbs/preview-revoke.js +56 -0
- package/src/verbs/pull.js +727 -0
- package/src/verbs/sites.js +57 -0
- package/src/verbs/status.js +94 -0
- package/src/verbs/theme-push.js +197 -0
- package/src/verbs/use.js +129 -0
- package/src/verbs/validate.js +477 -0
- package/src/verbs/whoami.js +108 -0
- package/src/workspace.js +1085 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { saveSiteFooterSettings, withRefusalGuidance } from "../api.js";
|
|
2
|
+
import { VERB_FOOTER_PUSH } from "../constants.js";
|
|
3
|
+
import { SiteAuthoringError } from "../errors.js";
|
|
4
|
+
import { projectFooterSettingsForWorkspace, validateFooterDocument } from "../footer-contract.js";
|
|
5
|
+
import { computeFooterDraftHash } from "../footer-draft-hash.js";
|
|
6
|
+
import { advanceFooterManifest, FOOTER_SETTINGS_FILE, readFooterWorkspaceContext } from "../footer-workspace.js";
|
|
7
|
+
import { openSession, successResult } from "../session.js";
|
|
8
|
+
import { SETTINGS_TYPE_SITE_PUBLISHING_PREFERENCES } from "../settings-catalog.js";
|
|
9
|
+
import { ApiError } from "../transport.js";
|
|
10
|
+
import { readWorkspaceJson, WORKSPACE_LIMITS, writeWorkspaceJson } from "../workspace.js";
|
|
11
|
+
|
|
12
|
+
function isPlainObject(value) {
|
|
13
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function requireSettingsDocument(document_, siteId, expectedEntityId = siteId) {
|
|
17
|
+
if (!isPlainObject(document_) || !isPlainObject(document_.settings)) {
|
|
18
|
+
throw new SiteAuthoringError(
|
|
19
|
+
"footer.settings_invalid",
|
|
20
|
+
`${FOOTER_SETTINGS_FILE} must be the settings document written by pull.`,
|
|
21
|
+
{ field: FOOTER_SETTINGS_FILE },
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (
|
|
25
|
+
document_.entityId !== expectedEntityId
|
|
26
|
+
|| document_.settingsType !== SETTINGS_TYPE_SITE_PUBLISHING_PREFERENCES
|
|
27
|
+
) {
|
|
28
|
+
throw new SiteAuthoringError(
|
|
29
|
+
"footer.settings_site_mismatch",
|
|
30
|
+
`${FOOTER_SETTINGS_FILE} is not bound to this site. Run 'taproot-site pull' again.`,
|
|
31
|
+
{ field: FOOTER_SETTINGS_FILE },
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return document_;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The complete local validation phase shared by live push and offline fixtures. */
|
|
38
|
+
export function validateFooterWorkspaceDocument(document_, siteId, options = {}) {
|
|
39
|
+
const { expectedEntityId = siteId, ...validationOptions } = options;
|
|
40
|
+
const settingsDocument = requireSettingsDocument(document_, siteId, expectedEntityId);
|
|
41
|
+
return {
|
|
42
|
+
document: settingsDocument,
|
|
43
|
+
footerSettings: validateFooterDocument(settingsDocument.settings.footerSettings, validationOptions),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function translateConcurrency(error) {
|
|
48
|
+
if (error instanceof ApiError && error.hasField("ExpectedFooterDraftHash")) {
|
|
49
|
+
return new SiteAuthoringError(
|
|
50
|
+
"footer.concurrent_modification",
|
|
51
|
+
"The footer changed after this workspace was pulled. Run 'taproot-site pull', reconcile the footer document, and retry.",
|
|
52
|
+
{ field: "expectedFooterDraftHash", status: error.status },
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function footerPush(invocation) {
|
|
59
|
+
const { client, config, siteId, onProgress } = await openSession(invocation);
|
|
60
|
+
const context = await readFooterWorkspaceContext(config.workspaceDir, siteId);
|
|
61
|
+
const { document: document_, footerSettings } = validateFooterWorkspaceDocument(
|
|
62
|
+
await readWorkspaceJson(config.workspaceDir, FOOTER_SETTINGS_FILE, WORKSPACE_LIMITS.settingsBytes),
|
|
63
|
+
siteId,
|
|
64
|
+
{
|
|
65
|
+
knownPageResourceIds: context.knownPageResourceIds,
|
|
66
|
+
knownImageIds: context.knownImageIds,
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
onProgress("Validated the complete footer document, stable page targets, and site-owned image references locally.");
|
|
70
|
+
|
|
71
|
+
let response;
|
|
72
|
+
try {
|
|
73
|
+
response = await withRefusalGuidance(onProgress, "footer push", async () =>
|
|
74
|
+
await saveSiteFooterSettings(
|
|
75
|
+
client,
|
|
76
|
+
siteId,
|
|
77
|
+
footerSettings,
|
|
78
|
+
context.footer.expectedDraftHash,
|
|
79
|
+
));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw translateConcurrency(error);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!isPlainObject(response.footerSettings)) {
|
|
85
|
+
throw new SiteAuthoringError(
|
|
86
|
+
"api.footer_settings_contract",
|
|
87
|
+
"Taproot returned no normalized footer document after saving.",
|
|
88
|
+
{ field: "footerSettings" },
|
|
89
|
+
).withCompletedWrites(["footerSettings"]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let saved;
|
|
93
|
+
let footerDraftHash;
|
|
94
|
+
try {
|
|
95
|
+
saved = projectFooterSettingsForWorkspace(response.footerSettings);
|
|
96
|
+
footerDraftHash = computeFooterDraftHash(saved);
|
|
97
|
+
const nextDocument = structuredClone(document_);
|
|
98
|
+
nextDocument.settings.footerSettings = saved;
|
|
99
|
+
// The authorable document lands before its token. If either local write
|
|
100
|
+
// fails, a stale token prevents the next push from overwriting the saved
|
|
101
|
+
// remote footer and the completed-write report tells the caller to pull.
|
|
102
|
+
await writeWorkspaceJson(config.workspaceDir, FOOTER_SETTINGS_FILE, nextDocument);
|
|
103
|
+
await advanceFooterManifest(config.workspaceDir, siteId, saved, footerDraftHash);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof SiteAuthoringError) throw error.withCompletedWrites(["footerSettings"]);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
onProgress("Saved the complete footer and refreshed its local draft hash.");
|
|
109
|
+
|
|
110
|
+
return successResult(VERB_FOOTER_PUSH, siteId, {
|
|
111
|
+
footerFile: FOOTER_SETTINGS_FILE,
|
|
112
|
+
footerDraftHash,
|
|
113
|
+
written: { items: ["footerSettings"], count: 1 },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VERB_APPROVE,
|
|
3
|
+
VERB_DEPLOY,
|
|
4
|
+
VERB_ENV,
|
|
5
|
+
VERB_FOOTER_PUSH,
|
|
6
|
+
VERB_LOGIN,
|
|
7
|
+
VERB_LOGOUT,
|
|
8
|
+
VERB_MEDIA_UPLOAD,
|
|
9
|
+
VERB_NAV_PUSH,
|
|
10
|
+
VERB_PAGES_PUSH,
|
|
11
|
+
VERB_PULL,
|
|
12
|
+
VERB_PREVIEW_PAGE,
|
|
13
|
+
VERB_PREVIEW_REVOKE,
|
|
14
|
+
VERB_SITES,
|
|
15
|
+
VERB_STATUS,
|
|
16
|
+
VERB_THEME_PUSH,
|
|
17
|
+
VERB_USE,
|
|
18
|
+
VERB_VALIDATE,
|
|
19
|
+
VERB_WHOAMI,
|
|
20
|
+
} from "../constants.js";
|
|
21
|
+
import { approve } from "./approve.js";
|
|
22
|
+
import { deploy } from "./deploy.js";
|
|
23
|
+
import { env } from "./env.js";
|
|
24
|
+
import { footerPush } from "./footer-push.js";
|
|
25
|
+
import { login } from "./login.js";
|
|
26
|
+
import { logout } from "./logout.js";
|
|
27
|
+
import { mediaUpload } from "./media-upload.js";
|
|
28
|
+
import { navPush } from "./nav-push.js";
|
|
29
|
+
import { pagesPush } from "./pages-push.js";
|
|
30
|
+
import { pull } from "./pull.js";
|
|
31
|
+
import { previewPage } from "./preview-page.js";
|
|
32
|
+
import { previewRevoke } from "./preview-revoke.js";
|
|
33
|
+
import { sites } from "./sites.js";
|
|
34
|
+
import { status } from "./status.js";
|
|
35
|
+
import { themePush } from "./theme-push.js";
|
|
36
|
+
import { use } from "./use.js";
|
|
37
|
+
import { validateFixture } from "./validate.js";
|
|
38
|
+
import { whoami } from "./whoami.js";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The verb seam.
|
|
42
|
+
*
|
|
43
|
+
* `cli.js` parses the command line, decides nothing about what a verb means,
|
|
44
|
+
* and calls exactly one handler from this table with a single frozen
|
|
45
|
+
* invocation object:
|
|
46
|
+
*
|
|
47
|
+
* ```
|
|
48
|
+
* {
|
|
49
|
+
* verb, // the canonical verb name, e.g. "pages push"
|
|
50
|
+
* cwd, // directory configuration discovery starts from
|
|
51
|
+
* environment, // the process environment: TAPROOT_SITE_KEY, and the HOME /
|
|
52
|
+
* // XDG_CONFIG_HOME that locate the stored credential
|
|
53
|
+
* configPath, // an explicit --config path, or undefined
|
|
54
|
+
* deployTarget, // "staging" | "production" for `deploy`, else undefined
|
|
55
|
+
* quiet, // --quiet; suppresses progress, never the JSON result
|
|
56
|
+
* allowRawHtml, // --allow-raw-html; `pages push` only, false everywhere else
|
|
57
|
+
* keyName, // --name; `login` only, undefined everywhere else
|
|
58
|
+
* onProgress, // human progress sink (stderr, or a no-op when quiet)
|
|
59
|
+
* fetch, // injectable fetch, for tests
|
|
60
|
+
* signal, // optional AbortSignal
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* Two verbs are credential-free but not config-free. `login` and `logout`
|
|
65
|
+
* manage the stored credential rather than using one, so they read the
|
|
66
|
+
* configuration (for the site id and the reviewed API origin) and never require
|
|
67
|
+
* `TAPROOT_SITE_KEY`.
|
|
68
|
+
*
|
|
69
|
+
* Verbs that take positional arguments receive them under their own
|
|
70
|
+
* seam name, and only when some were given — a bare invocation keeps each
|
|
71
|
+
* verb's documented default:
|
|
72
|
+
*
|
|
73
|
+
* ```
|
|
74
|
+
* {
|
|
75
|
+
* fixturePath, // validate: one offline authoring-fixture directory
|
|
76
|
+
* paths, // media upload: files and/or directories to upload;
|
|
77
|
+
* // with none, the workspace's media/ directory is walked
|
|
78
|
+
* pagePaths, // approve: narrow to these page paths; with none, every
|
|
79
|
+
* // draft the workspace manifest tracks is staged
|
|
80
|
+
* pageId, // preview page: the one persisted draft UUID to render
|
|
81
|
+
* previewIds, // preview revoke: [page UUID, snapshot UUID]
|
|
82
|
+
* }
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* A handler resolves with the machine-readable result object that reaches
|
|
86
|
+
* stdout, or throws a `SiteAuthoringError`.
|
|
87
|
+
*
|
|
88
|
+
* Handlers also accept a few optional keys `cli.js` does not set. They are the
|
|
89
|
+
* seams tests inject through, and the shape a programmatic caller fills in:
|
|
90
|
+
*
|
|
91
|
+
* ```
|
|
92
|
+
* {
|
|
93
|
+
* client, // a prebuilt SiteApiClient, bypassing construction
|
|
94
|
+
* sleep, now, // deterministic clocks for the bounded polls
|
|
95
|
+
* content, // the src/content/ module (pages push)
|
|
96
|
+
* stagedPageIds, // deploy --staging: an explicit candidate; refused
|
|
97
|
+
* selectedSettingsTypes, // outright for --production, which promotes a
|
|
98
|
+
* includeNavigation, // staging deployment and nothing else
|
|
99
|
+
* stagingDeploymentId, // deploy --production: promote this one
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* Every optional key defaults to the safe reading when absent: no raw HTML, the
|
|
104
|
+
* workspace's own directories, and the whole approved candidate.
|
|
105
|
+
*/
|
|
106
|
+
export const VERB_HANDLERS = Object.freeze({
|
|
107
|
+
[VERB_VALIDATE]: validateFixture,
|
|
108
|
+
[VERB_LOGIN]: login,
|
|
109
|
+
[VERB_LOGOUT]: logout,
|
|
110
|
+
[VERB_PULL]: pull,
|
|
111
|
+
[VERB_PAGES_PUSH]: pagesPush,
|
|
112
|
+
[VERB_NAV_PUSH]: navPush,
|
|
113
|
+
[VERB_THEME_PUSH]: themePush,
|
|
114
|
+
[VERB_FOOTER_PUSH]: footerPush,
|
|
115
|
+
[VERB_MEDIA_UPLOAD]: mediaUpload,
|
|
116
|
+
[VERB_APPROVE]: approve,
|
|
117
|
+
[VERB_DEPLOY]: deploy,
|
|
118
|
+
[VERB_PREVIEW_PAGE]: previewPage,
|
|
119
|
+
[VERB_PREVIEW_REVOKE]: previewRevoke,
|
|
120
|
+
[VERB_SITES]: sites,
|
|
121
|
+
[VERB_USE]: use,
|
|
122
|
+
[VERB_WHOAMI]: whoami,
|
|
123
|
+
[VERB_ENV]: env,
|
|
124
|
+
[VERB_STATUS]: status,
|
|
125
|
+
});
|