@taprootio/site-authoring 0.14.0 → 0.15.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.
Files changed (39) hide show
  1. package/README.md +62 -10
  2. package/examples/riverbend-wellness/.taproot-site-media.json +23 -0
  3. package/examples/riverbend-wellness/pages/index.md +1 -17
  4. package/package.json +2 -2
  5. package/src/api.js +59 -1
  6. package/src/cli.js +147 -22
  7. package/src/constants.js +6 -0
  8. package/src/content/components.js +64 -6
  9. package/src/content/free-form-sections.js +21 -4
  10. package/src/content/markdown.js +161 -26
  11. package/src/content/tiptap-prosemirror.ts +66 -8
  12. package/src/content/validate-document.js +30 -6
  13. package/src/content/vocabulary.js +26 -0
  14. package/src/errors.js +22 -0
  15. package/src/espalier-shared.js +11 -0
  16. package/src/font-catalog.js +87 -0
  17. package/src/footer-contract.js +1 -1
  18. package/src/output.js +19 -1
  19. package/src/presentation-reference.js +21 -7
  20. package/src/problems.js +116 -0
  21. package/src/reference-help.js +100 -34
  22. package/src/session.js +13 -0
  23. package/src/theme-proposals.js +393 -0
  24. package/src/theme-validation.js +31 -10
  25. package/src/typed-page-reference.js +11 -3
  26. package/src/typed-pages.js +31 -9
  27. package/src/verbs/deploy.js +1 -0
  28. package/src/verbs/index.js +13 -0
  29. package/src/verbs/media-upload.js +127 -30
  30. package/src/verbs/nav-push.js +7 -0
  31. package/src/verbs/pages-push.js +406 -199
  32. package/src/verbs/places.js +61 -0
  33. package/src/verbs/plan.js +585 -0
  34. package/src/verbs/theme-push.js +68 -36
  35. package/src/verbs/validate.js +264 -217
  36. package/src/video-prepare.js +48 -12
  37. package/src/workspace-check.js +199 -0
  38. package/src/workspace-ledger.js +173 -0
  39. package/src/workspace.js +47 -4
@@ -286,6 +286,69 @@ function describeDifferences(validated, presentation) {
286
286
  });
287
287
  }
288
288
 
289
+ /** The refusal for a workspace pulled before the presentation baseline existed. */
290
+ export function presentationPullRequired() {
291
+ return new SiteAuthoringError(
292
+ "theme.pull_required",
293
+ "The pull manifest records no presentation baseline, so this push has no revision to be fenced by. Run "
294
+ + "'taproot-site pull' against a Taproot that serves the atomic presentation save, then retry 'theme push'.",
295
+ { field: "presentation.revision" },
296
+ );
297
+ }
298
+
299
+ /** The refusal for a presentation that moved since the pull, naming where the two differ. */
300
+ export function presentationMovedRefusal({ baseline, current, differences }) {
301
+ return new SiteAuthoringError(
302
+ "theme.concurrent_modification",
303
+ `The site's presentation changed after this workspace was pulled (revision ${baseline.revision} → `
304
+ + `${current.revision}), so nothing was written. Keep copies of the edited settings files, run `
305
+ + "'taproot-site pull' to refresh the baseline, re-apply the edits, and push again; the paths below name "
306
+ + "where the workspace and the site now differ.",
307
+ { field: "revision", alternatives: differences.filter((entry) => entry.paths.length > 0).map((entry) => entry.file) },
308
+ ).withDifferences(differences.flatMap((entry) => entry.paths.map((path) => `${entry.file}:${path}`)));
309
+ }
310
+
311
+ /**
312
+ * What pushing this validated presentation would do, from one fresh read of
313
+ * the site: the JSON paths that differ, whether the pull's baseline is still
314
+ * current, and whether a save whose response was lost would be replayed.
315
+ * `theme push` and `plan` both decide from this (TR00823).
316
+ */
317
+ export async function comparePresentation(client, siteId, validated, manifest) {
318
+ const baseline = readPresentationBaseline(manifest);
319
+ let current;
320
+ try {
321
+ current = await getSitePresentation(client, siteId);
322
+ } catch (error) {
323
+ throw translateServerSupport(error, "read");
324
+ }
325
+ const stale = baseline !== undefined && baseline.revision !== current.revision;
326
+ // A save this workspace sent and never heard back from (TR00807). When the
327
+ // change set about to go is the same one, a moved revision is not evidence
328
+ // of a concurrent edit: it is what that save leaves behind when it
329
+ // committed. Replaying it under the baseline it was sent with lets the site
330
+ // answer already-current, or apply it if it never committed; anything else
331
+ // it refuses itself. A pending record for a different change set proves
332
+ // nothing about the site and takes the ordinary path.
333
+ const changeSet = buildChangeSet(validated);
334
+ const changeSetHash = computePresentationChangeSetHash(changeSet);
335
+ const pending = readPendingPresentationSave(manifest);
336
+ const replayable = pending !== undefined && pending.changeSetHash === changeSetHash;
337
+ const differences = describeDifferences(validated, current);
338
+ return {
339
+ baseline,
340
+ current,
341
+ stale,
342
+ changeSet,
343
+ changeSetHash,
344
+ pending,
345
+ replayable,
346
+ replaying: stale && replayable,
347
+ differences,
348
+ changed: differences.some((entry) => entry.paths.length > 0 || entry.truncated === true),
349
+ };
350
+ }
351
+
289
352
  export async function themePush(invocation) {
290
353
  const session = await openSession(invocation);
291
354
  const { client, config, siteId, onProgress } = session;
@@ -307,12 +370,7 @@ export async function themePush(invocation) {
307
370
  requireFooterContentPushed(appearanceContext.footer, publishing.footerSettings);
308
371
  const baseline = readPresentationBaseline(appearanceContext.manifest);
309
372
  if (!dryRun && baseline === undefined) {
310
- throw new SiteAuthoringError(
311
- "theme.pull_required",
312
- "The pull manifest records no presentation baseline, so this push has no revision to be fenced by. Run "
313
- + "'taproot-site pull' against a Taproot that serves the atomic presentation save, then retry 'theme push'.",
314
- { field: "presentation.revision" },
315
- );
373
+ throw presentationPullRequired();
316
374
  }
317
375
  onProgress(`Validated the complete light/dark theme pair and ${scalarOperations.length} appearance settings.`);
318
376
  for (const warning of themes.warnings) onProgress(`Espalier warning: ${warning}`);
@@ -329,29 +387,11 @@ export async function themePush(invocation) {
329
387
  // The fresh read serves both modes: the dry run compares against it, and
330
388
  // the push takes the footer document's draft token from it. Nothing before
331
389
  // this point, and nothing in a dry run after it, writes to the site.
332
- let current;
333
- try {
334
- current = await withRefusalGuidance(onProgress, "theme push", async () => await getSitePresentation(client, siteId));
335
- } catch (error) {
336
- throw translateServerSupport(error, "read");
337
- }
338
- const stale = baseline !== undefined && baseline.revision !== current.revision;
339
- // A save this workspace sent and never heard back from (TR00807). When the
340
- // change set about to go is the same one, a moved revision is not evidence
341
- // of a concurrent edit: it is what that save leaves behind when it
342
- // committed. Replaying it under the baseline it was sent with lets the site
343
- // answer already-current, or apply it if it never committed; anything else
344
- // it refuses itself. A pending record for a different change set proves
345
- // nothing about the site and takes the ordinary path.
346
- const changeSet = buildChangeSet(validated);
347
- const changeSetHash = computePresentationChangeSetHash(changeSet);
348
- const pending = readPendingPresentationSave(appearanceContext.manifest);
349
- const replayable = pending !== undefined && pending.changeSetHash === changeSetHash;
350
- const replaying = stale && replayable;
390
+ const comparison = await withRefusalGuidance(onProgress, "theme push", async () =>
391
+ await comparePresentation(client, siteId, validated, appearanceContext.manifest));
392
+ const { current, stale, changeSet, changeSetHash, pending, replayable, replaying, differences, changed } = comparison;
351
393
 
352
394
  if (dryRun) {
353
- const differences = describeDifferences(validated, current);
354
- const changed = differences.some((entry) => entry.paths.length > 0 || entry.truncated === true);
355
395
  for (const entry of differences) {
356
396
  if (entry.paths.length === 0 && entry.truncated !== true) continue;
357
397
  onProgress(`${entry.file} differs at: ${entry.paths.join(", ")}${entry.truncated ? ", and more" : ""}.`);
@@ -401,15 +441,7 @@ export async function themePush(invocation) {
401
441
  + "it. The pull that refreshes the baseline shows what the site holds now.",
402
442
  );
403
443
  }
404
- const differences = describeDifferences(validated, current);
405
- throw new SiteAuthoringError(
406
- "theme.concurrent_modification",
407
- `The site's presentation changed after this workspace was pulled (revision ${baseline.revision} → `
408
- + `${current.revision}), so nothing was written. Keep copies of the edited settings files, run `
409
- + "'taproot-site pull' to refresh the baseline, re-apply the edits, and push again; the paths below name "
410
- + "where the workspace and the site now differ.",
411
- { field: "revision", alternatives: differences.filter((entry) => entry.paths.length > 0).map((entry) => entry.file) },
412
- ).withDifferences(differences.flatMap((entry) => entry.paths.map((path) => `${entry.file}:${path}`)));
444
+ throw presentationMovedRefusal(comparison);
413
445
  }
414
446
 
415
447
  const expectedRevision = replaying ? pending.expectedRevision : baseline.revision;