canopycms 0.0.61 → 0.0.63
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/dist/ai/index.d.ts +9 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +9 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/json-to-markdown.js +1 -2
- package/dist/ai/json-to-markdown.js.map +1 -1
- package/dist/ai/to-plain-text.d.ts +57 -0
- package/dist/ai/to-plain-text.d.ts.map +1 -0
- package/dist/ai/to-plain-text.js +255 -0
- package/dist/ai/to-plain-text.js.map +1 -0
- package/dist/ai/transform-components.d.ts.map +1 -1
- package/dist/ai/transform-components.js +70 -5
- package/dist/ai/transform-components.js.map +1 -1
- package/dist/api/__test__/mock-client.d.ts +5 -1
- package/dist/api/__test__/mock-client.d.ts.map +1 -1
- package/dist/api/__test__/mock-client.js +9 -2
- package/dist/api/__test__/mock-client.js.map +1 -1
- package/dist/api/admin-branch-health.d.ts +36 -1
- package/dist/api/admin-branch-health.d.ts.map +1 -1
- package/dist/api/admin-branch-health.js +176 -2
- package/dist/api/admin-branch-health.js.map +1 -1
- package/dist/api/admin.d.ts +8 -1
- package/dist/api/admin.d.ts.map +1 -1
- package/dist/api/admin.js.map +1 -1
- package/dist/api/branch.d.ts.map +1 -1
- package/dist/api/branch.js +16 -2
- package/dist/api/branch.js.map +1 -1
- package/dist/api/client.d.ts +5 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +6 -0
- package/dist/api/client.js.map +1 -1
- package/dist/api/content.d.ts +11 -5
- package/dist/api/content.d.ts.map +1 -1
- package/dist/api/content.js +79 -6
- package/dist/api/content.js.map +1 -1
- package/dist/api/entries.d.ts +10 -0
- package/dist/api/entries.d.ts.map +1 -1
- package/dist/api/entries.js +40 -15
- package/dist/api/entries.js.map +1 -1
- package/dist/api/schema.d.ts +4 -4
- package/dist/api/schema.d.ts.map +1 -1
- package/dist/api/schema.js +13 -13
- package/dist/api/schema.js.map +1 -1
- package/dist/assets/transform.d.ts.map +1 -1
- package/dist/assets/transform.js +19 -6
- package/dist/assets/transform.js.map +1 -1
- package/dist/authorization/branch.d.ts +55 -3
- package/dist/authorization/branch.d.ts.map +1 -1
- package/dist/authorization/branch.js +55 -9
- package/dist/authorization/branch.js.map +1 -1
- package/dist/authorization/types.d.ts +1 -1
- package/dist/authorization/types.d.ts.map +1 -1
- package/dist/branch-health.d.ts +13 -0
- package/dist/branch-health.d.ts.map +1 -1
- package/dist/branch-health.js +23 -0
- package/dist/branch-health.js.map +1 -1
- package/dist/build-canopy.d.ts +49 -0
- package/dist/build-canopy.d.ts.map +1 -0
- package/dist/build-canopy.js +51 -0
- package/dist/build-canopy.js.map +1 -0
- package/dist/cli/cli.js +935 -562
- package/dist/cli/generate-ai-content.js +837 -480
- package/dist/cli/init.js +25 -7
- package/dist/cli/template-files/Dockerfile.cms.template +19 -1
- package/dist/cli/template-files/canopycms.config.ts.template +25 -1
- package/dist/cli/template-files/cms-stack.ts.template +7 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/config/schemas/field.d.ts +15 -15
- package/dist/config/types.d.ts +2 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +0 -1
- package/dist/config/types.js.map +1 -1
- package/dist/config/validation.d.ts +9 -0
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +14 -0
- package/dist/config/validation.js.map +1 -1
- package/dist/content-id-index.d.ts +97 -5
- package/dist/content-id-index.d.ts.map +1 -1
- package/dist/content-id-index.js +170 -18
- package/dist/content-id-index.js.map +1 -1
- package/dist/content-listing.d.ts +110 -5
- package/dist/content-listing.d.ts.map +1 -1
- package/dist/content-listing.js +164 -23
- package/dist/content-listing.js.map +1 -1
- package/dist/content-reader.d.ts +41 -4
- package/dist/content-reader.d.ts.map +1 -1
- package/dist/content-reader.js +17 -4
- package/dist/content-reader.js.map +1 -1
- package/dist/content-store.d.ts +91 -8
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +443 -258
- package/dist/content-store.js.map +1 -1
- package/dist/content-tree.d.ts +31 -2
- package/dist/content-tree.d.ts.map +1 -1
- package/dist/content-tree.js +22 -5
- package/dist/content-tree.js.map +1 -1
- package/dist/context.d.ts +30 -10
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +55 -4
- package/dist/context.js.map +1 -1
- package/dist/editor/BranchManager.d.ts.map +1 -1
- package/dist/editor/BranchManager.js +18 -6
- package/dist/editor/BranchManager.js.map +1 -1
- package/dist/editor/CanopyEditor.d.ts.map +1 -1
- package/dist/editor/CanopyEditor.js +2 -2
- package/dist/editor/CanopyEditor.js.map +1 -1
- package/dist/editor/CanopyEditorPage.d.ts +9 -1
- package/dist/editor/CanopyEditorPage.d.ts.map +1 -1
- package/dist/editor/CanopyEditorPage.js +9 -2
- package/dist/editor/CanopyEditorPage.js.map +1 -1
- package/dist/editor/Editor.d.ts +9 -1
- package/dist/editor/Editor.d.ts.map +1 -1
- package/dist/editor/Editor.js +58 -24
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/FormRenderer.d.ts.map +1 -1
- package/dist/editor/FormRenderer.js +12 -0
- package/dist/editor/FormRenderer.js.map +1 -1
- package/dist/editor/admin/SystemHealthPanel.js +6 -2
- package/dist/editor/admin/SystemHealthPanel.js.map +1 -1
- package/dist/editor/components/EntryCreateModal.d.ts +9 -1
- package/dist/editor/components/EntryCreateModal.d.ts.map +1 -1
- package/dist/editor/components/EntryCreateModal.js +4 -1
- package/dist/editor/components/EntryCreateModal.js.map +1 -1
- package/dist/editor/fields/DateTimeField.d.ts +46 -0
- package/dist/editor/fields/DateTimeField.d.ts.map +1 -0
- package/dist/editor/fields/DateTimeField.js +66 -0
- package/dist/editor/fields/DateTimeField.js.map +1 -0
- package/dist/editor/fields/NumberField.d.ts +34 -0
- package/dist/editor/fields/NumberField.d.ts.map +1 -0
- package/dist/editor/fields/NumberField.js +53 -0
- package/dist/editor/fields/NumberField.js.map +1 -0
- package/dist/editor/fields/NumberListField.d.ts +25 -0
- package/dist/editor/fields/NumberListField.d.ts.map +1 -0
- package/dist/editor/fields/NumberListField.js +53 -0
- package/dist/editor/fields/NumberListField.js.map +1 -0
- package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
- package/dist/editor/hooks/useBranchManager.js +115 -26
- package/dist/editor/hooks/useBranchManager.js.map +1 -1
- package/dist/editor/hooks/useDraftManager.d.ts +8 -0
- package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
- package/dist/editor/hooks/useDraftManager.js +261 -21
- package/dist/editor/hooks/useDraftManager.js.map +1 -1
- package/dist/editor/hooks/useEntryManager.d.ts +21 -0
- package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
- package/dist/editor/hooks/useEntryManager.js +25 -2
- package/dist/editor/hooks/useEntryManager.js.map +1 -1
- package/dist/editor/theme.d.ts +8 -8
- package/dist/entry-schema.d.ts +228 -8
- package/dist/entry-schema.d.ts.map +1 -1
- package/dist/entry-schema.js +124 -0
- package/dist/entry-schema.js.map +1 -1
- package/dist/git-manager.d.ts +51 -7
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +105 -14
- package/dist/git-manager.js.map +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/handler.js +70 -27
- package/dist/http/handler.js.map +1 -1
- package/dist/http/router.d.ts.map +1 -1
- package/dist/http/router.js +40 -3
- package/dist/http/router.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/operating-mode/deployment-name-fixtures.d.ts +29 -0
- package/dist/operating-mode/deployment-name-fixtures.d.ts.map +1 -0
- package/dist/operating-mode/deployment-name-fixtures.js +55 -0
- package/dist/operating-mode/deployment-name-fixtures.js.map +1 -0
- package/dist/operating-mode/deployment-name.d.ts +9 -0
- package/dist/operating-mode/deployment-name.d.ts.map +1 -1
- package/dist/operating-mode/deployment-name.js +9 -1
- package/dist/operating-mode/deployment-name.js.map +1 -1
- package/dist/operating-mode/index.d.ts +2 -1
- package/dist/operating-mode/index.d.ts.map +1 -1
- package/dist/operating-mode/index.js +4 -1
- package/dist/operating-mode/index.js.map +1 -1
- package/dist/operating-mode/mode-env.d.ts +58 -0
- package/dist/operating-mode/mode-env.d.ts.map +1 -0
- package/dist/operating-mode/mode-env.js +97 -0
- package/dist/operating-mode/mode-env.js.map +1 -0
- package/dist/resolve-canopy-user.d.ts +34 -0
- package/dist/resolve-canopy-user.d.ts.map +1 -0
- package/dist/resolve-canopy-user.js +55 -0
- package/dist/resolve-canopy-user.js.map +1 -0
- package/dist/schema/schema-store.d.ts +53 -2
- package/dist/schema/schema-store.d.ts.map +1 -1
- package/dist/schema/schema-store.js +92 -21
- package/dist/schema/schema-store.js.map +1 -1
- package/dist/server.d.ts +60 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +58 -0
- package/dist/server.js.map +1 -1
- package/dist/services.d.ts +1 -2
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +42 -12
- package/dist/services.js.map +1 -1
- package/dist/settings-workspace.d.ts +40 -0
- package/dist/settings-workspace.d.ts.map +1 -1
- package/dist/settings-workspace.js +139 -151
- package/dist/settings-workspace.js.map +1 -1
- package/dist/static/index.d.ts +65 -5
- package/dist/static/index.d.ts.map +1 -1
- package/dist/static/index.js +65 -12
- package/dist/static/index.js.map +1 -1
- package/dist/static/seo.d.ts +150 -0
- package/dist/static/seo.d.ts.map +1 -0
- package/dist/static/seo.js +189 -0
- package/dist/static/seo.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/content-write-lock.d.ts +127 -0
- package/dist/utils/content-write-lock.d.ts.map +1 -0
- package/dist/utils/content-write-lock.js +170 -0
- package/dist/utils/content-write-lock.js.map +1 -0
- package/dist/utils/error.d.ts +1 -1
- package/dist/utils/error.js +1 -1
- package/dist/utils/git.d.ts +16 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +22 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/sanitize-href.d.ts +74 -3
- package/dist/utils/sanitize-href.d.ts.map +1 -1
- package/dist/utils/sanitize-href.js +125 -6
- package/dist/utils/sanitize-href.js.map +1 -1
- package/dist/validation/entry-link-validator.d.ts +1 -1
- package/dist/validation/entry-link-validator.js +3 -3
- package/dist/validation/entry-link-validator.js.map +1 -1
- package/dist/validation/entry-validator.js +1 -1
- package/dist/validation/entry-validator.js.map +1 -1
- package/dist/worker/cms-worker.d.ts +13 -0
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +326 -264
- package/dist/worker/cms-worker.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic SEO field extraction and URL shaping.
|
|
3
|
+
*
|
|
4
|
+
* This module is pure: it maps an entry's raw data onto a neutral `SeoFields` shape and
|
|
5
|
+
* resolves URLs. Framework adapters (e.g. canopycms-next's `entryToMetadata`) turn that shape
|
|
6
|
+
* into their own metadata objects; nothing here imports a framework or a node built-in.
|
|
7
|
+
*
|
|
8
|
+
* The field names default to the recommended SEO field group — see `defineSeoFieldGroup` in
|
|
9
|
+
* entry-schema.ts, which builds a schema group from these same names so the schema side and
|
|
10
|
+
* the read side cannot drift.
|
|
11
|
+
*/
|
|
12
|
+
/** og:type values covered by the recommended group. */
|
|
13
|
+
export type SeoOgType = 'website' | 'article' | 'profile';
|
|
14
|
+
/** twitter:card values covered by the recommended group. */
|
|
15
|
+
export type SeoTwitterCard = 'summary' | 'summary_large_image';
|
|
16
|
+
/** The neutral, framework-agnostic SEO shape. */
|
|
17
|
+
export interface SeoFields {
|
|
18
|
+
title?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
ogImage?: string;
|
|
21
|
+
ogType?: SeoOgType;
|
|
22
|
+
canonical?: string;
|
|
23
|
+
noindex?: boolean;
|
|
24
|
+
twitterCard?: SeoTwitterCard;
|
|
25
|
+
}
|
|
26
|
+
/** Entry field names backing each neutral field. */
|
|
27
|
+
export type SeoFieldNames = Record<keyof SeoFields, string>;
|
|
28
|
+
/**
|
|
29
|
+
* The recommended SEO field names. `defineSeoFieldGroup()` emits a schema group using exactly
|
|
30
|
+
* these names, so an adopter who uses both ends needs no configuration at all.
|
|
31
|
+
*/
|
|
32
|
+
export declare const DEFAULT_SEO_FIELD_NAMES: {
|
|
33
|
+
readonly title: "metaTitle";
|
|
34
|
+
readonly description: "metaDescription";
|
|
35
|
+
readonly ogImage: "ogImage";
|
|
36
|
+
readonly ogType: "ogType";
|
|
37
|
+
readonly canonical: "canonical";
|
|
38
|
+
readonly noindex: "noindex";
|
|
39
|
+
readonly twitterCard: "twitterCard";
|
|
40
|
+
};
|
|
41
|
+
export interface ExtractSeoFieldsOptions {
|
|
42
|
+
/** Per-field name overrides (e.g. `{ title: 'seoTitle' }`). */
|
|
43
|
+
fields?: Partial<SeoFieldNames>;
|
|
44
|
+
/**
|
|
45
|
+
* Read the fields from a nested object under this key instead of from flat entry fields.
|
|
46
|
+
* Pair with `defineSeoFieldGroup({ group: 'seo' })`, which nests the same fields under that
|
|
47
|
+
* key in the content file. Omit for the flat (inline-group) convention, which is the default
|
|
48
|
+
* on both ends.
|
|
49
|
+
*/
|
|
50
|
+
group?: string;
|
|
51
|
+
/** Used when the entry has no meta title (typically the entry's own title/heading). */
|
|
52
|
+
fallbackTitle?: string;
|
|
53
|
+
/** Used when the entry has no meta description. */
|
|
54
|
+
fallbackDescription?: string;
|
|
55
|
+
}
|
|
56
|
+
/** Just the parts of `ExtractSeoFieldsOptions` that locate the fields, with no fallbacks. */
|
|
57
|
+
export type SeoFieldLocation = Pick<ExtractSeoFieldsOptions, 'fields' | 'group'>;
|
|
58
|
+
/**
|
|
59
|
+
* Map an entry's SEO fields onto the neutral shape.
|
|
60
|
+
*
|
|
61
|
+
* Entry data is `unknown` — every read is guarded, unknown enum values are dropped rather than
|
|
62
|
+
* passed through, and non-object data (null, a string, an array) yields an all-undefined result
|
|
63
|
+
* rather than throwing.
|
|
64
|
+
*/
|
|
65
|
+
export declare function extractSeoFields(entryData: unknown, opts?: ExtractSeoFieldsOptions): SeoFields;
|
|
66
|
+
/**
|
|
67
|
+
* THE `noindex` predicate. Every surface that suppresses an entry must route through this one
|
|
68
|
+
* function rather than re-deriving the flag:
|
|
69
|
+
*
|
|
70
|
+
* - the page's own `robots: { index: false }` (canopycms-next's `entryToMetadata`), and
|
|
71
|
+
* - exclusion from the sitemap (canopycms-next's `generateContentSitemap`).
|
|
72
|
+
*
|
|
73
|
+
* Those two shipping from one predicate is the point of shipping them together. Where they were
|
|
74
|
+
* derived separately, a `noindex` entry stayed advertised in one surface while correctly absent
|
|
75
|
+
* from the other, and nothing warned.
|
|
76
|
+
*
|
|
77
|
+
* NOT applied to enumeration: `noindex` means "don't advertise this", not "don't build this".
|
|
78
|
+
* `collectRoutableEntries` and `generateStaticParams` must still emit the entry, so its URL
|
|
79
|
+
* resolves for anyone holding the link.
|
|
80
|
+
*/
|
|
81
|
+
export declare function isNoindexEntry(entryData: unknown, opts?: SeoFieldLocation): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* An off-site URL: either scheme-qualified (`https://…`) or LITERAL protocol-relative
|
|
84
|
+
* (`//cdn…`). Both name a host we don't control, so neither may be rewritten against the site
|
|
85
|
+
* origin — `resolveSeoUrl` passes a `true` result through verbatim.
|
|
86
|
+
*
|
|
87
|
+
* Deliberately narrower than "resolves off-origin": a WHATWG-backslash-equivalent spelling
|
|
88
|
+
* (`/\evil.com`, `\\evil.com`, `\/evil.com` — see `utils/sanitize-href.ts`'s
|
|
89
|
+
* `isImplicitlyOffOrigin`) also resolves off-origin in a browser, but is NOT recognized here as
|
|
90
|
+
* an intentional off-site pointer the way a literal `//cdn…` is. `resolveSeoUrl` handles that
|
|
91
|
+
* case separately, by neutralizing (`neutralizeImplicitOffOrigin`) rather than passing through: a
|
|
92
|
+
* `false` result here isn't a guarantee the value is a safe site-relative path on its own, only
|
|
93
|
+
* that it isn't a *declared* off-site one.
|
|
94
|
+
*/
|
|
95
|
+
export declare function isAbsoluteUrl(url: string): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Strip trailing slashes without a regex.
|
|
98
|
+
*
|
|
99
|
+
* The obvious `replace(/\/+$/, '')` is a polynomial-ReDoS shape (CodeQL
|
|
100
|
+
* `js/polynomial-redos`, flagged high): on a value that is mostly slashes but does not end in
|
|
101
|
+
* one, the engine retries `\/+` from every position and the match cost is quadratic in the
|
|
102
|
+
* input length. `siteUrl` is adopter-supplied and can reach here from config or an env var, so
|
|
103
|
+
* it counts as uncontrolled. A character scan is linear and needs no reasoning about
|
|
104
|
+
* backtracking.
|
|
105
|
+
*
|
|
106
|
+
* Exported (from `canopycms/server`) so adopter code normalizing its own site-origin env var —
|
|
107
|
+
* e.g. a `SITE_URL` constant built from `NEXT_PUBLIC_SITE_URL` — has a package-provided linear
|
|
108
|
+
* way to do it instead of reaching for the same regex this function replaced.
|
|
109
|
+
*/
|
|
110
|
+
export declare function stripTrailingSlashes(value: string): string;
|
|
111
|
+
/**
|
|
112
|
+
* Append a trailing slash to a site-relative path, matching a site that serves `/contact/`.
|
|
113
|
+
*
|
|
114
|
+
* Leaves the root (`/`) and file-like paths (a last segment containing a dot, e.g.
|
|
115
|
+
* `/blog/rss.xml`) alone, and never doubles an existing slash.
|
|
116
|
+
*
|
|
117
|
+
* A query string and/or fragment (`?page=2`, `#section`) is split off BEFORE the slash decision
|
|
118
|
+
* and placement, then reattached after — so `/blog?page=2` becomes `/blog/?page=2`, never
|
|
119
|
+
* `/blog?page=2/` (a literal trailing slash inside the query string, which is not what "serve
|
|
120
|
+
* with a trailing slash" means and breaks the URL).
|
|
121
|
+
*/
|
|
122
|
+
export declare function withTrailingSlash(path: string): string;
|
|
123
|
+
export interface ResolveSeoUrlOptions {
|
|
124
|
+
/** Site origin (e.g. `https://example.com`). Trailing slashes are stripped. */
|
|
125
|
+
siteUrl?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Emit site-relative paths with a trailing slash. CanopyCMS has no knowledge of your
|
|
128
|
+
* framework's routing config, so this must be stated explicitly — see the note on
|
|
129
|
+
* `generateContentSitemap`.
|
|
130
|
+
*/
|
|
131
|
+
trailingSlash?: boolean;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a possibly-relative URL for public emission (canonical tag, sitemap URL, OG image).
|
|
135
|
+
*
|
|
136
|
+
* ORDER MATTERS — the absolute check runs FIRST. An absolute or protocol-relative value is a
|
|
137
|
+
* deliberate off-site pointer (syndication, a partner-hosted copy, a CDN image) and passes
|
|
138
|
+
* through verbatim; only a site-relative path gets trailing-slash normalization and the site
|
|
139
|
+
* origin. Normalizing first turns `https://other.org/page` into
|
|
140
|
+
* `<siteUrl>/https://other.org/page/`.
|
|
141
|
+
*
|
|
142
|
+
* With no `siteUrl`, a site-relative path is returned still relative (normalized), which is
|
|
143
|
+
* what you want when the framework resolves relative metadata URLs itself.
|
|
144
|
+
*
|
|
145
|
+
* A NOT-absolute value is also run through `neutralizeImplicitOffOrigin` before normalization,
|
|
146
|
+
* so a WHATWG-backslash-equivalent spelling that `isAbsoluteUrl` correctly declines to call
|
|
147
|
+
* "absolute" (see its doc) can't still be read by a browser as protocol-relative once emitted.
|
|
148
|
+
*/
|
|
149
|
+
export declare function resolveSeoUrl(pathOrUrl: string, opts?: ResolveSeoUrlOptions): string;
|
|
150
|
+
//# sourceMappingURL=seo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../../src/static/seo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,uDAAuD;AACvD,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAA;AAEzD,4DAA4D;AAC5D,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,qBAAqB,CAAA;AAE9D,iDAAiD;AACjD,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,WAAW,CAAC,EAAE,cAAc,CAAA;CAC7B;AAED,oDAAoD;AACpD,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,SAAS,EAAE,MAAM,CAAC,CAAA;AAE3D;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;CAQF,CAAA;AAElC,MAAM,WAAW,uBAAuB;IACtC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;IAC/B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uFAAuF;IACvF,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mDAAmD;IACnD,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED,6FAA6F;AAC7F,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,uBAAuB,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAA;AA6ChF;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,OAAO,EAClB,IAAI,GAAE,uBAA4B,GACjC,SAAS,CAcX;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAEvF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,oBAAyB,GAAG,MAAM,CAUxF"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic SEO field extraction and URL shaping.
|
|
3
|
+
*
|
|
4
|
+
* This module is pure: it maps an entry's raw data onto a neutral `SeoFields` shape and
|
|
5
|
+
* resolves URLs. Framework adapters (e.g. canopycms-next's `entryToMetadata`) turn that shape
|
|
6
|
+
* into their own metadata objects; nothing here imports a framework or a node built-in.
|
|
7
|
+
*
|
|
8
|
+
* The field names default to the recommended SEO field group — see `defineSeoFieldGroup` in
|
|
9
|
+
* entry-schema.ts, which builds a schema group from these same names so the schema side and
|
|
10
|
+
* the read side cannot drift.
|
|
11
|
+
*/
|
|
12
|
+
import { neutralizeImplicitOffOrigin } from '../utils/sanitize-href.js';
|
|
13
|
+
/**
|
|
14
|
+
* The recommended SEO field names. `defineSeoFieldGroup()` emits a schema group using exactly
|
|
15
|
+
* these names, so an adopter who uses both ends needs no configuration at all.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_SEO_FIELD_NAMES = {
|
|
18
|
+
title: 'metaTitle',
|
|
19
|
+
description: 'metaDescription',
|
|
20
|
+
ogImage: 'ogImage',
|
|
21
|
+
ogType: 'ogType',
|
|
22
|
+
canonical: 'canonical',
|
|
23
|
+
noindex: 'noindex',
|
|
24
|
+
twitterCard: 'twitterCard',
|
|
25
|
+
};
|
|
26
|
+
const asRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
27
|
+
? value
|
|
28
|
+
: undefined;
|
|
29
|
+
/**
|
|
30
|
+
* A non-empty trimmed string, or undefined.
|
|
31
|
+
*
|
|
32
|
+
* Empty is treated as UNSET, deliberately: CanopyCMS writes optional fields present-but-empty
|
|
33
|
+
* rather than omitting them, so `metaTitle: ''` is what an untouched SEO group looks like on
|
|
34
|
+
* disk. Treating it as a value would let a blank field beat the fallback title on every entry
|
|
35
|
+
* whose author never opened the SEO section.
|
|
36
|
+
*/
|
|
37
|
+
const asText = (value) => {
|
|
38
|
+
if (typeof value !== 'string')
|
|
39
|
+
return undefined;
|
|
40
|
+
const trimmed = value.trim();
|
|
41
|
+
return trimmed === '' ? undefined : trimmed;
|
|
42
|
+
};
|
|
43
|
+
const asFlag = (value) => typeof value === 'boolean' ? value : undefined;
|
|
44
|
+
/**
|
|
45
|
+
* An image URL from either convention: a plain string field, or an `image` field, whose value
|
|
46
|
+
* is a structured `{ src, alt, ... }` object. Supporting both means switching the recommended
|
|
47
|
+
* `ogImage` field to `type: 'image'` doesn't silently drop every OG image.
|
|
48
|
+
*/
|
|
49
|
+
const asImageUrl = (value) => {
|
|
50
|
+
const text = asText(value);
|
|
51
|
+
if (text)
|
|
52
|
+
return text;
|
|
53
|
+
return asText(asRecord(value)?.src);
|
|
54
|
+
};
|
|
55
|
+
const asOgType = (value) => {
|
|
56
|
+
const text = asText(value);
|
|
57
|
+
return text === 'website' || text === 'article' || text === 'profile' ? text : undefined;
|
|
58
|
+
};
|
|
59
|
+
const asTwitterCard = (value) => {
|
|
60
|
+
const text = asText(value);
|
|
61
|
+
return text === 'summary' || text === 'summary_large_image' ? text : undefined;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Map an entry's SEO fields onto the neutral shape.
|
|
65
|
+
*
|
|
66
|
+
* Entry data is `unknown` — every read is guarded, unknown enum values are dropped rather than
|
|
67
|
+
* passed through, and non-object data (null, a string, an array) yields an all-undefined result
|
|
68
|
+
* rather than throwing.
|
|
69
|
+
*/
|
|
70
|
+
export function extractSeoFields(entryData, opts = {}) {
|
|
71
|
+
const names = { ...DEFAULT_SEO_FIELD_NAMES, ...opts.fields };
|
|
72
|
+
const entry = asRecord(entryData);
|
|
73
|
+
const source = opts.group ? asRecord(entry?.[opts.group]) : entry;
|
|
74
|
+
return {
|
|
75
|
+
title: asText(source?.[names.title]) ?? asText(opts.fallbackTitle),
|
|
76
|
+
description: asText(source?.[names.description]) ?? asText(opts.fallbackDescription),
|
|
77
|
+
ogImage: asImageUrl(source?.[names.ogImage]),
|
|
78
|
+
ogType: asOgType(source?.[names.ogType]),
|
|
79
|
+
canonical: asText(source?.[names.canonical]),
|
|
80
|
+
noindex: asFlag(source?.[names.noindex]),
|
|
81
|
+
twitterCard: asTwitterCard(source?.[names.twitterCard]),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* THE `noindex` predicate. Every surface that suppresses an entry must route through this one
|
|
86
|
+
* function rather than re-deriving the flag:
|
|
87
|
+
*
|
|
88
|
+
* - the page's own `robots: { index: false }` (canopycms-next's `entryToMetadata`), and
|
|
89
|
+
* - exclusion from the sitemap (canopycms-next's `generateContentSitemap`).
|
|
90
|
+
*
|
|
91
|
+
* Those two shipping from one predicate is the point of shipping them together. Where they were
|
|
92
|
+
* derived separately, a `noindex` entry stayed advertised in one surface while correctly absent
|
|
93
|
+
* from the other, and nothing warned.
|
|
94
|
+
*
|
|
95
|
+
* NOT applied to enumeration: `noindex` means "don't advertise this", not "don't build this".
|
|
96
|
+
* `collectRoutableEntries` and `generateStaticParams` must still emit the entry, so its URL
|
|
97
|
+
* resolves for anyone holding the link.
|
|
98
|
+
*/
|
|
99
|
+
export function isNoindexEntry(entryData, opts = {}) {
|
|
100
|
+
return extractSeoFields(entryData, opts).noindex === true;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* An off-site URL: either scheme-qualified (`https://…`) or LITERAL protocol-relative
|
|
104
|
+
* (`//cdn…`). Both name a host we don't control, so neither may be rewritten against the site
|
|
105
|
+
* origin — `resolveSeoUrl` passes a `true` result through verbatim.
|
|
106
|
+
*
|
|
107
|
+
* Deliberately narrower than "resolves off-origin": a WHATWG-backslash-equivalent spelling
|
|
108
|
+
* (`/\evil.com`, `\\evil.com`, `\/evil.com` — see `utils/sanitize-href.ts`'s
|
|
109
|
+
* `isImplicitlyOffOrigin`) also resolves off-origin in a browser, but is NOT recognized here as
|
|
110
|
+
* an intentional off-site pointer the way a literal `//cdn…` is. `resolveSeoUrl` handles that
|
|
111
|
+
* case separately, by neutralizing (`neutralizeImplicitOffOrigin`) rather than passing through: a
|
|
112
|
+
* `false` result here isn't a guarantee the value is a safe site-relative path on its own, only
|
|
113
|
+
* that it isn't a *declared* off-site one.
|
|
114
|
+
*/
|
|
115
|
+
export function isAbsoluteUrl(url) {
|
|
116
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith('//');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Strip trailing slashes without a regex.
|
|
120
|
+
*
|
|
121
|
+
* The obvious `replace(/\/+$/, '')` is a polynomial-ReDoS shape (CodeQL
|
|
122
|
+
* `js/polynomial-redos`, flagged high): on a value that is mostly slashes but does not end in
|
|
123
|
+
* one, the engine retries `\/+` from every position and the match cost is quadratic in the
|
|
124
|
+
* input length. `siteUrl` is adopter-supplied and can reach here from config or an env var, so
|
|
125
|
+
* it counts as uncontrolled. A character scan is linear and needs no reasoning about
|
|
126
|
+
* backtracking.
|
|
127
|
+
*
|
|
128
|
+
* Exported (from `canopycms/server`) so adopter code normalizing its own site-origin env var —
|
|
129
|
+
* e.g. a `SITE_URL` constant built from `NEXT_PUBLIC_SITE_URL` — has a package-provided linear
|
|
130
|
+
* way to do it instead of reaching for the same regex this function replaced.
|
|
131
|
+
*/
|
|
132
|
+
export function stripTrailingSlashes(value) {
|
|
133
|
+
let end = value.length;
|
|
134
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47 /* '/' */)
|
|
135
|
+
end--;
|
|
136
|
+
return value.slice(0, end);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Append a trailing slash to a site-relative path, matching a site that serves `/contact/`.
|
|
140
|
+
*
|
|
141
|
+
* Leaves the root (`/`) and file-like paths (a last segment containing a dot, e.g.
|
|
142
|
+
* `/blog/rss.xml`) alone, and never doubles an existing slash.
|
|
143
|
+
*
|
|
144
|
+
* A query string and/or fragment (`?page=2`, `#section`) is split off BEFORE the slash decision
|
|
145
|
+
* and placement, then reattached after — so `/blog?page=2` becomes `/blog/?page=2`, never
|
|
146
|
+
* `/blog?page=2/` (a literal trailing slash inside the query string, which is not what "serve
|
|
147
|
+
* with a trailing slash" means and breaks the URL).
|
|
148
|
+
*/
|
|
149
|
+
export function withTrailingSlash(path) {
|
|
150
|
+
const splitIndex = path.search(/[?#]/);
|
|
151
|
+
const base = splitIndex === -1 ? path : path.slice(0, splitIndex);
|
|
152
|
+
const suffix = splitIndex === -1 ? '' : path.slice(splitIndex);
|
|
153
|
+
const withLeading = base.startsWith('/') ? base : `/${base}`;
|
|
154
|
+
if (withLeading === '/' || withLeading.endsWith('/'))
|
|
155
|
+
return withLeading + suffix;
|
|
156
|
+
const lastSegment = withLeading.slice(withLeading.lastIndexOf('/') + 1);
|
|
157
|
+
if (lastSegment.includes('.'))
|
|
158
|
+
return withLeading + suffix;
|
|
159
|
+
return `${withLeading}/${suffix}`;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Resolve a possibly-relative URL for public emission (canonical tag, sitemap URL, OG image).
|
|
163
|
+
*
|
|
164
|
+
* ORDER MATTERS — the absolute check runs FIRST. An absolute or protocol-relative value is a
|
|
165
|
+
* deliberate off-site pointer (syndication, a partner-hosted copy, a CDN image) and passes
|
|
166
|
+
* through verbatim; only a site-relative path gets trailing-slash normalization and the site
|
|
167
|
+
* origin. Normalizing first turns `https://other.org/page` into
|
|
168
|
+
* `<siteUrl>/https://other.org/page/`.
|
|
169
|
+
*
|
|
170
|
+
* With no `siteUrl`, a site-relative path is returned still relative (normalized), which is
|
|
171
|
+
* what you want when the framework resolves relative metadata URLs itself.
|
|
172
|
+
*
|
|
173
|
+
* A NOT-absolute value is also run through `neutralizeImplicitOffOrigin` before normalization,
|
|
174
|
+
* so a WHATWG-backslash-equivalent spelling that `isAbsoluteUrl` correctly declines to call
|
|
175
|
+
* "absolute" (see its doc) can't still be read by a browser as protocol-relative once emitted.
|
|
176
|
+
*/
|
|
177
|
+
export function resolveSeoUrl(pathOrUrl, opts = {}) {
|
|
178
|
+
if (isAbsoluteUrl(pathOrUrl))
|
|
179
|
+
return pathOrUrl;
|
|
180
|
+
const safePathOrUrl = neutralizeImplicitOffOrigin(pathOrUrl);
|
|
181
|
+
const normalized = opts.trailingSlash
|
|
182
|
+
? withTrailingSlash(safePathOrUrl)
|
|
183
|
+
: safePathOrUrl.startsWith('/')
|
|
184
|
+
? safePathOrUrl
|
|
185
|
+
: `/${safePathOrUrl}`;
|
|
186
|
+
const origin = opts.siteUrl === undefined ? undefined : stripTrailingSlashes(opts.siteUrl);
|
|
187
|
+
return origin ? origin + normalized : normalized;
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=seo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo.js","sourceRoot":"","sources":["../../src/static/seo.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -124,6 +124,16 @@ export interface WorkerStatusReport {
|
|
|
124
124
|
durationMs: number;
|
|
125
125
|
rebased: string[];
|
|
126
126
|
skippedDirty: string[];
|
|
127
|
+
/**
|
|
128
|
+
* [SYNC-C1] Branches skipped because a content write held the branch's
|
|
129
|
+
* cross-host content-write lock (utils/content-write-lock.ts) -- the
|
|
130
|
+
* worker yields and retries next cycle.
|
|
131
|
+
*
|
|
132
|
+
* Optional for the same reason as `tracked` below: absent from a
|
|
133
|
+
* worker-status.json written by a worker predating that lock. Readers must
|
|
134
|
+
* tolerate its absence.
|
|
135
|
+
*/
|
|
136
|
+
skippedLocked?: string[];
|
|
127
137
|
failed: {
|
|
128
138
|
branch: string;
|
|
129
139
|
error: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [SYNC-C1] Cross-host mutual exclusion between the worker's rebase loop and
|
|
3
|
+
* content writes against the same branch working tree.
|
|
4
|
+
*
|
|
5
|
+
* Production runs a Lambda (API) and an EC2 worker over one EFS filesystem.
|
|
6
|
+
* `ContentStore`'s write/delete/rename critical sections were guarded only by
|
|
7
|
+
* the in-process mutex (`utils/async-mutex`), which does not cross that
|
|
8
|
+
* boundary, while `CmsWorker.rebaseActiveBranches()` rewrites the very same
|
|
9
|
+
* working tree. A write landing after `git rebase` started is destroyed two
|
|
10
|
+
* ways -- `git checkout --theirs <file>` overwrites it with the branch's
|
|
11
|
+
* committed version (and the rebase then SUCCEEDS, logging nothing), and
|
|
12
|
+
* `git rebase --abort` hard-resets the tree. Either way the editor already got
|
|
13
|
+
* its 200. This lock is what serializes the two.
|
|
14
|
+
*
|
|
15
|
+
* **Asymmetric by design.** The worker retries every branch on its next sync
|
|
16
|
+
* cycle (~5 minutes) while the editor is a person waiting on a save, so the
|
|
17
|
+
* worker yields and the writer waits:
|
|
18
|
+
*
|
|
19
|
+
* - Worker: {@link tryAcquireContentWriteLock} -- zero retries. On contention
|
|
20
|
+
* it skips that branch this cycle (an extension of the existing
|
|
21
|
+
* skip-dirty-branches behavior) and picks it up next cycle.
|
|
22
|
+
* - Writers: {@link withContentWriteLock} -- a short bounded wait, then a
|
|
23
|
+
* retriable failure the API surfaces as a 409.
|
|
24
|
+
*
|
|
25
|
+
* **Reads never take this lock.** An extra EFS round-trip on every read is not
|
|
26
|
+
* an acceptable price, and a read racing a rebase gets a consistent-enough
|
|
27
|
+
* older or newer file, never a destroyed one.
|
|
28
|
+
*
|
|
29
|
+
* ## Why the lock is anchored on `{branchRoot}/.canopy-meta`
|
|
30
|
+
*
|
|
31
|
+
* proper-lockfile keys its module-level `locks{}` bookkeeping (refresh timer,
|
|
32
|
+
* release function) by the **target path** passed to `lock()`, NOT by
|
|
33
|
+
* `lockfilePath`. The provisioning lock targets `path.dirname(branchRoot)` --
|
|
34
|
+
* the shared content-branches directory -- so anchoring here on that same
|
|
35
|
+
* directory with a different lock NAME would land on the same registry key:
|
|
36
|
+
* one process holding both (a Lambda container provisioning a workspace and
|
|
37
|
+
* writing content) would have the second acquisition clobber the first's
|
|
38
|
+
* bookkeeping. Targeting `{branchRoot}/.canopy-meta` instead gives:
|
|
39
|
+
*
|
|
40
|
+
* - a distinct in-process registry key from the provisioning lock (and from
|
|
41
|
+
* every other branch's content lock -- the provisioning locks already share
|
|
42
|
+
* one key across branches);
|
|
43
|
+
* - a distinct on-disk lock directory, so the server-enforced mkdir cannot
|
|
44
|
+
* collide either; and
|
|
45
|
+
* - no deadlock potential: the two locks are never both required, and the only
|
|
46
|
+
* possible acquisition order (provision, then write) is consistent.
|
|
47
|
+
*
|
|
48
|
+
* `.canopy-meta/` is git-excluded in every branch clone (`ensureGitExclude`),
|
|
49
|
+
* so the lock directory can never dirty the working tree or be swept into
|
|
50
|
+
* `git add .` at publish time.
|
|
51
|
+
*
|
|
52
|
+
* ## Honest caveat (do not oversell this)
|
|
53
|
+
*
|
|
54
|
+
* `proper-lockfile` decides a holder is dead by reading the lock directory's
|
|
55
|
+
* mtime with `fs.stat`, which on EFS is served through the NFS attribute
|
|
56
|
+
* cache. A live holder refreshes every `stale/2`, but a waiter can read a
|
|
57
|
+
* cached mtime and conclude the lock is stale when it is not, taking it over.
|
|
58
|
+
* The failure mode of a bad takeover is exactly today's behavior (two
|
|
59
|
+
* unsynchronized writers), so this is a strict improvement -- not a proof of
|
|
60
|
+
* mutual exclusion. See docs/concurrency.md.
|
|
61
|
+
*/
|
|
62
|
+
/** On-disk name of the lock marker (a directory, created by mkdir). */
|
|
63
|
+
export declare const CONTENT_WRITE_LOCK_NAME = "content-write.lock";
|
|
64
|
+
/**
|
|
65
|
+
* Default bounded wait for a content write.
|
|
66
|
+
*
|
|
67
|
+
* Deliberately short. A rebase holds this lock for a fetch plus a replay plus
|
|
68
|
+
* N conflict rounds of git subprocesses on EFS -- far longer than any wait an
|
|
69
|
+
* interactive save can absorb, and longer than the API's own latency budget.
|
|
70
|
+
* So the wait is not sized to outlast a rebase (nothing reasonable could); it
|
|
71
|
+
* is sized to absorb lock handoff and the short holds of other writers, and to
|
|
72
|
+
* turn everything longer into a fast, explicit "retry" the editor can act on
|
|
73
|
+
* rather than a request that hangs toward the Lambda timeout. The worker
|
|
74
|
+
* revisits the branch next cycle, so the blocked state is transient.
|
|
75
|
+
*
|
|
76
|
+
* This is ALSO the writer-vs-writer budget, not only the rebase one. The lock
|
|
77
|
+
* is per-branch-root, so every write to one branch now serializes behind it
|
|
78
|
+
* where in-process serialization used to be per-entry -- bounded on Lambda
|
|
79
|
+
* (one invocation per container), but real under `next dev` and build-time
|
|
80
|
+
* provisioning across worker processes. The case to watch is a write whose
|
|
81
|
+
* in-lock path triggers a full `idIndex()` rescan of a large tree over EFS:
|
|
82
|
+
* that can plausibly exceed this budget and start 409ing unrelated saves.
|
|
83
|
+
* Making it configurable for prod tuning is tracked in
|
|
84
|
+
* .claude/future-tasks/content-write-lock-tuning-and-granularity.md.
|
|
85
|
+
*/
|
|
86
|
+
export declare const DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2000;
|
|
87
|
+
/**
|
|
88
|
+
* Thrown when the bounded wait expires with the branch's content lock still
|
|
89
|
+
* held. Retriable -- callers translate this into a 409 with a message that
|
|
90
|
+
* says so.
|
|
91
|
+
*
|
|
92
|
+
* The message says "syncing OR another save" because BOTH produce it. The
|
|
93
|
+
* lock is taken by `write`/`delete`/`renameEntry` and by the admin
|
|
94
|
+
* repair-content-duplicates action as well as by the worker's rebase, so a
|
|
95
|
+
* message naming only the rebase was a confident claim about something that
|
|
96
|
+
* may not be happening. `api/content.ts` routes this error ahead of the
|
|
97
|
+
* generic conflict specifically so the editor sees this wording, which makes
|
|
98
|
+
* the wording load-bearing rather than cosmetic.
|
|
99
|
+
*/
|
|
100
|
+
export declare class ContentWriteLockBusyError extends Error {
|
|
101
|
+
constructor(message?: string);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Acquire the branch's content-write lock WITHOUT waiting.
|
|
105
|
+
*
|
|
106
|
+
* Throws an error with `code === 'ELOCKED'` when a live holder has it. Used by
|
|
107
|
+
* the worker's rebase loop (which skips the branch and retries next cycle) and
|
|
108
|
+
* by tests standing in for a writer.
|
|
109
|
+
*/
|
|
110
|
+
export declare function tryAcquireContentWriteLock(branchRoot: string): Promise<() => Promise<void>>;
|
|
111
|
+
/**
|
|
112
|
+
* Acquire the branch's content-write lock with a short bounded wait.
|
|
113
|
+
*
|
|
114
|
+
* Retries ONLY on genuine contention (`ELOCKED`), the same discipline
|
|
115
|
+
* `withOccFileLock` uses: proper-lockfile's own retry loop retries blindly on
|
|
116
|
+
* any error, which would burn the whole budget re-hitting e.g. ENOENT after
|
|
117
|
+
* the branch directory was deleted out from under the caller.
|
|
118
|
+
*
|
|
119
|
+
* @throws ContentWriteLockBusyError when the budget expires under contention.
|
|
120
|
+
*/
|
|
121
|
+
export declare function acquireContentWriteLock(branchRoot: string, waitMs?: number): Promise<() => Promise<void>>;
|
|
122
|
+
/**
|
|
123
|
+
* Run `fn` holding the branch's content-write lock, releasing it in a
|
|
124
|
+
* `finally` so a throw can never strand it.
|
|
125
|
+
*/
|
|
126
|
+
export declare function withContentWriteLock<T>(branchRoot: string, fn: () => Promise<T>, waitMs?: number): Promise<T>;
|
|
127
|
+
//# sourceMappingURL=content-write-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-write-lock.d.ts","sourceRoot":"","sources":["../../src/utils/content-write-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AAUH,uEAAuE;AACvE,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAE3D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,kCAAkC,OAAO,CAAA;AAEtD;;;;;;;;;;;;GAYG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBAEhD,OAAO,SAAuI;CAKjJ;AAYD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAE3F;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAA2C,GAClD,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAe9B;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,MAAM,GAAE,MAA2C,GAClD,OAAO,CAAC,CAAC,CAAC,CAOZ"}
|