@weatherboard/cms 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -5
- package/dist/contracts/query.d.cts +1 -1
- package/dist/contracts/query.d.mts +1 -1
- package/dist/contracts/query.d.ts +1 -1
- package/dist/contracts/text.d.cts +5 -3
- package/dist/contracts/text.d.mts +5 -3
- package/dist/contracts/text.d.ts +5 -3
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +3 -3
- package/dist/next.cjs +1 -1
- package/dist/next.cjs.map +2 -2
- package/dist/next.mjs +1 -1
- package/dist/next.mjs.map +2 -2
- package/dist/portable/sections.d.cts +136 -0
- package/dist/portable/sections.d.mts +136 -0
- package/dist/portable/sections.d.ts +136 -0
- package/dist/portable/site-content.d.cts +13 -0
- package/dist/portable/site-content.d.mts +13 -0
- package/dist/portable/site-content.d.ts +13 -0
- package/dist/sanity.cjs.map +1 -1
- package/dist/sanity.mjs.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +2 -2
- package/dist/server.mjs +1 -1
- package/dist/server.mjs.map +2 -2
- package/dist/site-content.cjs +3 -3
- package/dist/site-content.cjs.map +4 -4
- package/dist/site-content.d.cts +3 -1
- package/dist/site-content.d.mts +3 -1
- package/dist/site-content.d.ts +3 -1
- package/dist/site-content.mjs +3 -3
- package/dist/site-content.mjs.map +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A-1915 (A-1739 M1c). The reader's half of the shared section set that
|
|
3
|
+
* `@weatherboard/sanity-schema-base` declares in `src/sections.ts` (A-1889):
|
|
4
|
+
* `page.sections[]`, drawn from the four home section types plus
|
|
5
|
+
* `textMediaBlock`, `faqBlock` (items `faqItem`), `galleryBlock` and
|
|
6
|
+
* `contactBlock`.
|
|
7
|
+
*
|
|
8
|
+
* ===========================================================================
|
|
9
|
+
* LAYOUTS ARE THE A-1876 VARIANT NAMES, EXACTLY
|
|
10
|
+
* ===========================================================================
|
|
11
|
+
*
|
|
12
|
+
* The three lists below are the composition variant names, spelled as A-1876
|
|
13
|
+
* spells them, so a contract test can import them and compare them with the
|
|
14
|
+
* UI package's. A missing or unknown stored `layout` resolves to the
|
|
15
|
+
* composition's default and records `section.layout.defaulted`, the rule cms
|
|
16
|
+
* 0.2.0 set for `siteHero.layout`. It never throws and never yields an empty
|
|
17
|
+
* section.
|
|
18
|
+
*
|
|
19
|
+
* ===========================================================================
|
|
20
|
+
* WARNINGS, NOT REFUSALS, FOR THREE THINGS
|
|
21
|
+
* ===========================================================================
|
|
22
|
+
*
|
|
23
|
+
* The rest of this reader refuses a malformed document by name. Three things
|
|
24
|
+
* in a section are warnings instead, each recorded with its path:
|
|
25
|
+
*
|
|
26
|
+
* section.layout.defaulted missing or unknown layout: the default renders
|
|
27
|
+
* section.type.unknown a section `_type` this reader does not know is
|
|
28
|
+
* skipped, so a newer model never breaks a page
|
|
29
|
+
* section.image.missing-alt an image without alt text is DROPPED, never
|
|
30
|
+
* rendered without it
|
|
31
|
+
* section.image.missing-asset an image with no uploaded asset is dropped
|
|
32
|
+
* section.empty.skipped a section left with nothing to show (a gallery
|
|
33
|
+
* whose every photo was dropped, a contact
|
|
34
|
+
* section switched off and empty) is skipped
|
|
35
|
+
*/
|
|
36
|
+
import type { ActionInput, CompositionBlockInput, CopyInput } from "./site-content.cjs";
|
|
37
|
+
/** `Split`: text with media. */
|
|
38
|
+
export declare const SPLIT_LAYOUTS: readonly ["media-end", "media-start", "media-below"];
|
|
39
|
+
/** `HeadingList`: a heading column and a list column. */
|
|
40
|
+
export declare const HEADING_LIST_LAYOUTS: readonly ["beside", "stacked"];
|
|
41
|
+
/** `StaggerRow`: a row of two to four items. */
|
|
42
|
+
export declare const STAGGER_ROW_LAYOUTS: readonly ["staggered", "aligned"];
|
|
43
|
+
export type SplitLayout = (typeof SPLIT_LAYOUTS)[number];
|
|
44
|
+
export type HeadingListLayout = (typeof HEADING_LIST_LAYOUTS)[number];
|
|
45
|
+
export type StaggerRowLayout = (typeof STAGGER_ROW_LAYOUTS)[number];
|
|
46
|
+
/** What a missing or unknown `layout` resolves to, per composition. */
|
|
47
|
+
export declare const SECTION_LAYOUT_DEFAULTS: {
|
|
48
|
+
readonly Split: "media-end";
|
|
49
|
+
readonly HeadingList: "beside";
|
|
50
|
+
readonly StaggerRow: "staggered";
|
|
51
|
+
};
|
|
52
|
+
/** Sanity `_type` on the left, the reader's `kind` on the right; the same rows as schema-base's `SECTION_CONTRACTS`. */
|
|
53
|
+
export declare const READER_SECTION_KINDS: {
|
|
54
|
+
readonly featureStoriesBlock: "feature_stories";
|
|
55
|
+
readonly surfaceCardBlock: "cta";
|
|
56
|
+
readonly accentBandBlock: "accent_band";
|
|
57
|
+
readonly locationsBlock: "locations";
|
|
58
|
+
readonly textMediaBlock: "text_media";
|
|
59
|
+
readonly faqBlock: "faq";
|
|
60
|
+
readonly galleryBlock: "gallery";
|
|
61
|
+
readonly contactBlock: "contact";
|
|
62
|
+
};
|
|
63
|
+
/** A gallery is a `StaggerRow`, which holds two to four items. */
|
|
64
|
+
export declare const READER_GALLERY_PHOTO_MIN = 2;
|
|
65
|
+
export declare const READER_GALLERY_PHOTO_LIMIT = 4;
|
|
66
|
+
export declare const SECTION_WARNINGS: {
|
|
67
|
+
readonly layoutDefaulted: "section.layout.defaulted";
|
|
68
|
+
readonly unknownType: "section.type.unknown";
|
|
69
|
+
readonly imageMissingAlt: "section.image.missing-alt";
|
|
70
|
+
readonly imageMissingAsset: "section.image.missing-asset";
|
|
71
|
+
readonly emptySkipped: "section.empty.skipped";
|
|
72
|
+
};
|
|
73
|
+
export type SectionWarningCode = (typeof SECTION_WARNINGS)[keyof typeof SECTION_WARNINGS];
|
|
74
|
+
/** A named warning and the path in the stored document it is about. Never copy. */
|
|
75
|
+
export type SectionWarning = {
|
|
76
|
+
readonly code: SectionWarningCode;
|
|
77
|
+
readonly where: string;
|
|
78
|
+
};
|
|
79
|
+
/** A Sanity image as a site needs it. `asset` is the image asset's id; `alt` is never empty. */
|
|
80
|
+
export type SectionImageInput = {
|
|
81
|
+
readonly asset: string;
|
|
82
|
+
readonly alt: string;
|
|
83
|
+
readonly caption: string | null;
|
|
84
|
+
readonly hotspot: {
|
|
85
|
+
readonly x: number;
|
|
86
|
+
readonly y: number;
|
|
87
|
+
} | null;
|
|
88
|
+
};
|
|
89
|
+
export type TextMediaSectionInput = {
|
|
90
|
+
readonly kind: "text_media";
|
|
91
|
+
readonly anchor: string | null;
|
|
92
|
+
readonly layout: SplitLayout;
|
|
93
|
+
readonly heading: CopyInput;
|
|
94
|
+
readonly tail: CopyInput | null;
|
|
95
|
+
readonly body: CopyInput;
|
|
96
|
+
readonly action: ActionInput | null;
|
|
97
|
+
/** `null` only when the stored photo was dropped (no alt text or no asset); a warning names it. */
|
|
98
|
+
readonly image: SectionImageInput | null;
|
|
99
|
+
};
|
|
100
|
+
export type FaqSectionInput = {
|
|
101
|
+
readonly kind: "faq";
|
|
102
|
+
readonly anchor: string | null;
|
|
103
|
+
readonly layout: HeadingListLayout;
|
|
104
|
+
readonly heading: CopyInput;
|
|
105
|
+
readonly tail: CopyInput | null;
|
|
106
|
+
readonly intro: CopyInput | null;
|
|
107
|
+
readonly items: readonly {
|
|
108
|
+
readonly question: CopyInput;
|
|
109
|
+
readonly answer: CopyInput;
|
|
110
|
+
}[];
|
|
111
|
+
};
|
|
112
|
+
export type GallerySectionInput = {
|
|
113
|
+
readonly kind: "gallery";
|
|
114
|
+
readonly anchor: string | null;
|
|
115
|
+
readonly layout: StaggerRowLayout;
|
|
116
|
+
readonly heading: CopyInput | null;
|
|
117
|
+
readonly images: readonly SectionImageInput[];
|
|
118
|
+
};
|
|
119
|
+
export type ContactSectionInput = {
|
|
120
|
+
readonly kind: "contact";
|
|
121
|
+
readonly anchor: string | null;
|
|
122
|
+
readonly layout: SplitLayout;
|
|
123
|
+
readonly heading: CopyInput;
|
|
124
|
+
readonly body: CopyInput | null;
|
|
125
|
+
/** Show the site settings' email address. */
|
|
126
|
+
readonly showEmail: boolean;
|
|
127
|
+
/** Show the site settings' phone number. */
|
|
128
|
+
readonly showPhone: boolean;
|
|
129
|
+
readonly action: ActionInput | null;
|
|
130
|
+
readonly image: SectionImageInput | null;
|
|
131
|
+
};
|
|
132
|
+
export type NewSectionInput = TextMediaSectionInput | FaqSectionInput | GallerySectionInput | ContactSectionInput;
|
|
133
|
+
/** One entry of `PageContent.sections`: a home section kind or one of the new kinds. */
|
|
134
|
+
export type PageSectionInput = Exclude<CompositionBlockInput, {
|
|
135
|
+
readonly kind: "campaign";
|
|
136
|
+
}> | NewSectionInput;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A-1915 (A-1739 M1c). The reader's half of the shared section set that
|
|
3
|
+
* `@weatherboard/sanity-schema-base` declares in `src/sections.ts` (A-1889):
|
|
4
|
+
* `page.sections[]`, drawn from the four home section types plus
|
|
5
|
+
* `textMediaBlock`, `faqBlock` (items `faqItem`), `galleryBlock` and
|
|
6
|
+
* `contactBlock`.
|
|
7
|
+
*
|
|
8
|
+
* ===========================================================================
|
|
9
|
+
* LAYOUTS ARE THE A-1876 VARIANT NAMES, EXACTLY
|
|
10
|
+
* ===========================================================================
|
|
11
|
+
*
|
|
12
|
+
* The three lists below are the composition variant names, spelled as A-1876
|
|
13
|
+
* spells them, so a contract test can import them and compare them with the
|
|
14
|
+
* UI package's. A missing or unknown stored `layout` resolves to the
|
|
15
|
+
* composition's default and records `section.layout.defaulted`, the rule cms
|
|
16
|
+
* 0.2.0 set for `siteHero.layout`. It never throws and never yields an empty
|
|
17
|
+
* section.
|
|
18
|
+
*
|
|
19
|
+
* ===========================================================================
|
|
20
|
+
* WARNINGS, NOT REFUSALS, FOR THREE THINGS
|
|
21
|
+
* ===========================================================================
|
|
22
|
+
*
|
|
23
|
+
* The rest of this reader refuses a malformed document by name. Three things
|
|
24
|
+
* in a section are warnings instead, each recorded with its path:
|
|
25
|
+
*
|
|
26
|
+
* section.layout.defaulted missing or unknown layout: the default renders
|
|
27
|
+
* section.type.unknown a section `_type` this reader does not know is
|
|
28
|
+
* skipped, so a newer model never breaks a page
|
|
29
|
+
* section.image.missing-alt an image without alt text is DROPPED, never
|
|
30
|
+
* rendered without it
|
|
31
|
+
* section.image.missing-asset an image with no uploaded asset is dropped
|
|
32
|
+
* section.empty.skipped a section left with nothing to show (a gallery
|
|
33
|
+
* whose every photo was dropped, a contact
|
|
34
|
+
* section switched off and empty) is skipped
|
|
35
|
+
*/
|
|
36
|
+
import type { ActionInput, CompositionBlockInput, CopyInput } from "./site-content.mjs";
|
|
37
|
+
/** `Split`: text with media. */
|
|
38
|
+
export declare const SPLIT_LAYOUTS: readonly ["media-end", "media-start", "media-below"];
|
|
39
|
+
/** `HeadingList`: a heading column and a list column. */
|
|
40
|
+
export declare const HEADING_LIST_LAYOUTS: readonly ["beside", "stacked"];
|
|
41
|
+
/** `StaggerRow`: a row of two to four items. */
|
|
42
|
+
export declare const STAGGER_ROW_LAYOUTS: readonly ["staggered", "aligned"];
|
|
43
|
+
export type SplitLayout = (typeof SPLIT_LAYOUTS)[number];
|
|
44
|
+
export type HeadingListLayout = (typeof HEADING_LIST_LAYOUTS)[number];
|
|
45
|
+
export type StaggerRowLayout = (typeof STAGGER_ROW_LAYOUTS)[number];
|
|
46
|
+
/** What a missing or unknown `layout` resolves to, per composition. */
|
|
47
|
+
export declare const SECTION_LAYOUT_DEFAULTS: {
|
|
48
|
+
readonly Split: "media-end";
|
|
49
|
+
readonly HeadingList: "beside";
|
|
50
|
+
readonly StaggerRow: "staggered";
|
|
51
|
+
};
|
|
52
|
+
/** Sanity `_type` on the left, the reader's `kind` on the right; the same rows as schema-base's `SECTION_CONTRACTS`. */
|
|
53
|
+
export declare const READER_SECTION_KINDS: {
|
|
54
|
+
readonly featureStoriesBlock: "feature_stories";
|
|
55
|
+
readonly surfaceCardBlock: "cta";
|
|
56
|
+
readonly accentBandBlock: "accent_band";
|
|
57
|
+
readonly locationsBlock: "locations";
|
|
58
|
+
readonly textMediaBlock: "text_media";
|
|
59
|
+
readonly faqBlock: "faq";
|
|
60
|
+
readonly galleryBlock: "gallery";
|
|
61
|
+
readonly contactBlock: "contact";
|
|
62
|
+
};
|
|
63
|
+
/** A gallery is a `StaggerRow`, which holds two to four items. */
|
|
64
|
+
export declare const READER_GALLERY_PHOTO_MIN = 2;
|
|
65
|
+
export declare const READER_GALLERY_PHOTO_LIMIT = 4;
|
|
66
|
+
export declare const SECTION_WARNINGS: {
|
|
67
|
+
readonly layoutDefaulted: "section.layout.defaulted";
|
|
68
|
+
readonly unknownType: "section.type.unknown";
|
|
69
|
+
readonly imageMissingAlt: "section.image.missing-alt";
|
|
70
|
+
readonly imageMissingAsset: "section.image.missing-asset";
|
|
71
|
+
readonly emptySkipped: "section.empty.skipped";
|
|
72
|
+
};
|
|
73
|
+
export type SectionWarningCode = (typeof SECTION_WARNINGS)[keyof typeof SECTION_WARNINGS];
|
|
74
|
+
/** A named warning and the path in the stored document it is about. Never copy. */
|
|
75
|
+
export type SectionWarning = {
|
|
76
|
+
readonly code: SectionWarningCode;
|
|
77
|
+
readonly where: string;
|
|
78
|
+
};
|
|
79
|
+
/** A Sanity image as a site needs it. `asset` is the image asset's id; `alt` is never empty. */
|
|
80
|
+
export type SectionImageInput = {
|
|
81
|
+
readonly asset: string;
|
|
82
|
+
readonly alt: string;
|
|
83
|
+
readonly caption: string | null;
|
|
84
|
+
readonly hotspot: {
|
|
85
|
+
readonly x: number;
|
|
86
|
+
readonly y: number;
|
|
87
|
+
} | null;
|
|
88
|
+
};
|
|
89
|
+
export type TextMediaSectionInput = {
|
|
90
|
+
readonly kind: "text_media";
|
|
91
|
+
readonly anchor: string | null;
|
|
92
|
+
readonly layout: SplitLayout;
|
|
93
|
+
readonly heading: CopyInput;
|
|
94
|
+
readonly tail: CopyInput | null;
|
|
95
|
+
readonly body: CopyInput;
|
|
96
|
+
readonly action: ActionInput | null;
|
|
97
|
+
/** `null` only when the stored photo was dropped (no alt text or no asset); a warning names it. */
|
|
98
|
+
readonly image: SectionImageInput | null;
|
|
99
|
+
};
|
|
100
|
+
export type FaqSectionInput = {
|
|
101
|
+
readonly kind: "faq";
|
|
102
|
+
readonly anchor: string | null;
|
|
103
|
+
readonly layout: HeadingListLayout;
|
|
104
|
+
readonly heading: CopyInput;
|
|
105
|
+
readonly tail: CopyInput | null;
|
|
106
|
+
readonly intro: CopyInput | null;
|
|
107
|
+
readonly items: readonly {
|
|
108
|
+
readonly question: CopyInput;
|
|
109
|
+
readonly answer: CopyInput;
|
|
110
|
+
}[];
|
|
111
|
+
};
|
|
112
|
+
export type GallerySectionInput = {
|
|
113
|
+
readonly kind: "gallery";
|
|
114
|
+
readonly anchor: string | null;
|
|
115
|
+
readonly layout: StaggerRowLayout;
|
|
116
|
+
readonly heading: CopyInput | null;
|
|
117
|
+
readonly images: readonly SectionImageInput[];
|
|
118
|
+
};
|
|
119
|
+
export type ContactSectionInput = {
|
|
120
|
+
readonly kind: "contact";
|
|
121
|
+
readonly anchor: string | null;
|
|
122
|
+
readonly layout: SplitLayout;
|
|
123
|
+
readonly heading: CopyInput;
|
|
124
|
+
readonly body: CopyInput | null;
|
|
125
|
+
/** Show the site settings' email address. */
|
|
126
|
+
readonly showEmail: boolean;
|
|
127
|
+
/** Show the site settings' phone number. */
|
|
128
|
+
readonly showPhone: boolean;
|
|
129
|
+
readonly action: ActionInput | null;
|
|
130
|
+
readonly image: SectionImageInput | null;
|
|
131
|
+
};
|
|
132
|
+
export type NewSectionInput = TextMediaSectionInput | FaqSectionInput | GallerySectionInput | ContactSectionInput;
|
|
133
|
+
/** One entry of `PageContent.sections`: a home section kind or one of the new kinds. */
|
|
134
|
+
export type PageSectionInput = Exclude<CompositionBlockInput, {
|
|
135
|
+
readonly kind: "campaign";
|
|
136
|
+
}> | NewSectionInput;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A-1915 (A-1739 M1c). The reader's half of the shared section set that
|
|
3
|
+
* `@weatherboard/sanity-schema-base` declares in `src/sections.ts` (A-1889):
|
|
4
|
+
* `page.sections[]`, drawn from the four home section types plus
|
|
5
|
+
* `textMediaBlock`, `faqBlock` (items `faqItem`), `galleryBlock` and
|
|
6
|
+
* `contactBlock`.
|
|
7
|
+
*
|
|
8
|
+
* ===========================================================================
|
|
9
|
+
* LAYOUTS ARE THE A-1876 VARIANT NAMES, EXACTLY
|
|
10
|
+
* ===========================================================================
|
|
11
|
+
*
|
|
12
|
+
* The three lists below are the composition variant names, spelled as A-1876
|
|
13
|
+
* spells them, so a contract test can import them and compare them with the
|
|
14
|
+
* UI package's. A missing or unknown stored `layout` resolves to the
|
|
15
|
+
* composition's default and records `section.layout.defaulted`, the rule cms
|
|
16
|
+
* 0.2.0 set for `siteHero.layout`. It never throws and never yields an empty
|
|
17
|
+
* section.
|
|
18
|
+
*
|
|
19
|
+
* ===========================================================================
|
|
20
|
+
* WARNINGS, NOT REFUSALS, FOR THREE THINGS
|
|
21
|
+
* ===========================================================================
|
|
22
|
+
*
|
|
23
|
+
* The rest of this reader refuses a malformed document by name. Three things
|
|
24
|
+
* in a section are warnings instead, each recorded with its path:
|
|
25
|
+
*
|
|
26
|
+
* section.layout.defaulted missing or unknown layout: the default renders
|
|
27
|
+
* section.type.unknown a section `_type` this reader does not know is
|
|
28
|
+
* skipped, so a newer model never breaks a page
|
|
29
|
+
* section.image.missing-alt an image without alt text is DROPPED, never
|
|
30
|
+
* rendered without it
|
|
31
|
+
* section.image.missing-asset an image with no uploaded asset is dropped
|
|
32
|
+
* section.empty.skipped a section left with nothing to show (a gallery
|
|
33
|
+
* whose every photo was dropped, a contact
|
|
34
|
+
* section switched off and empty) is skipped
|
|
35
|
+
*/
|
|
36
|
+
import type { ActionInput, CompositionBlockInput, CopyInput } from "./site-content.js";
|
|
37
|
+
/** `Split`: text with media. */
|
|
38
|
+
export declare const SPLIT_LAYOUTS: readonly ["media-end", "media-start", "media-below"];
|
|
39
|
+
/** `HeadingList`: a heading column and a list column. */
|
|
40
|
+
export declare const HEADING_LIST_LAYOUTS: readonly ["beside", "stacked"];
|
|
41
|
+
/** `StaggerRow`: a row of two to four items. */
|
|
42
|
+
export declare const STAGGER_ROW_LAYOUTS: readonly ["staggered", "aligned"];
|
|
43
|
+
export type SplitLayout = (typeof SPLIT_LAYOUTS)[number];
|
|
44
|
+
export type HeadingListLayout = (typeof HEADING_LIST_LAYOUTS)[number];
|
|
45
|
+
export type StaggerRowLayout = (typeof STAGGER_ROW_LAYOUTS)[number];
|
|
46
|
+
/** What a missing or unknown `layout` resolves to, per composition. */
|
|
47
|
+
export declare const SECTION_LAYOUT_DEFAULTS: {
|
|
48
|
+
readonly Split: "media-end";
|
|
49
|
+
readonly HeadingList: "beside";
|
|
50
|
+
readonly StaggerRow: "staggered";
|
|
51
|
+
};
|
|
52
|
+
/** Sanity `_type` on the left, the reader's `kind` on the right; the same rows as schema-base's `SECTION_CONTRACTS`. */
|
|
53
|
+
export declare const READER_SECTION_KINDS: {
|
|
54
|
+
readonly featureStoriesBlock: "feature_stories";
|
|
55
|
+
readonly surfaceCardBlock: "cta";
|
|
56
|
+
readonly accentBandBlock: "accent_band";
|
|
57
|
+
readonly locationsBlock: "locations";
|
|
58
|
+
readonly textMediaBlock: "text_media";
|
|
59
|
+
readonly faqBlock: "faq";
|
|
60
|
+
readonly galleryBlock: "gallery";
|
|
61
|
+
readonly contactBlock: "contact";
|
|
62
|
+
};
|
|
63
|
+
/** A gallery is a `StaggerRow`, which holds two to four items. */
|
|
64
|
+
export declare const READER_GALLERY_PHOTO_MIN = 2;
|
|
65
|
+
export declare const READER_GALLERY_PHOTO_LIMIT = 4;
|
|
66
|
+
export declare const SECTION_WARNINGS: {
|
|
67
|
+
readonly layoutDefaulted: "section.layout.defaulted";
|
|
68
|
+
readonly unknownType: "section.type.unknown";
|
|
69
|
+
readonly imageMissingAlt: "section.image.missing-alt";
|
|
70
|
+
readonly imageMissingAsset: "section.image.missing-asset";
|
|
71
|
+
readonly emptySkipped: "section.empty.skipped";
|
|
72
|
+
};
|
|
73
|
+
export type SectionWarningCode = (typeof SECTION_WARNINGS)[keyof typeof SECTION_WARNINGS];
|
|
74
|
+
/** A named warning and the path in the stored document it is about. Never copy. */
|
|
75
|
+
export type SectionWarning = {
|
|
76
|
+
readonly code: SectionWarningCode;
|
|
77
|
+
readonly where: string;
|
|
78
|
+
};
|
|
79
|
+
/** A Sanity image as a site needs it. `asset` is the image asset's id; `alt` is never empty. */
|
|
80
|
+
export type SectionImageInput = {
|
|
81
|
+
readonly asset: string;
|
|
82
|
+
readonly alt: string;
|
|
83
|
+
readonly caption: string | null;
|
|
84
|
+
readonly hotspot: {
|
|
85
|
+
readonly x: number;
|
|
86
|
+
readonly y: number;
|
|
87
|
+
} | null;
|
|
88
|
+
};
|
|
89
|
+
export type TextMediaSectionInput = {
|
|
90
|
+
readonly kind: "text_media";
|
|
91
|
+
readonly anchor: string | null;
|
|
92
|
+
readonly layout: SplitLayout;
|
|
93
|
+
readonly heading: CopyInput;
|
|
94
|
+
readonly tail: CopyInput | null;
|
|
95
|
+
readonly body: CopyInput;
|
|
96
|
+
readonly action: ActionInput | null;
|
|
97
|
+
/** `null` only when the stored photo was dropped (no alt text or no asset); a warning names it. */
|
|
98
|
+
readonly image: SectionImageInput | null;
|
|
99
|
+
};
|
|
100
|
+
export type FaqSectionInput = {
|
|
101
|
+
readonly kind: "faq";
|
|
102
|
+
readonly anchor: string | null;
|
|
103
|
+
readonly layout: HeadingListLayout;
|
|
104
|
+
readonly heading: CopyInput;
|
|
105
|
+
readonly tail: CopyInput | null;
|
|
106
|
+
readonly intro: CopyInput | null;
|
|
107
|
+
readonly items: readonly {
|
|
108
|
+
readonly question: CopyInput;
|
|
109
|
+
readonly answer: CopyInput;
|
|
110
|
+
}[];
|
|
111
|
+
};
|
|
112
|
+
export type GallerySectionInput = {
|
|
113
|
+
readonly kind: "gallery";
|
|
114
|
+
readonly anchor: string | null;
|
|
115
|
+
readonly layout: StaggerRowLayout;
|
|
116
|
+
readonly heading: CopyInput | null;
|
|
117
|
+
readonly images: readonly SectionImageInput[];
|
|
118
|
+
};
|
|
119
|
+
export type ContactSectionInput = {
|
|
120
|
+
readonly kind: "contact";
|
|
121
|
+
readonly anchor: string | null;
|
|
122
|
+
readonly layout: SplitLayout;
|
|
123
|
+
readonly heading: CopyInput;
|
|
124
|
+
readonly body: CopyInput | null;
|
|
125
|
+
/** Show the site settings' email address. */
|
|
126
|
+
readonly showEmail: boolean;
|
|
127
|
+
/** Show the site settings' phone number. */
|
|
128
|
+
readonly showPhone: boolean;
|
|
129
|
+
readonly action: ActionInput | null;
|
|
130
|
+
readonly image: SectionImageInput | null;
|
|
131
|
+
};
|
|
132
|
+
export type NewSectionInput = TextMediaSectionInput | FaqSectionInput | GallerySectionInput | ContactSectionInput;
|
|
133
|
+
/** One entry of `PageContent.sections`: a home section kind or one of the new kinds. */
|
|
134
|
+
export type PageSectionInput = Exclude<CompositionBlockInput, {
|
|
135
|
+
readonly kind: "campaign";
|
|
136
|
+
}> | NewSectionInput;
|
|
@@ -54,6 +54,11 @@
|
|
|
54
54
|
* accentBandBlock kind `accent_band`
|
|
55
55
|
* locationsBlock kind `locations` (jumpLink → anchor)
|
|
56
56
|
*
|
|
57
|
+
* A content page's `sections[]` (A-1915) reads those four kinds plus
|
|
58
|
+
* `text_media`, `faq`, `gallery` and `contact` into `PageContent.sections`,
|
|
59
|
+
* with their named warnings in `PageContent.warnings`. `sections.ts` holds the
|
|
60
|
+
* kinds, the A-1876 layout names and the warning codes.
|
|
61
|
+
*
|
|
57
62
|
* Two places the composition cannot carry a proposed marker: a `cta` block's
|
|
58
63
|
* plain strings, and the campaign's title and body (which travel as Markdown).
|
|
59
64
|
* There the text is returned plain and the line is still listed in
|
|
@@ -61,6 +66,7 @@
|
|
|
61
66
|
* approval. Nothing is dropped silently.
|
|
62
67
|
*/
|
|
63
68
|
import { type CmsQuery } from "../contracts/query.cjs";
|
|
69
|
+
import { type PageSectionInput, type SectionWarning } from "./sections.cjs";
|
|
64
70
|
export type CopyInput = string | {
|
|
65
71
|
readonly text: string;
|
|
66
72
|
readonly copySource: "captured" | "proposed";
|
|
@@ -142,6 +148,13 @@ export type PageContent = {
|
|
|
142
148
|
readonly body: string;
|
|
143
149
|
readonly migrated: boolean;
|
|
144
150
|
readonly proposedCopy: readonly ProposedCopy[];
|
|
151
|
+
/**
|
|
152
|
+
* A-1915. Present only when the stored page has a `sections` list, so a page
|
|
153
|
+
* written before sections existed reads exactly as it did in 0.2.0.
|
|
154
|
+
*/
|
|
155
|
+
readonly sections?: readonly PageSectionInput[];
|
|
156
|
+
/** A-1915. Present with `sections`: every named section warning, with its path. */
|
|
157
|
+
readonly warnings?: readonly SectionWarning[];
|
|
145
158
|
};
|
|
146
159
|
export type SiteSettingsContent = {
|
|
147
160
|
readonly name: string;
|
|
@@ -54,6 +54,11 @@
|
|
|
54
54
|
* accentBandBlock kind `accent_band`
|
|
55
55
|
* locationsBlock kind `locations` (jumpLink → anchor)
|
|
56
56
|
*
|
|
57
|
+
* A content page's `sections[]` (A-1915) reads those four kinds plus
|
|
58
|
+
* `text_media`, `faq`, `gallery` and `contact` into `PageContent.sections`,
|
|
59
|
+
* with their named warnings in `PageContent.warnings`. `sections.ts` holds the
|
|
60
|
+
* kinds, the A-1876 layout names and the warning codes.
|
|
61
|
+
*
|
|
57
62
|
* Two places the composition cannot carry a proposed marker: a `cta` block's
|
|
58
63
|
* plain strings, and the campaign's title and body (which travel as Markdown).
|
|
59
64
|
* There the text is returned plain and the line is still listed in
|
|
@@ -61,6 +66,7 @@
|
|
|
61
66
|
* approval. Nothing is dropped silently.
|
|
62
67
|
*/
|
|
63
68
|
import { type CmsQuery } from "../contracts/query.mjs";
|
|
69
|
+
import { type PageSectionInput, type SectionWarning } from "./sections.mjs";
|
|
64
70
|
export type CopyInput = string | {
|
|
65
71
|
readonly text: string;
|
|
66
72
|
readonly copySource: "captured" | "proposed";
|
|
@@ -142,6 +148,13 @@ export type PageContent = {
|
|
|
142
148
|
readonly body: string;
|
|
143
149
|
readonly migrated: boolean;
|
|
144
150
|
readonly proposedCopy: readonly ProposedCopy[];
|
|
151
|
+
/**
|
|
152
|
+
* A-1915. Present only when the stored page has a `sections` list, so a page
|
|
153
|
+
* written before sections existed reads exactly as it did in 0.2.0.
|
|
154
|
+
*/
|
|
155
|
+
readonly sections?: readonly PageSectionInput[];
|
|
156
|
+
/** A-1915. Present with `sections`: every named section warning, with its path. */
|
|
157
|
+
readonly warnings?: readonly SectionWarning[];
|
|
145
158
|
};
|
|
146
159
|
export type SiteSettingsContent = {
|
|
147
160
|
readonly name: string;
|
|
@@ -54,6 +54,11 @@
|
|
|
54
54
|
* accentBandBlock kind `accent_band`
|
|
55
55
|
* locationsBlock kind `locations` (jumpLink → anchor)
|
|
56
56
|
*
|
|
57
|
+
* A content page's `sections[]` (A-1915) reads those four kinds plus
|
|
58
|
+
* `text_media`, `faq`, `gallery` and `contact` into `PageContent.sections`,
|
|
59
|
+
* with their named warnings in `PageContent.warnings`. `sections.ts` holds the
|
|
60
|
+
* kinds, the A-1876 layout names and the warning codes.
|
|
61
|
+
*
|
|
57
62
|
* Two places the composition cannot carry a proposed marker: a `cta` block's
|
|
58
63
|
* plain strings, and the campaign's title and body (which travel as Markdown).
|
|
59
64
|
* There the text is returned plain and the line is still listed in
|
|
@@ -61,6 +66,7 @@
|
|
|
61
66
|
* approval. Nothing is dropped silently.
|
|
62
67
|
*/
|
|
63
68
|
import { type CmsQuery } from "../contracts/query.js";
|
|
69
|
+
import { type PageSectionInput, type SectionWarning } from "./sections.js";
|
|
64
70
|
export type CopyInput = string | {
|
|
65
71
|
readonly text: string;
|
|
66
72
|
readonly copySource: "captured" | "proposed";
|
|
@@ -142,6 +148,13 @@ export type PageContent = {
|
|
|
142
148
|
readonly body: string;
|
|
143
149
|
readonly migrated: boolean;
|
|
144
150
|
readonly proposedCopy: readonly ProposedCopy[];
|
|
151
|
+
/**
|
|
152
|
+
* A-1915. Present only when the stored page has a `sections` list, so a page
|
|
153
|
+
* written before sections existed reads exactly as it did in 0.2.0.
|
|
154
|
+
*/
|
|
155
|
+
readonly sections?: readonly PageSectionInput[];
|
|
156
|
+
/** A-1915. Present with `sections`: every named section warning, with its path. */
|
|
157
|
+
readonly warnings?: readonly SectionWarning[];
|
|
145
158
|
};
|
|
146
159
|
export type SiteSettingsContent = {
|
|
147
160
|
readonly name: string;
|
package/dist/sanity.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/sanity.ts", "../src/contracts/query.ts", "../src/adapters/sanity/adapter.ts"],
|
|
4
|
-
"sourcesContent": ["import \"server-only\";\nexport { createSanityAdapter, sanityBindingFromEnv } from \"./adapters/sanity/adapter.js\";\nexport type { SanityBinding, SanityAdapterOptions } from \"./adapters/sanity/adapter.js\";\n", "/** A-1822 S1 owns this implementation. S0 registers the public signatures. */\nexport type CmsParams = Readonly<Record<string, string | number | boolean | null>>;\nexport type CmsDependencies = {\n readonly types: readonly string[];\n readonly ids?: readonly string[];\n};\nexport type CmsResolution<T> = {\n readonly data: T;\n readonly fallbackFields: readonly string[];\n readonly warnings?: readonly \"siteHero.layout.defaulted\"[];\n};\nexport type CmsQuery<T> = {\n readonly name: string;\n readonly query: string;\n readonly params: CmsParams;\n readonly dependencies: CmsDependencies;\n readonly resolve: (raw: unknown, fallback: T) => CmsResolution<T>;\n};\n\nconst TYPE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;\nconst ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;\nconst SCOPE = /^[a-z0-9]{6,12}\\.[a-z][a-z0-9_-]{0,63}$/;\n\nfunction dependenciesCopy(dependencies: CmsDependencies): CmsDependencies {\n if (!dependencies || !Array.isArray(dependencies.types) || dependencies.types.length === 0) {\n throw new TypeError(\"CMS dependencies require document types\");\n }\n const types = [...dependencies.types];\n for (const type of types) {\n if (typeof type !== \"string\" || !TYPE.test(type)) throw new TypeError(\"Invalid CMS document type\");\n }\n if (dependencies.ids !== undefined && !Array.isArray(dependencies.ids)) throw new TypeError(\"Invalid CMS document IDs\");\n const ids = dependencies.ids === undefined ? undefined : [...dependencies.ids];\n for (const id of ids ?? []) {\n if (typeof id !== \"string\" || !ID.test(id) || /^(drafts|versions)\\./.test(id)) {\n throw new TypeError(\"Invalid CMS document ID\");\n }\n }\n if (new Set(types).size + new Set(ids).size > 128) throw new TypeError(\"Too many CMS tags\");\n return Object.freeze({ types: Object.freeze(types), ...(ids === undefined ? {} : { ids: Object.freeze(ids) }) });\n}\n\nexport function defineCmsQuery<T>(query: CmsQuery<T>): CmsQuery<T> {\n if (!query || typeof query.name !== \"string\" || !/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(query.name)\n || typeof query.query !== \"string\" || query.query.trim() === \"\" || typeof query.resolve !== \"function\") {\n throw new TypeError(\"Invalid CMS query configuration\");\n }\n const params = query.params;\n if (!params || typeof params !== \"object\" || (Object.getPrototypeOf(params) !== Object.prototype && Object.getPrototypeOf(params) !== null)) {\n throw new TypeError(\"CMS params must be plain JSON scalars\");\n }\n const copied: Record<string, string | number | boolean | null> = {};\n for (const key of Reflect.ownKeys(params)) {\n const descriptor = Object.getOwnPropertyDescriptor(params, key);\n if (typeof key !== \"string\" || !descriptor || !Object.hasOwn(descriptor, \"value\") || !descriptor.enumerable) {\n throw new TypeError(\"CMS params must be plain JSON scalars\");\n }\n const value: unknown = descriptor.value;\n if (!(value === null || typeof value === \"string\" || typeof value === \"boolean\" || (typeof value === \"number\" && Number.isFinite(value)))) {\n throw new TypeError(\"CMS params must be plain JSON scalars\");\n }\n Object.defineProperty(copied, key, { value, enumerable: true });\n }\n return Object.freeze({ name: query.name, query: query.query, params: Object.freeze(copied), dependencies: dependenciesCopy(query.dependencies), resolve: query.resolve });\n}\n\nexport function cmsTags(scope: string, dependencies: CmsDependencies): readonly string[] {\n if (typeof scope !== \"string\" || scope.length > 77 || !SCOPE.test(scope)) throw new TypeError(\"Invalid CMS scope\");\n const copied = dependenciesCopy(dependencies);\n const tags = [\n ...[...new Set(copied.types)].sort().map((type) => `cms:${scope}:type:${type}`),\n ...[...new Set(copied.ids ?? [])].sort().map((id) => `cms:${scope}:id:${id}`),\n ];\n return Object.freeze(tags);\n}\n", "import type { CmsAdapter, CmsFetch } from \"../../runtime/reader.js\";\nimport { cmsTags, defineCmsQuery } from \"../../contracts/query.js\";\n\nexport type SanityBinding = {\n readonly projectId: string;\n readonly dataset: string;\n readonly apiVersion: string;\n};\nexport type SanityAdapterOptions = {\n readonly binding: SanityBinding;\n readonly readToken?: string;\n readonly fetchImpl?: CmsFetch;\n readonly timeoutMs?: number;\n};\n\nconst RESPONSE_LIMIT = 2 * 1024 * 1024;\n\nfunction bindingCopy(binding: SanityBinding): SanityBinding {\n if (!binding || typeof binding.projectId !== \"string\" || !/^[a-z0-9]{6,12}$/.test(binding.projectId)\n || typeof binding.dataset !== \"string\" || !/^[a-z][a-z0-9_-]{0,63}$/.test(binding.dataset)\n || typeof binding.apiVersion !== \"string\" || !/^v?\\d{4}-\\d{2}-\\d{2}$/.test(binding.apiVersion)) {\n throw new TypeError(\"Invalid Sanity binding configuration\");\n }\n return Object.freeze({ projectId: binding.projectId, dataset: binding.dataset, apiVersion: binding.apiVersion.replace(/^v/, \"\") });\n}\n\n/** Applies policy via fetch's Next metadata. Standard fetch has no persistent cache or HTML invalidation. */\nexport function createSanityAdapter(options: SanityAdapterOptions): CmsAdapter {\n const binding = bindingCopy(options?.binding);\n const timeoutMs = options.timeoutMs ?? 1500;\n const token = options.readToken;\n if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647\n || (options.fetchImpl !== undefined && typeof options.fetchImpl !== \"function\")\n || (token !== undefined && (typeof token !== \"string\" || token.trim() === \"\" || /[\\r\\n]/.test(token)))) {\n throw new TypeError(\"Invalid Sanity adapter configuration\");\n }\n const fetchImpl = options.fetchImpl;\n const scope = `${binding.projectId}.${binding.dataset}`;\n return Object.freeze({\n scope,\n async read(query, policy) {\n // Direct structural callers receive the same configuration refusals.\n let validated;\n try {\n if (!policy || !Number.isSafeInteger(policy.revalidateSeconds) || policy.revalidateSeconds < 3600 || !Array.isArray(policy.tags)) throw new TypeError();\n const types: string[] = [];\n const ids: string[] = [];\n for (const tag of policy.tags) {\n if (typeof tag !== \"string\") throw new TypeError();\n if (tag.startsWith(`cms:${scope}:type:`)) types.push(tag.slice(`cms:${scope}:type:`.length));\n else if (tag.startsWith(`cms:${scope}:id:`)) ids.push(tag.slice(`cms:${scope}:id:`.length));\n else throw new TypeError();\n }\n const expected = cmsTags(scope, { types, ids });\n if (expected.length !== policy.tags.length || expected.some((tag, i) => tag !== policy.tags[i])) throw new TypeError();\n validated = defineCmsQuery({ ...query, dependencies: { types, ids }, resolve: (raw: unknown) => ({ data: raw, fallbackFields: [] }) });\n } catch { throw new TypeError(\"Invalid Sanity read configuration\"); }\n const url = new URL(`https://${binding.projectId}.api.sanity.io/v${binding.apiVersion}/data/query/${binding.dataset}`);\n url.searchParams.set(\"query\", validated.query);\n url.searchParams.set(\"perspective\", \"published\");\n for (const key of Object.keys(validated.params).sort()) url.searchParams.set(`$${key}`, JSON.stringify(validated.params[key]));\n const controller = new AbortController();\n let bodyReader: ReadableStreamDefaultReader<Uint8Array> | undefined;\n let expired = false;\n const cancel = (): void => {\n controller.abort();\n if (bodyReader) void bodyReader.cancel().catch(() => {});\n };\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<Awaited<ReturnType<CmsAdapter[\"read\"]>>>((resolve) => {\n timer = setTimeout(() => {\n expired = true;\n cancel();\n resolve({ ok: false, reason: \"timeout\" });\n }, timeoutMs);\n });\n const work = async (): Promise<Awaited<ReturnType<CmsAdapter[\"read\"]>>> => {\n try {\n // Look up global fetch at read time so Next's patched fetch is retained.\n const response = await (fetchImpl ?? globalThis.fetch)(url.href, {\n method: \"GET\", redirect: \"error\", signal: controller.signal,\n headers: token === undefined ? {} : { Authorization: `Bearer ${token}` },\n next: { revalidate: policy.revalidateSeconds, tags: [...policy.tags] },\n });\n if (expired) { void response.body?.cancel().catch(() => {}); return { ok: false, reason: \"timeout\" }; }\n if (!response.ok) {\n void response.body?.cancel().catch(() => {});\n const status = response.status;\n const reason = status === 402 ? \"plan-limit\" : status === 401 || status === 403 ? \"permission\"\n : status === 429 ? \"rate-limit\" : \"upstream\";\n return { ok: false, reason, status };\n }\n if (!response.body) return { ok: false, reason: \"malformed\" };\n bodyReader = response.body.getReader();\n let bytes = 0;\n let text = \"\";\n const decoder = new TextDecoder(\"utf-8\", { fatal: true });\n while (true) {\n const chunk = await bodyReader.read();\n if (expired) return { ok: false, reason: \"timeout\" };\n if (chunk.done) break;\n bytes += chunk.value.byteLength;\n if (bytes > RESPONSE_LIMIT) { cancel(); return { ok: false, reason: \"malformed\" }; }\n try { text += decoder.decode(chunk.value, { stream: true }); }\n catch { cancel(); return { ok: false, reason: \"malformed\" }; }\n }\n try {\n text += decoder.decode();\n const envelope: unknown = JSON.parse(text);\n if (!envelope || typeof envelope !== \"object\" || Array.isArray(envelope) || !Object.hasOwn(envelope, \"result\")) return { ok: false, reason: \"malformed\" };\n return { ok: true, value: (envelope as Record<string, unknown>).result };\n } catch { return { ok: false, reason: \"malformed\" }; }\n } catch { return { ok: false, reason: expired ? \"timeout\" : \"network\" }; }\n };\n try { return await Promise.race([work(), deadline]); }\n finally { clearTimeout(timer); }\n },\n } satisfies CmsAdapter);\n}\n\nexport function sanityBindingFromEnv(env: Readonly<Record<string, string | undefined>>): SanityBinding {\n return bindingCopy({ projectId: env.SANITY_PROJECT_ID, dataset: env.SANITY_DATASET, apiVersion: env.SANITY_API_VERSION } as SanityBinding);\n}\n"],
|
|
4
|
+
"sourcesContent": ["import \"server-only\";\nexport { createSanityAdapter, sanityBindingFromEnv } from \"./adapters/sanity/adapter.js\";\nexport type { SanityBinding, SanityAdapterOptions } from \"./adapters/sanity/adapter.js\";\n", "/** A-1822 S1 owns this implementation. S0 registers the public signatures. */\nexport type CmsParams = Readonly<Record<string, string | number | boolean | null>>;\nexport type CmsDependencies = {\n readonly types: readonly string[];\n readonly ids?: readonly string[];\n};\nexport type CmsResolution<T> = {\n readonly data: T;\n readonly fallbackFields: readonly string[];\n readonly warnings?: readonly (\"siteHero.layout.defaulted\" | \"siteHero.tagline.dropped\" | \"siteHero.taglines.defaulted\")[];\n};\nexport type CmsQuery<T> = {\n readonly name: string;\n readonly query: string;\n readonly params: CmsParams;\n readonly dependencies: CmsDependencies;\n readonly resolve: (raw: unknown, fallback: T) => CmsResolution<T>;\n};\n\nconst TYPE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;\nconst ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;\nconst SCOPE = /^[a-z0-9]{6,12}\\.[a-z][a-z0-9_-]{0,63}$/;\n\nfunction dependenciesCopy(dependencies: CmsDependencies): CmsDependencies {\n if (!dependencies || !Array.isArray(dependencies.types) || dependencies.types.length === 0) {\n throw new TypeError(\"CMS dependencies require document types\");\n }\n const types = [...dependencies.types];\n for (const type of types) {\n if (typeof type !== \"string\" || !TYPE.test(type)) throw new TypeError(\"Invalid CMS document type\");\n }\n if (dependencies.ids !== undefined && !Array.isArray(dependencies.ids)) throw new TypeError(\"Invalid CMS document IDs\");\n const ids = dependencies.ids === undefined ? undefined : [...dependencies.ids];\n for (const id of ids ?? []) {\n if (typeof id !== \"string\" || !ID.test(id) || /^(drafts|versions)\\./.test(id)) {\n throw new TypeError(\"Invalid CMS document ID\");\n }\n }\n if (new Set(types).size + new Set(ids).size > 128) throw new TypeError(\"Too many CMS tags\");\n return Object.freeze({ types: Object.freeze(types), ...(ids === undefined ? {} : { ids: Object.freeze(ids) }) });\n}\n\nexport function defineCmsQuery<T>(query: CmsQuery<T>): CmsQuery<T> {\n if (!query || typeof query.name !== \"string\" || !/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(query.name)\n || typeof query.query !== \"string\" || query.query.trim() === \"\" || typeof query.resolve !== \"function\") {\n throw new TypeError(\"Invalid CMS query configuration\");\n }\n const params = query.params;\n if (!params || typeof params !== \"object\" || (Object.getPrototypeOf(params) !== Object.prototype && Object.getPrototypeOf(params) !== null)) {\n throw new TypeError(\"CMS params must be plain JSON scalars\");\n }\n const copied: Record<string, string | number | boolean | null> = {};\n for (const key of Reflect.ownKeys(params)) {\n const descriptor = Object.getOwnPropertyDescriptor(params, key);\n if (typeof key !== \"string\" || !descriptor || !Object.hasOwn(descriptor, \"value\") || !descriptor.enumerable) {\n throw new TypeError(\"CMS params must be plain JSON scalars\");\n }\n const value: unknown = descriptor.value;\n if (!(value === null || typeof value === \"string\" || typeof value === \"boolean\" || (typeof value === \"number\" && Number.isFinite(value)))) {\n throw new TypeError(\"CMS params must be plain JSON scalars\");\n }\n Object.defineProperty(copied, key, { value, enumerable: true });\n }\n return Object.freeze({ name: query.name, query: query.query, params: Object.freeze(copied), dependencies: dependenciesCopy(query.dependencies), resolve: query.resolve });\n}\n\nexport function cmsTags(scope: string, dependencies: CmsDependencies): readonly string[] {\n if (typeof scope !== \"string\" || scope.length > 77 || !SCOPE.test(scope)) throw new TypeError(\"Invalid CMS scope\");\n const copied = dependenciesCopy(dependencies);\n const tags = [\n ...[...new Set(copied.types)].sort().map((type) => `cms:${scope}:type:${type}`),\n ...[...new Set(copied.ids ?? [])].sort().map((id) => `cms:${scope}:id:${id}`),\n ];\n return Object.freeze(tags);\n}\n", "import type { CmsAdapter, CmsFetch } from \"../../runtime/reader.js\";\nimport { cmsTags, defineCmsQuery } from \"../../contracts/query.js\";\n\nexport type SanityBinding = {\n readonly projectId: string;\n readonly dataset: string;\n readonly apiVersion: string;\n};\nexport type SanityAdapterOptions = {\n readonly binding: SanityBinding;\n readonly readToken?: string;\n readonly fetchImpl?: CmsFetch;\n readonly timeoutMs?: number;\n};\n\nconst RESPONSE_LIMIT = 2 * 1024 * 1024;\n\nfunction bindingCopy(binding: SanityBinding): SanityBinding {\n if (!binding || typeof binding.projectId !== \"string\" || !/^[a-z0-9]{6,12}$/.test(binding.projectId)\n || typeof binding.dataset !== \"string\" || !/^[a-z][a-z0-9_-]{0,63}$/.test(binding.dataset)\n || typeof binding.apiVersion !== \"string\" || !/^v?\\d{4}-\\d{2}-\\d{2}$/.test(binding.apiVersion)) {\n throw new TypeError(\"Invalid Sanity binding configuration\");\n }\n return Object.freeze({ projectId: binding.projectId, dataset: binding.dataset, apiVersion: binding.apiVersion.replace(/^v/, \"\") });\n}\n\n/** Applies policy via fetch's Next metadata. Standard fetch has no persistent cache or HTML invalidation. */\nexport function createSanityAdapter(options: SanityAdapterOptions): CmsAdapter {\n const binding = bindingCopy(options?.binding);\n const timeoutMs = options.timeoutMs ?? 1500;\n const token = options.readToken;\n if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647\n || (options.fetchImpl !== undefined && typeof options.fetchImpl !== \"function\")\n || (token !== undefined && (typeof token !== \"string\" || token.trim() === \"\" || /[\\r\\n]/.test(token)))) {\n throw new TypeError(\"Invalid Sanity adapter configuration\");\n }\n const fetchImpl = options.fetchImpl;\n const scope = `${binding.projectId}.${binding.dataset}`;\n return Object.freeze({\n scope,\n async read(query, policy) {\n // Direct structural callers receive the same configuration refusals.\n let validated;\n try {\n if (!policy || !Number.isSafeInteger(policy.revalidateSeconds) || policy.revalidateSeconds < 3600 || !Array.isArray(policy.tags)) throw new TypeError();\n const types: string[] = [];\n const ids: string[] = [];\n for (const tag of policy.tags) {\n if (typeof tag !== \"string\") throw new TypeError();\n if (tag.startsWith(`cms:${scope}:type:`)) types.push(tag.slice(`cms:${scope}:type:`.length));\n else if (tag.startsWith(`cms:${scope}:id:`)) ids.push(tag.slice(`cms:${scope}:id:`.length));\n else throw new TypeError();\n }\n const expected = cmsTags(scope, { types, ids });\n if (expected.length !== policy.tags.length || expected.some((tag, i) => tag !== policy.tags[i])) throw new TypeError();\n validated = defineCmsQuery({ ...query, dependencies: { types, ids }, resolve: (raw: unknown) => ({ data: raw, fallbackFields: [] }) });\n } catch { throw new TypeError(\"Invalid Sanity read configuration\"); }\n const url = new URL(`https://${binding.projectId}.api.sanity.io/v${binding.apiVersion}/data/query/${binding.dataset}`);\n url.searchParams.set(\"query\", validated.query);\n url.searchParams.set(\"perspective\", \"published\");\n for (const key of Object.keys(validated.params).sort()) url.searchParams.set(`$${key}`, JSON.stringify(validated.params[key]));\n const controller = new AbortController();\n let bodyReader: ReadableStreamDefaultReader<Uint8Array> | undefined;\n let expired = false;\n const cancel = (): void => {\n controller.abort();\n if (bodyReader) void bodyReader.cancel().catch(() => {});\n };\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<Awaited<ReturnType<CmsAdapter[\"read\"]>>>((resolve) => {\n timer = setTimeout(() => {\n expired = true;\n cancel();\n resolve({ ok: false, reason: \"timeout\" });\n }, timeoutMs);\n });\n const work = async (): Promise<Awaited<ReturnType<CmsAdapter[\"read\"]>>> => {\n try {\n // Look up global fetch at read time so Next's patched fetch is retained.\n const response = await (fetchImpl ?? globalThis.fetch)(url.href, {\n method: \"GET\", redirect: \"error\", signal: controller.signal,\n headers: token === undefined ? {} : { Authorization: `Bearer ${token}` },\n next: { revalidate: policy.revalidateSeconds, tags: [...policy.tags] },\n });\n if (expired) { void response.body?.cancel().catch(() => {}); return { ok: false, reason: \"timeout\" }; }\n if (!response.ok) {\n void response.body?.cancel().catch(() => {});\n const status = response.status;\n const reason = status === 402 ? \"plan-limit\" : status === 401 || status === 403 ? \"permission\"\n : status === 429 ? \"rate-limit\" : \"upstream\";\n return { ok: false, reason, status };\n }\n if (!response.body) return { ok: false, reason: \"malformed\" };\n bodyReader = response.body.getReader();\n let bytes = 0;\n let text = \"\";\n const decoder = new TextDecoder(\"utf-8\", { fatal: true });\n while (true) {\n const chunk = await bodyReader.read();\n if (expired) return { ok: false, reason: \"timeout\" };\n if (chunk.done) break;\n bytes += chunk.value.byteLength;\n if (bytes > RESPONSE_LIMIT) { cancel(); return { ok: false, reason: \"malformed\" }; }\n try { text += decoder.decode(chunk.value, { stream: true }); }\n catch { cancel(); return { ok: false, reason: \"malformed\" }; }\n }\n try {\n text += decoder.decode();\n const envelope: unknown = JSON.parse(text);\n if (!envelope || typeof envelope !== \"object\" || Array.isArray(envelope) || !Object.hasOwn(envelope, \"result\")) return { ok: false, reason: \"malformed\" };\n return { ok: true, value: (envelope as Record<string, unknown>).result };\n } catch { return { ok: false, reason: \"malformed\" }; }\n } catch { return { ok: false, reason: expired ? \"timeout\" : \"network\" }; }\n };\n try { return await Promise.race([work(), deadline]); }\n finally { clearTimeout(timer); }\n },\n } satisfies CmsAdapter);\n}\n\nexport function sanityBindingFromEnv(env: Readonly<Record<string, string | undefined>>): SanityBinding {\n return bindingCopy({ projectId: env.SANITY_PROJECT_ID, dataset: env.SANITY_DATASET, apiVersion: env.SANITY_API_VERSION } as SanityBinding);\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["sanity_exports", "__export", "createSanityAdapter", "sanityBindingFromEnv", "__toCommonJS", "import_server_only", "TYPE", "ID", "SCOPE", "dependenciesCopy", "dependencies", "types", "type", "ids", "id", "defineCmsQuery", "query", "params", "copied", "key", "descriptor", "value", "cmsTags", "scope", "tags", "RESPONSE_LIMIT", "bindingCopy", "binding", "createSanityAdapter", "options", "timeoutMs", "token", "fetchImpl", "scope", "query", "policy", "validated", "types", "ids", "tag", "expected", "cmsTags", "i", "defineCmsQuery", "raw", "url", "key", "controller", "bodyReader", "expired", "cancel", "timer", "deadline", "resolve", "work", "response", "status", "bytes", "text", "decoder", "chunk", "envelope", "sanityBindingFromEnv", "env"]
|
|
7
7
|
}
|