canopycms 0.0.67 → 0.0.68-int.101
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 +24 -17
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +118 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +19 -1
- package/dist/api/github-sync.js +57 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +5928 -2195
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +2185 -587
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +32 -22
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/PreviewFrame.d.ts +24 -0
- package/dist/editor/PreviewFrame.js +140 -0
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +204 -37
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +9 -21
- package/dist/editor/preview-bridge.js +19 -139
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +15 -0
- package/dist/github-service.js +19 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +64 -9
- package/dist/server.js +48 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-serialize.d.ts +5 -2
- package/dist/utils/content-serialize.js +98 -24
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +99 -14
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +8 -2
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { CanopyUser } from './user.js';
|
|
2
|
+
/**
|
|
3
|
+
* Who submitted (and edited) a branch, recorded in the submit commit's trailers
|
|
4
|
+
* and the pull request body. The bot stays the commit author; these records are
|
|
5
|
+
* the only place the editing user appears.
|
|
6
|
+
*
|
|
7
|
+
* Display names, ids and emails come from the auth provider and end up in git
|
|
8
|
+
* history and in GitHub-rendered Markdown, so each passes through a sanitizer
|
|
9
|
+
* below before it is written anywhere.
|
|
10
|
+
*/
|
|
11
|
+
export interface SubmissionEditor {
|
|
12
|
+
userId: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
email?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The PR body region canopycms owns. On update only the text between these
|
|
18
|
+
* markers is replaced, so anything a human wrote around it survives.
|
|
19
|
+
* @internal Exported for tests.
|
|
20
|
+
*/
|
|
21
|
+
export declare const PR_SECTION_START = "<!-- canopycms:submission:start -->";
|
|
22
|
+
/** @internal Exported for tests. */
|
|
23
|
+
export declare const PR_SECTION_END = "<!-- canopycms:submission:end -->";
|
|
24
|
+
/**
|
|
25
|
+
* A display name safe for a single line of git trailer or Markdown, or undefined.
|
|
26
|
+
* @internal Exported for tests.
|
|
27
|
+
*/
|
|
28
|
+
export declare function sanitizeDisplayName(raw: string | undefined): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The auth user id unchanged, or undefined if it holds characters unsafe to record.
|
|
31
|
+
* @internal Exported for tests.
|
|
32
|
+
*/
|
|
33
|
+
export declare function sanitizeUserId(raw: string | undefined): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* The email as given if it is a plain single-address email, otherwise undefined.
|
|
36
|
+
* @internal Exported for tests.
|
|
37
|
+
*/
|
|
38
|
+
export declare function sanitizeEmail(raw: string | undefined): string | undefined;
|
|
39
|
+
/** The submitter as a SubmissionEditor, or undefined for an anonymous user. */
|
|
40
|
+
export declare function submissionEditorFromUser(user: CanopyUser): SubmissionEditor | undefined;
|
|
41
|
+
export interface CommitTrailerOptions {
|
|
42
|
+
/** One `Edited-by: Name (id)` per editor. */
|
|
43
|
+
editedBy: boolean;
|
|
44
|
+
/** One `Co-authored-by: Name <email>` per editor whose email is valid. */
|
|
45
|
+
coAuthoredBy: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** Git trailer lines for the given editors, deduplicated by user id. */
|
|
48
|
+
export declare function buildEditorTrailers(editors: readonly SubmissionEditor[], options: CommitTrailerOptions): string[];
|
|
49
|
+
/**
|
|
50
|
+
* `subject`, then a blank line and the trailer block when there are trailers.
|
|
51
|
+
* Git reads trailers only from the last paragraph, so the blank line is what
|
|
52
|
+
* makes them trailers rather than part of the subject.
|
|
53
|
+
*/
|
|
54
|
+
export declare function appendTrailers(subject: string, trailers: readonly string[]): string;
|
|
55
|
+
export interface PrSectionInput {
|
|
56
|
+
/** The branch description the editor wrote; free Markdown. */
|
|
57
|
+
description?: string;
|
|
58
|
+
submitter?: SubmissionEditor;
|
|
59
|
+
/** Editors other than the submitter, when known. */
|
|
60
|
+
editors?: readonly SubmissionEditor[];
|
|
61
|
+
/** Repo-relative paths changed on the branch. */
|
|
62
|
+
changedPaths: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
/** The canopycms-owned PR body section, including its start and end markers. */
|
|
65
|
+
export declare function buildPrSection(input: PrSectionInput): string;
|
|
66
|
+
/**
|
|
67
|
+
* `existing` with its canopycms section replaced by `section`, keeping every
|
|
68
|
+
* character outside the markers. The section is the first end marker together
|
|
69
|
+
* with the nearest start marker before it, so a lone start marker a human quoted
|
|
70
|
+
* earlier in the body is skipped. With no such pair, stray markers are
|
|
71
|
+
* removed and `section` is appended after the human text.
|
|
72
|
+
*/
|
|
73
|
+
export declare function mergePrSection(existing: string | null | undefined, section: string): string;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { canopyLogWarn } from './utils/logger.js';
|
|
2
|
+
const MAX_NAME_LENGTH = 80;
|
|
3
|
+
const MAX_ID_LENGTH = 128;
|
|
4
|
+
const MAX_EMAIL_LENGTH = 254;
|
|
5
|
+
const MAX_DESCRIPTION_LENGTH = 10_000;
|
|
6
|
+
const MAX_LISTED_ENTRIES = 100;
|
|
7
|
+
/**
|
|
8
|
+
* The PR body region canopycms owns. On update only the text between these
|
|
9
|
+
* markers is replaced, so anything a human wrote around it survives.
|
|
10
|
+
* @internal Exported for tests.
|
|
11
|
+
*/
|
|
12
|
+
export const PR_SECTION_START = '<!-- canopycms:submission:start -->';
|
|
13
|
+
/** @internal Exported for tests. */
|
|
14
|
+
export const PR_SECTION_END = '<!-- canopycms:submission:end -->';
|
|
15
|
+
// Cc (C0/C1 controls incl. newlines), Cf (zero-width and bidi overrides), Zl/Zp
|
|
16
|
+
// (Unicode line/paragraph separators) can forge a trailer line or hide text; Cs
|
|
17
|
+
// (a lone surrogate) reaches git as U+FFFD, since Node encodes it so.
|
|
18
|
+
const INVISIBLE_OR_CONTROL = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/gu;
|
|
19
|
+
// `<>` would end a Co-authored-by email or open HTML / a section marker, `()`
|
|
20
|
+
// would let a name forge the id that follows it, a backtick would close the
|
|
21
|
+
// code span names render in, and a backslash is Markdown's escape character.
|
|
22
|
+
const STRUCTURAL = /[<>()`\\]/g;
|
|
23
|
+
function truncate(value, max) {
|
|
24
|
+
const chars = Array.from(value);
|
|
25
|
+
return chars.length > max ? `${chars.slice(0, max - 1).join('')}…` : value;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A changed path for a code span: only what could break out of the span or out
|
|
29
|
+
* of the section is removed, so `app/(group)/page.mdx` stays distinct from
|
|
30
|
+
* `app/page.mdx`. No NFKC, for the same reason.
|
|
31
|
+
*/
|
|
32
|
+
function cleanPath(raw) {
|
|
33
|
+
const cleaned = raw.replace(INVISIBLE_OR_CONTROL, ' ').replace(/[<>`]/g, '').trim();
|
|
34
|
+
return cleaned ? truncate(cleaned, 300) : undefined;
|
|
35
|
+
}
|
|
36
|
+
function cleanText(raw, max) {
|
|
37
|
+
if (raw === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
const cleaned = raw
|
|
40
|
+
.normalize('NFKC')
|
|
41
|
+
.replace(INVISIBLE_OR_CONTROL, ' ')
|
|
42
|
+
.replace(STRUCTURAL, '')
|
|
43
|
+
.replace(/\s+/g, ' ')
|
|
44
|
+
.trim();
|
|
45
|
+
return cleaned ? truncate(cleaned, max) : undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A display name safe for a single line of git trailer or Markdown, or undefined.
|
|
49
|
+
* @internal Exported for tests.
|
|
50
|
+
*/
|
|
51
|
+
export function sanitizeDisplayName(raw) {
|
|
52
|
+
return cleanText(raw, MAX_NAME_LENGTH);
|
|
53
|
+
}
|
|
54
|
+
// Ids and emails are accepted as the provider issued them or rejected, never
|
|
55
|
+
// cleaned: cleaning could merge two users or attribute an edit to someone else.
|
|
56
|
+
// A trailer still swaps in look-alikes for display (trailerText).
|
|
57
|
+
const UNSAFE_VERBATIM_CHAR = /[\s\p{Cc}\p{Cf}\p{Cs}<>()`\\]/u;
|
|
58
|
+
/**
|
|
59
|
+
* The auth user id unchanged, or undefined if it holds characters unsafe to record.
|
|
60
|
+
* @internal Exported for tests.
|
|
61
|
+
*/
|
|
62
|
+
export function sanitizeUserId(raw) {
|
|
63
|
+
if (!raw || Array.from(raw).length > MAX_ID_LENGTH || UNSAFE_VERBATIM_CHAR.test(raw))
|
|
64
|
+
return undefined;
|
|
65
|
+
return raw;
|
|
66
|
+
}
|
|
67
|
+
const EMAIL_SHAPE = /^[^\s@<>()"',;:\\[\]]+@[^\s@<>()"',;:\\[\]]+\.[^\s@<>()"',;:\\[\]]+$/;
|
|
68
|
+
/**
|
|
69
|
+
* The email as given if it is a plain single-address email, otherwise undefined.
|
|
70
|
+
* @internal Exported for tests.
|
|
71
|
+
*/
|
|
72
|
+
export function sanitizeEmail(raw) {
|
|
73
|
+
if (raw === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
const trimmed = raw.trim();
|
|
76
|
+
// `\s` misses most C0/C1 controls, and a NUL makes git's spawn throw. `#` is
|
|
77
|
+
// legal in an address but GitHub links `#12` in a commit message.
|
|
78
|
+
if (trimmed.length > MAX_EMAIL_LENGTH ||
|
|
79
|
+
UNSAFE_VERBATIM_CHAR.test(trimmed) ||
|
|
80
|
+
trimmed.includes('#'))
|
|
81
|
+
return undefined;
|
|
82
|
+
return EMAIL_SHAPE.test(trimmed) ? trimmed : undefined;
|
|
83
|
+
}
|
|
84
|
+
/** The submitter as a SubmissionEditor, or undefined for an anonymous user. */
|
|
85
|
+
export function submissionEditorFromUser(user) {
|
|
86
|
+
if (user.type !== 'authenticated')
|
|
87
|
+
return undefined;
|
|
88
|
+
return { userId: user.userId, name: user.name, email: user.email };
|
|
89
|
+
}
|
|
90
|
+
function cleanEditors(editors) {
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
const result = [];
|
|
93
|
+
for (const editor of editors) {
|
|
94
|
+
const userId = sanitizeUserId(editor.userId);
|
|
95
|
+
if (!userId) {
|
|
96
|
+
// The id itself stays out of the log: it is the unsafe value.
|
|
97
|
+
if (editor.userId) {
|
|
98
|
+
canopyLogWarn('CanopyCMS: Not recording an editor whose user id is unsafe to write to git');
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (seen.has(userId))
|
|
103
|
+
continue;
|
|
104
|
+
seen.add(userId);
|
|
105
|
+
result.push({
|
|
106
|
+
userId,
|
|
107
|
+
name: sanitizeDisplayName(editor.name),
|
|
108
|
+
email: sanitizeEmail(editor.email),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* GitHub autolinks commit messages: `@user` mentions, `scheme://` URLs, and `#12` /
|
|
115
|
+
* `GH-12` issue references, which a closing keyword ("Closes #12") turns into an
|
|
116
|
+
* issue close once the commit reaches the default branch. A trailer value gets
|
|
117
|
+
* `@` and `#` swapped for look-alikes and the other two patterns broken. Its
|
|
118
|
+
* input is already single-line and structure-free.
|
|
119
|
+
*/
|
|
120
|
+
function trailerText(value) {
|
|
121
|
+
return value
|
|
122
|
+
.replace(/@/g, '@')
|
|
123
|
+
.replace(/#/g, '#')
|
|
124
|
+
.replace(/:\/\//g, ': //')
|
|
125
|
+
.replace(/\bGH-(?=\d)/gi, (m) => `${m.slice(0, 2)}‐`);
|
|
126
|
+
}
|
|
127
|
+
/** Git trailer lines for the given editors, deduplicated by user id. */
|
|
128
|
+
export function buildEditorTrailers(editors, options) {
|
|
129
|
+
const trailers = [];
|
|
130
|
+
const clean = cleanEditors(editors);
|
|
131
|
+
if (options.editedBy) {
|
|
132
|
+
for (const editor of clean) {
|
|
133
|
+
trailers.push(editor.name
|
|
134
|
+
? `Edited-by: ${trailerText(editor.name)} (${trailerText(editor.userId)})`
|
|
135
|
+
: `Edited-by: ${trailerText(editor.userId)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (options.coAuthoredBy) {
|
|
139
|
+
for (const editor of clean) {
|
|
140
|
+
if (!editor.email)
|
|
141
|
+
continue;
|
|
142
|
+
trailers.push(`Co-authored-by: ${trailerText(editor.name ?? editor.userId)} <${editor.email}>`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return trailers;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* `subject`, then a blank line and the trailer block when there are trailers.
|
|
149
|
+
* Git reads trailers only from the last paragraph, so the blank line is what
|
|
150
|
+
* makes them trailers rather than part of the subject.
|
|
151
|
+
*/
|
|
152
|
+
export function appendTrailers(subject, trailers) {
|
|
153
|
+
return trailers.length > 0 ? `${subject.trimEnd()}\n\n${trailers.join('\n')}` : subject;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Renders as inline code: GitHub neither autolinks, mentions nor interprets HTML
|
|
157
|
+
* inside a code span. The input is already free of backticks.
|
|
158
|
+
*/
|
|
159
|
+
function codeSpan(value) {
|
|
160
|
+
return `\`${value}\``;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Free Markdown with every HTML comment opener and closer escaped, so the text
|
|
164
|
+
* can neither close the canopycms section early nor open a comment that hides
|
|
165
|
+
* the rest of the body. Escaped comments render as their literal text.
|
|
166
|
+
*/
|
|
167
|
+
function neutralizeComments(text) {
|
|
168
|
+
return text.replace(/<!--/g, '<!--').replace(/--!?>/g, '-->');
|
|
169
|
+
}
|
|
170
|
+
function editorLabel(editor) {
|
|
171
|
+
return editor.name
|
|
172
|
+
? `${codeSpan(editor.name)} (${codeSpan(editor.userId)})`
|
|
173
|
+
: codeSpan(editor.userId);
|
|
174
|
+
}
|
|
175
|
+
/** The canopycms-owned PR body section, including its start and end markers. */
|
|
176
|
+
export function buildPrSection(input) {
|
|
177
|
+
const parts = [];
|
|
178
|
+
const [submitter] = input.submitter ? cleanEditors([input.submitter]) : [];
|
|
179
|
+
parts.push(submitter
|
|
180
|
+
? `Submitted by ${editorLabel(submitter)} via CanopyCMS.`
|
|
181
|
+
: 'Submitted via CanopyCMS.');
|
|
182
|
+
const others = cleanEditors(input.editors ?? []).filter((e) => e.userId !== submitter?.userId);
|
|
183
|
+
if (others.length > 0) {
|
|
184
|
+
parts.push(`Also edited by: ${others.map(editorLabel).join(', ')}`);
|
|
185
|
+
}
|
|
186
|
+
const paths = [...new Set(input.changedPaths)]
|
|
187
|
+
.map(cleanPath)
|
|
188
|
+
.filter((p) => p !== undefined);
|
|
189
|
+
if (paths.length > 0) {
|
|
190
|
+
const listed = paths.slice(0, MAX_LISTED_ENTRIES).map((p) => `- ${codeSpan(p)}`);
|
|
191
|
+
if (paths.length > MAX_LISTED_ENTRIES) {
|
|
192
|
+
listed.push(`- …and ${paths.length - MAX_LISTED_ENTRIES} more`);
|
|
193
|
+
}
|
|
194
|
+
parts.push(`**Changed entries (${paths.length})**\n\n${listed.join('\n')}`);
|
|
195
|
+
}
|
|
196
|
+
// Last, so an unclosed fence or `<details>` in free Markdown cannot swallow
|
|
197
|
+
// the attribution above it.
|
|
198
|
+
const description = input.description?.trim();
|
|
199
|
+
if (description)
|
|
200
|
+
parts.push(neutralizeComments(truncate(description, MAX_DESCRIPTION_LENGTH)));
|
|
201
|
+
return `${PR_SECTION_START}\n${parts.join('\n\n')}\n${PR_SECTION_END}`;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* `existing` with its canopycms section replaced by `section`, keeping every
|
|
205
|
+
* character outside the markers. The section is the first end marker together
|
|
206
|
+
* with the nearest start marker before it, so a lone start marker a human quoted
|
|
207
|
+
* earlier in the body is skipped. With no such pair, stray markers are
|
|
208
|
+
* removed and `section` is appended after the human text.
|
|
209
|
+
*/
|
|
210
|
+
export function mergePrSection(existing, section) {
|
|
211
|
+
const body = existing ?? '';
|
|
212
|
+
for (let end = body.indexOf(PR_SECTION_END); end >= 0;) {
|
|
213
|
+
const start = body.lastIndexOf(PR_SECTION_START, end - PR_SECTION_START.length);
|
|
214
|
+
if (start >= 0) {
|
|
215
|
+
return body.slice(0, start) + section + body.slice(end + PR_SECTION_END.length);
|
|
216
|
+
}
|
|
217
|
+
end = body.indexOf(PR_SECTION_END, end + PR_SECTION_END.length);
|
|
218
|
+
}
|
|
219
|
+
const human = body.split(PR_SECTION_START).join('').split(PR_SECTION_END).join('').trimEnd();
|
|
220
|
+
return human ? `${human}\n\n${section}` : section;
|
|
221
|
+
}
|
package/dist/sync-core.d.ts
CHANGED
|
@@ -44,13 +44,24 @@ export interface PushContentToWorkspaceOptions {
|
|
|
44
44
|
commitMessage?: string;
|
|
45
45
|
/** When set, (re)tag the resulting commit as the sync base (used by `sync both` 3-way merges). */
|
|
46
46
|
baseTag?: string;
|
|
47
|
+
/**
|
|
48
|
+
* When set, uncommitted workspace changes (editor saves) are first committed with this
|
|
49
|
+
* message, so the content replacement leaves them in history rather than discarding them.
|
|
50
|
+
*/
|
|
51
|
+
saveEditorStateMessage?: string;
|
|
47
52
|
}
|
|
48
53
|
/**
|
|
49
54
|
* Copy working-tree content into a branch workspace and commit it. Prompt-free — the interactive CLI
|
|
50
55
|
* (`canopycms sync push`) calls this for the actual copy + commit + tag step.
|
|
51
56
|
*
|
|
52
|
-
*
|
|
57
|
+
* [SYNC-C1] Replaces the workspace's content directory wholesale, so it runs under the branch's
|
|
58
|
+
* content-write lock: an editor save landing mid-swap would go into the directory being discarded.
|
|
59
|
+
* Rejects with `ContentWriteLockBusyError` when the branch is busy.
|
|
60
|
+
*
|
|
61
|
+
* Returns the number of changed files committed (0 when content was already up to date), and
|
|
62
|
+
* whether uncommitted editor changes were committed first.
|
|
53
63
|
*/
|
|
54
64
|
export declare function pushContentToWorkspace(options: PushContentToWorkspaceOptions): Promise<{
|
|
55
65
|
fileCount: number;
|
|
66
|
+
savedEditorState: boolean;
|
|
56
67
|
}>;
|
package/dist/sync-core.js
CHANGED
|
@@ -7,7 +7,9 @@ import fs from 'node:fs/promises';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { simpleGit } from 'simple-git';
|
|
9
9
|
import { invalidateBranchContentCaches } from './content-index-generation.js';
|
|
10
|
+
import { withContentWriteLock } from './utils/content-write-lock.js';
|
|
10
11
|
import { filePathExists } from './utils/fs.js';
|
|
12
|
+
import { isCanopyInternalPath, stageAllExceptCanopyState } from './utils/git.js';
|
|
11
13
|
/** Git tag marking the last known sync point, used as the merge base for `sync both` 3-way merges. */
|
|
12
14
|
export const SYNC_BASE_TAG = 'canopycms-sync-base';
|
|
13
15
|
/** Validate that a resolved path stays within the expected parent directory. */
|
|
@@ -132,15 +134,34 @@ export async function diffContentTrees(workingTreeDir, branchDir) {
|
|
|
132
134
|
* Copy working-tree content into a branch workspace and commit it. Prompt-free — the interactive CLI
|
|
133
135
|
* (`canopycms sync push`) calls this for the actual copy + commit + tag step.
|
|
134
136
|
*
|
|
135
|
-
*
|
|
137
|
+
* [SYNC-C1] Replaces the workspace's content directory wholesale, so it runs under the branch's
|
|
138
|
+
* content-write lock: an editor save landing mid-swap would go into the directory being discarded.
|
|
139
|
+
* Rejects with `ContentWriteLockBusyError` when the branch is busy.
|
|
140
|
+
*
|
|
141
|
+
* Returns the number of changed files committed (0 when content was already up to date), and
|
|
142
|
+
* whether uncommitted editor changes were committed first.
|
|
136
143
|
*/
|
|
137
144
|
export async function pushContentToWorkspace(options) {
|
|
138
|
-
const { srcContentDir, branchPath, contentRoot
|
|
145
|
+
const { srcContentDir, branchPath, contentRoot } = options;
|
|
139
146
|
if (!(await filePathExists(srcContentDir))) {
|
|
140
|
-
return { fileCount: 0 };
|
|
147
|
+
return { fileCount: 0, savedEditorState: false };
|
|
141
148
|
}
|
|
142
149
|
const wsContentDir = path.join(branchPath, contentRoot);
|
|
143
150
|
assertWithinDir(wsContentDir, branchPath, 'content-root');
|
|
151
|
+
return withContentWriteLock(branchPath, () => replaceWorkspaceContent(options, wsContentDir));
|
|
152
|
+
}
|
|
153
|
+
async function replaceWorkspaceContent(options, wsContentDir) {
|
|
154
|
+
const { srcContentDir, branchPath, commitMessage, baseTag, saveEditorStateMessage } = options;
|
|
155
|
+
const wsGit = simpleGit({ baseDir: branchPath });
|
|
156
|
+
let savedEditorState = false;
|
|
157
|
+
if (saveEditorStateMessage) {
|
|
158
|
+
const editorChanges = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
159
|
+
if (editorChanges.length > 0) {
|
|
160
|
+
await stageAllExceptCanopyState(wsGit);
|
|
161
|
+
await wsGit.commit(saveEditorStateMessage);
|
|
162
|
+
savedEditorState = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
144
165
|
// Copy into a temp dir, then atomically swap it into place.
|
|
145
166
|
const tmpDir = `${wsContentDir}.sync-tmp-${Date.now()}`;
|
|
146
167
|
try {
|
|
@@ -154,23 +175,20 @@ export async function pushContentToWorkspace(options) {
|
|
|
154
175
|
// The content dir was replaced wholesale, so ContentStore ID indexes rooted
|
|
155
176
|
// at this workspace must be marked stale — in this process and (via the
|
|
156
177
|
// on-disk generation marker) in every other process sharing the filesystem.
|
|
157
|
-
// Done in the finally AFTER the git add/commit below
|
|
158
|
-
// .canopy-meta
|
|
159
|
-
// the sync commit via `add -A` (production workspaces git-exclude
|
|
160
|
-
// .canopy-meta/, but this function shouldn't depend on that).
|
|
178
|
+
// Done in the finally AFTER the git add/commit below, and staging skips
|
|
179
|
+
// .canopy-meta/, where the marker lives.
|
|
161
180
|
try {
|
|
162
|
-
|
|
163
|
-
await wsGit.
|
|
164
|
-
|
|
165
|
-
if (postStatus.files.length === 0) {
|
|
181
|
+
await stageAllExceptCanopyState(wsGit);
|
|
182
|
+
const staged = (await wsGit.status()).files.filter((f) => !isCanopyInternalPath(f.path));
|
|
183
|
+
if (staged.length === 0) {
|
|
166
184
|
if (baseTag)
|
|
167
185
|
await wsGit.tag(['-f', baseTag]);
|
|
168
|
-
return { fileCount: 0 };
|
|
186
|
+
return { fileCount: 0, savedEditorState };
|
|
169
187
|
}
|
|
170
188
|
await wsGit.commit(commitMessage ?? 'sync: update content from working tree');
|
|
171
189
|
if (baseTag)
|
|
172
190
|
await wsGit.tag(['-f', baseTag]);
|
|
173
|
-
return { fileCount:
|
|
191
|
+
return { fileCount: staged.length, savedEditorState };
|
|
174
192
|
}
|
|
175
193
|
finally {
|
|
176
194
|
await invalidateBranchContentCaches(branchPath);
|
|
@@ -32,4 +32,12 @@ export declare const WORKER_STATUS_FILE = "worker-status.json";
|
|
|
32
32
|
* `processTaskQueue`, `start()`); any that don't fall back on scheduleLoop's
|
|
33
33
|
* per-cycle catch, which logs and continues.
|
|
34
34
|
*/
|
|
35
|
+
/**
|
|
36
|
+
* The `lastFatalError` in the current status file, or undefined when the file is
|
|
37
|
+
* missing, unreadable or has none. Tolerant for the same reason readers are.
|
|
38
|
+
* Its one caller carries the value into a full snapshot right after taking the
|
|
39
|
+
* worker lock; a write racing in between only keeps or drops one crash alert
|
|
40
|
+
* until the next write.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readLastFatalError(taskDir: string): Promise<WorkerStatusReport['lastFatalError']>;
|
|
35
43
|
export declare function writeWorkerStatus(taskDir: string, report: WorkerStatusReport): Promise<void>;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* docs/concurrency.md) means a reader can see a snapshot a cache window old
|
|
20
20
|
* however carefully it was written.
|
|
21
21
|
*/
|
|
22
|
+
import fs from 'node:fs/promises';
|
|
22
23
|
import path from 'node:path';
|
|
23
24
|
import { atomicWriteFile } from '../utils/atomic-write.js';
|
|
24
25
|
export const WORKER_STATUS_FILE = 'worker-status.json';
|
|
@@ -33,6 +34,22 @@ export const WORKER_STATUS_FILE = 'worker-status.json';
|
|
|
33
34
|
* `processTaskQueue`, `start()`); any that don't fall back on scheduleLoop's
|
|
34
35
|
* per-cycle catch, which logs and continues.
|
|
35
36
|
*/
|
|
37
|
+
/**
|
|
38
|
+
* The `lastFatalError` in the current status file, or undefined when the file is
|
|
39
|
+
* missing, unreadable or has none. Tolerant for the same reason readers are.
|
|
40
|
+
* Its one caller carries the value into a full snapshot right after taking the
|
|
41
|
+
* worker lock; a write racing in between only keeps or drops one crash alert
|
|
42
|
+
* until the next write.
|
|
43
|
+
*/
|
|
44
|
+
export async function readLastFatalError(taskDir) {
|
|
45
|
+
try {
|
|
46
|
+
const content = await fs.readFile(path.join(taskDir, WORKER_STATUS_FILE), 'utf-8');
|
|
47
|
+
return JSON.parse(content).lastFatalError;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
36
53
|
export async function writeWorkerStatus(taskDir, report) {
|
|
37
54
|
const filePath = path.join(taskDir, WORKER_STATUS_FILE);
|
|
38
55
|
const stamped = { ...report, updatedAt: new Date().toISOString() };
|
package/dist/types.d.ts
CHANGED
|
@@ -102,6 +102,31 @@ export interface BranchContext extends BranchPaths {
|
|
|
102
102
|
export interface BranchContextWithSchema extends BranchContext {
|
|
103
103
|
flatSchema: import('./config/index.js').FlatSchemaItem[];
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Outcome of the worker's per-cycle fast-forward of the base branch's own clone
|
|
107
|
+
* (worker/git-sync.ts's `refreshBaseBranchWorkspace`); the rebase loop skips
|
|
108
|
+
* that clone.
|
|
109
|
+
*/
|
|
110
|
+
export interface BaseRefreshReport {
|
|
111
|
+
outcome: 'refreshed' | 'up-to-date' | 'skipped-dirty' | 'skipped-locked' | 'skipped-not-provisioned' | 'failed';
|
|
112
|
+
/** Tracked files blocking the refresh, at most 10; set when `skipped-dirty`. */
|
|
113
|
+
dirtyFiles?: string[];
|
|
114
|
+
/** What went wrong, credential-redacted; set when `failed` or `skipped-dirty`. */
|
|
115
|
+
message?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Files under `.canopy-meta/` the adopter's repo tracks, at most 10. canopycms
|
|
118
|
+
* rewrites that state per branch, so tracking it is an adopter misconfiguration
|
|
119
|
+
* the panel surfaces with its fix, whatever the outcome.
|
|
120
|
+
*/
|
|
121
|
+
trackedCanopyMeta?: string[];
|
|
122
|
+
}
|
|
123
|
+
/** What a running CMS process reports about the build it was started from. */
|
|
124
|
+
export interface BuildIdentity {
|
|
125
|
+
/** canopycms package version this process runs. */
|
|
126
|
+
canopycmsVersion: string;
|
|
127
|
+
/** Adopter source revision the deployed image was built from (CANOPY_SOURCE_SHA); absent when unset. */
|
|
128
|
+
sourceRevision?: string;
|
|
129
|
+
}
|
|
105
130
|
/**
|
|
106
131
|
* Wire shape of the worker's self-reported status file (worker-status.json, under
|
|
107
132
|
* the task queue dir), written by the CmsWorker daemon. Read-only here: GET
|
|
@@ -109,6 +134,7 @@ export interface BranchContextWithSchema extends BranchContext {
|
|
|
109
134
|
*/
|
|
110
135
|
export interface WorkerStatusReport {
|
|
111
136
|
version: 1;
|
|
137
|
+
/** canopycms version the worker runs. Optional: status files from older workers lack it. */
|
|
112
138
|
workerVersion?: string;
|
|
113
139
|
startedAt: string;
|
|
114
140
|
updatedAt: string;
|
|
@@ -123,8 +149,9 @@ export interface WorkerStatusReport {
|
|
|
123
149
|
rebased: string[];
|
|
124
150
|
skippedDirty: string[];
|
|
125
151
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
152
|
+
* Branches skipped because another process held a lock the worker
|
|
153
|
+
* try-acquires: the [SYNC-C1] content-write lock (utils/content-write-lock.ts)
|
|
154
|
+
* or the provisioning lock (worker/provisioned-workspace.ts). The worker
|
|
128
155
|
* yields and retries next cycle. Optional for the same reason as `tracked`
|
|
129
156
|
* below, and readers must tolerate its absence.
|
|
130
157
|
*/
|
|
@@ -133,6 +160,8 @@ export interface WorkerStatusReport {
|
|
|
133
160
|
branch: string;
|
|
134
161
|
error: string;
|
|
135
162
|
}[];
|
|
163
|
+
/** Optional for the same reason as `tracked` below. */
|
|
164
|
+
baseRefresh?: BaseRefreshReport;
|
|
136
165
|
/**
|
|
137
166
|
* Outcome of reconciling remote.git's `refs/heads/*` against GitHub's fetched
|
|
138
167
|
* tips, non-destructively (worker/git-sync.ts's `reconcileTrackedBranches`).
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* deletes every comment in the file, because comments live in neither the object nor that round
|
|
6
6
|
* trip. So these functions re-serialise onto the file's OWN parsed document: a node whose value
|
|
7
7
|
* did not change is left untouched, and an untouched node keeps its attached comments and its
|
|
8
|
-
* original quoting/block style. Only what actually changed is rewritten
|
|
8
|
+
* original quoting/block style. Only what actually changed is rewritten, and
|
|
9
|
+
* `yaml-source-splice.ts` writes it into the file's own text, so untouched lines keep their bytes
|
|
10
|
+
* wherever it can vouch for the result.
|
|
9
11
|
*
|
|
10
12
|
* The file gets no authority over its own content. The reconciler makes the document's key set
|
|
11
13
|
* match `data` exactly — a key the caller dropped disappears, a key the caller kept survives
|
|
@@ -19,7 +21,8 @@
|
|
|
19
21
|
*
|
|
20
22
|
* Falls back to a plain stringify — byte-identical to serialising without preservation — when
|
|
21
23
|
* there is nothing to preserve (a new file) or nothing trustworthy to preserve (the bytes on disk
|
|
22
|
-
* do not parse
|
|
24
|
+
* do not parse, or the reconciled document does not read back as its own data). A save must not
|
|
25
|
+
* fail because the previous content was malformed.
|
|
23
26
|
*/
|
|
24
27
|
export declare function serializeYaml(data: Record<string, unknown>, existingRaw?: string): string;
|
|
25
28
|
/**
|