@capacms/sdk 1.0.0-next.1 → 1.0.0-next.10
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/CHANGELOG.md +450 -0
- package/README.md +1698 -193
- package/bin/capa-codegen.js +192 -5
- package/bin/capa.js +235 -0
- package/bin/graphql-project.js +142 -0
- package/bin/project-env.js +58 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +17 -0
- package/dist/codegen.d.ts +55 -0
- package/dist/codegen.js +320 -39
- package/dist/config.d.ts +5 -36
- package/dist/config.js +47 -1
- package/dist/esm/image/index.d.ts +120 -0
- package/dist/esm/image/index.js +250 -0
- package/dist/esm/image/shared-params.generated.d.ts +190 -0
- package/dist/esm/image/shared-params.generated.js +461 -0
- package/dist/esm/nextjs/image-loader.d.ts +60 -0
- package/dist/esm/nextjs/image-loader.js +67 -0
- package/dist/esm/nextjs/overlay.d.ts +30 -0
- package/dist/esm/nextjs/overlay.js +75 -0
- package/dist/esm/overlay/index.d.ts +32 -0
- package/dist/esm/overlay/index.js +576 -0
- package/dist/esm/overlay/protocol.d.ts +187 -0
- package/dist/esm/overlay/protocol.js +240 -0
- package/dist/esm/package.json +4 -0
- package/dist/graphql-codegen.d.ts +117 -0
- package/dist/graphql-codegen.js +705 -0
- package/dist/http.js +1 -1
- package/dist/image/index.d.ts +120 -0
- package/dist/image/index.js +257 -0
- package/dist/image/shared-params.generated.d.ts +190 -0
- package/dist/image/shared-params.generated.js +471 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/next/attrs.d.ts +84 -10
- package/dist/next/attrs.js +119 -2
- package/dist/next/client.d.ts +176 -32
- package/dist/next/client.js +212 -90
- package/dist/next/entry-fields.d.ts +162 -0
- package/dist/next/entry-fields.js +2 -0
- package/dist/next/errors.d.ts +136 -0
- package/dist/next/errors.js +214 -0
- package/dist/next/field-names.d.ts +37 -0
- package/dist/next/field-names.js +145 -0
- package/dist/next/graphql/build.d.ts +27 -0
- package/dist/next/graphql/build.js +98 -0
- package/dist/next/graphql/documents.d.ts +67 -0
- package/dist/next/graphql/documents.js +35 -0
- package/dist/next/graphql/edit-mode.d.ts +16 -0
- package/dist/next/graphql/edit-mode.js +93 -0
- package/dist/next/graphql/filter-values.d.ts +34 -0
- package/dist/next/graphql/filter-values.js +96 -0
- package/dist/next/graphql/introspection.d.ts +89 -0
- package/dist/next/graphql/introspection.js +102 -0
- package/dist/next/graphql/plan.d.ts +115 -0
- package/dist/next/graphql/plan.js +531 -0
- package/dist/next/graphql/request.d.ts +228 -0
- package/dist/next/graphql/request.js +283 -0
- package/dist/next/graphql/rest.d.ts +66 -0
- package/dist/next/graphql/rest.js +502 -0
- package/dist/next/graphql/selection.d.ts +55 -0
- package/dist/next/graphql/selection.js +212 -0
- package/dist/next/graphql/sha256.d.ts +13 -0
- package/dist/next/graphql/sha256.js +86 -0
- package/dist/next/graphql/summary.d.ts +83 -0
- package/dist/next/graphql/summary.js +151 -0
- package/dist/next/graphql/tree-layout.d.ts +36 -0
- package/dist/next/graphql/tree-layout.js +20 -0
- package/dist/next/graphql/tree.d.ts +171 -0
- package/dist/next/graphql/tree.js +249 -0
- package/dist/next/graphql/typed.d.ts +261 -0
- package/dist/next/graphql/typed.js +146 -0
- package/dist/next/index.d.ts +30 -5
- package/dist/next/index.js +32 -1
- package/dist/next/inflate.d.ts +51 -0
- package/dist/next/inflate.js +243 -0
- package/dist/next/key-family.d.ts +31 -0
- package/dist/next/key-family.js +66 -0
- package/dist/next/select-types.d.ts +58 -5
- package/dist/next/system-keys.d.ts +27 -0
- package/dist/next/system-keys.js +42 -0
- package/dist/nextjs/image-loader.d.ts +60 -0
- package/dist/nextjs/image-loader.js +71 -0
- package/dist/nextjs/index.d.ts +484 -5
- package/dist/nextjs/index.js +688 -6
- package/dist/nextjs/overlay.d.ts +30 -0
- package/dist/nextjs/overlay.js +78 -0
- package/dist/overlay/index.d.ts +14 -2
- package/dist/overlay/index.js +282 -43
- package/dist/overlay/protocol.d.ts +98 -2
- package/dist/overlay/protocol.js +151 -4
- package/package.json +63 -15
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live preview protocol, site side. Pure: no DOM, no globals, so it can be
|
|
3
|
+
* tested under plain `node --test`.
|
|
4
|
+
*
|
|
5
|
+
* The Capa admin frames the site and the two talk over `postMessage`. Every
|
|
6
|
+
* message is a plain object `{ source, v: 1, type, ...fields }`:
|
|
7
|
+
*
|
|
8
|
+
* admin -> site (source "capa-admin")
|
|
9
|
+
* hello {} sent on the frame's load
|
|
10
|
+
* highlight { entryId, field: string | null } entryId "" clears
|
|
11
|
+
* outline { on: boolean } outline every tagged element
|
|
12
|
+
* refresh {} re-render the draft
|
|
13
|
+
* patch { entryId, fields: [{ field, value, saved }] }
|
|
14
|
+
* unsaved text, as the editor types it
|
|
15
|
+
*
|
|
16
|
+
* site -> admin (source "capa")
|
|
17
|
+
* ready { path, entries, features } after hello, and after every navigation.
|
|
18
|
+
* `features`: what this overlay can do
|
|
19
|
+
* beyond v1 (`SITE_FEATURES`)
|
|
20
|
+
* select { entryId, field } a tagged element was clicked
|
|
21
|
+
* hover { entryId, field } | { entryId: "", field: null }
|
|
22
|
+
* visible { entryId, field } the tagged element at the centre of the
|
|
23
|
+
* viewport changed (throttled, on scroll)
|
|
24
|
+
*
|
|
25
|
+
* `visible` arrived in 1.0.0-next.2 and is OPTIONAL: it is still `v: 1`,
|
|
26
|
+
* because an admin that does not know it drops an unknown type, and an older
|
|
27
|
+
* overlay simply never sends it. Additive messages keep the version; only a
|
|
28
|
+
* change to an existing shape would move it. `patch` and `features` are the
|
|
29
|
+
* same kind of addition: an admin sends `patch` only to an overlay whose
|
|
30
|
+
* `ready` lists "patch", and an older admin never reads `features`.
|
|
31
|
+
*
|
|
32
|
+
* A PATCH IS TEXT ONLY, and only where the page shows the field verbatim. The
|
|
33
|
+
* overlay changes a tagged element's text while it reads exactly `saved` (the
|
|
34
|
+
* value the page was rendered from) or what this page last patched it to, so
|
|
35
|
+
* a field the site formats (a price, a date, Markdown) is left alone until the
|
|
36
|
+
* save re-renders it (`patchedText`).
|
|
37
|
+
*
|
|
38
|
+
* The admin keeps its own copy of these shapes (`apps/admin`, no dependency on
|
|
39
|
+
* this package). If you change one side, change the other.
|
|
40
|
+
*/
|
|
41
|
+
export declare const PROTOCOL_VERSION = 1;
|
|
42
|
+
export declare const ADMIN_SOURCE = "capa-admin";
|
|
43
|
+
export declare const SITE_SOURCE = "capa";
|
|
44
|
+
/** What this overlay can do beyond v1, listed in its `ready`. */
|
|
45
|
+
export declare const SITE_FEATURES: readonly string[];
|
|
46
|
+
/** One field of a `patch`: the text being typed, and the saved value the page was rendered from. */
|
|
47
|
+
export interface PatchField {
|
|
48
|
+
field: string;
|
|
49
|
+
value: string;
|
|
50
|
+
saved: string;
|
|
51
|
+
}
|
|
52
|
+
/** A `patch` past these is refused whole: no form has that many text fields, or that much text in one. */
|
|
53
|
+
export declare const PATCH_MAX_FIELDS = 500;
|
|
54
|
+
export declare const PATCH_MAX_LENGTH = 100000;
|
|
55
|
+
export type AdminMessage = {
|
|
56
|
+
source: typeof ADMIN_SOURCE;
|
|
57
|
+
v: 1;
|
|
58
|
+
type: "hello";
|
|
59
|
+
} | {
|
|
60
|
+
source: typeof ADMIN_SOURCE;
|
|
61
|
+
v: 1;
|
|
62
|
+
type: "highlight";
|
|
63
|
+
entryId: string;
|
|
64
|
+
field: string | null;
|
|
65
|
+
} | {
|
|
66
|
+
source: typeof ADMIN_SOURCE;
|
|
67
|
+
v: 1;
|
|
68
|
+
type: "outline";
|
|
69
|
+
on: boolean;
|
|
70
|
+
} | {
|
|
71
|
+
source: typeof ADMIN_SOURCE;
|
|
72
|
+
v: 1;
|
|
73
|
+
type: "refresh";
|
|
74
|
+
} | {
|
|
75
|
+
source: typeof ADMIN_SOURCE;
|
|
76
|
+
v: 1;
|
|
77
|
+
type: "patch";
|
|
78
|
+
entryId: string;
|
|
79
|
+
fields: PatchField[];
|
|
80
|
+
};
|
|
81
|
+
export type SiteMessage = {
|
|
82
|
+
source: typeof SITE_SOURCE;
|
|
83
|
+
v: 1;
|
|
84
|
+
type: "ready";
|
|
85
|
+
path: string;
|
|
86
|
+
entries: string[];
|
|
87
|
+
features?: string[];
|
|
88
|
+
} | {
|
|
89
|
+
source: typeof SITE_SOURCE;
|
|
90
|
+
v: 1;
|
|
91
|
+
type: "select";
|
|
92
|
+
entryId: string;
|
|
93
|
+
field: string;
|
|
94
|
+
} | {
|
|
95
|
+
source: typeof SITE_SOURCE;
|
|
96
|
+
v: 1;
|
|
97
|
+
type: "hover";
|
|
98
|
+
entryId: string;
|
|
99
|
+
field: string | null;
|
|
100
|
+
} | {
|
|
101
|
+
source: typeof SITE_SOURCE;
|
|
102
|
+
v: 1;
|
|
103
|
+
type: "visible";
|
|
104
|
+
entryId: string;
|
|
105
|
+
field: string;
|
|
106
|
+
};
|
|
107
|
+
/** The parts of a `MessageEvent` the filter reads, so a test can hand in a literal. */
|
|
108
|
+
export interface MessageLike {
|
|
109
|
+
origin: string;
|
|
110
|
+
source: unknown;
|
|
111
|
+
data: unknown;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* `https://admin.example.com/` and `https://admin.example.com` are one origin,
|
|
115
|
+
* and a configured value that is not a URL at all is dropped rather than
|
|
116
|
+
* compared as a string, because `"*"` must never mean "anyone".
|
|
117
|
+
*/
|
|
118
|
+
export declare function normaliseOrigins(origins: readonly string[]): string[];
|
|
119
|
+
/**
|
|
120
|
+
* The one gate every incoming message goes through. A message is accepted only
|
|
121
|
+
* when all of these hold, and is `null` otherwise:
|
|
122
|
+
*
|
|
123
|
+
* - it came from the window that framed this page (`parent`), not from a
|
|
124
|
+
* popup, a sibling frame or this page itself
|
|
125
|
+
* - its origin is one the site configured as its admin
|
|
126
|
+
* - it says it is from the admin, speaks version 1, and has a known type
|
|
127
|
+
* whose fields have the right shapes
|
|
128
|
+
*
|
|
129
|
+
* `allowedOrigins` is expected already normalised (`normaliseOrigins`).
|
|
130
|
+
*/
|
|
131
|
+
export declare function acceptMessage(event: MessageLike, allowedOrigins: readonly string[], parent: unknown): AdminMessage | null;
|
|
132
|
+
/**
|
|
133
|
+
* The text a node should read after a patch, or null to leave it alone.
|
|
134
|
+
*
|
|
135
|
+
* Only while its text (trimmed) is the saved value or what this page last
|
|
136
|
+
* patched it to, so text the site formats is never touched. The whitespace a
|
|
137
|
+
* template put around it stays. Null too when there is nothing to change.
|
|
138
|
+
*/
|
|
139
|
+
export declare function patchedText(current: string, change: {
|
|
140
|
+
value: string;
|
|
141
|
+
saved: string;
|
|
142
|
+
}, lastPatched: string | null, around?: {
|
|
143
|
+
lead: string;
|
|
144
|
+
trail: string;
|
|
145
|
+
}): string | null;
|
|
146
|
+
/**
|
|
147
|
+
* The whitespace around a node's text, which a patch keeps. A node that is
|
|
148
|
+
* only whitespace keeps it in front. The overlay remembers this from the
|
|
149
|
+
* first patch, so text typed away to nothing and back lands where it was.
|
|
150
|
+
*/
|
|
151
|
+
export declare function textAround(current: string): {
|
|
152
|
+
lead: string;
|
|
153
|
+
trail: string;
|
|
154
|
+
};
|
|
155
|
+
export declare function readyMessage(path: string, entries: string[], features?: readonly string[]): SiteMessage;
|
|
156
|
+
export declare function selectMessage(entryId: string, field: string): SiteMessage;
|
|
157
|
+
export declare function hoverMessage(entryId: string, field: string | null): SiteMessage;
|
|
158
|
+
export declare function visibleMessage(entryId: string, field: string): SiteMessage;
|
|
159
|
+
/** The part of a tagged element `pickCentred` reads, so a test can hand in literals. */
|
|
160
|
+
export interface TaggedBox {
|
|
161
|
+
entryId: string;
|
|
162
|
+
field: string;
|
|
163
|
+
/** `getBoundingClientRect()`, viewport coordinates. */
|
|
164
|
+
top: number;
|
|
165
|
+
bottom: number;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The tagged element a reader is looking at: the one that contains the
|
|
169
|
+
* viewport's horizontal centre line, and when several do (a field inside a
|
|
170
|
+
* card that is also tagged) the SMALLEST, because the innermost element is
|
|
171
|
+
* the specific one. When none crosses the line, the nearest one that is at
|
|
172
|
+
* least partly on screen. Null when nothing tagged is on screen at all.
|
|
173
|
+
*
|
|
174
|
+
* `edge` is where the page is scrolled to. A heading near the top of a page
|
|
175
|
+
* can never reach the centre line, because the page cannot scroll above its
|
|
176
|
+
* top; at the top the topmost visible element is the one being read, and at
|
|
177
|
+
* the bottom the bottommost. The same rule a table of contents' scroll spy
|
|
178
|
+
* uses.
|
|
179
|
+
*
|
|
180
|
+
* Pure, so the admin's "follow the page" can be tested without a DOM.
|
|
181
|
+
*/
|
|
182
|
+
export declare function pickCentred(boxes: readonly TaggedBox[], viewportHeight: number, edge?: "top" | "bottom" | null): TaggedBox | null;
|
|
183
|
+
/**
|
|
184
|
+
* Which edge a scroll position is at, for `pickCentred`. A page that does not
|
|
185
|
+
* scroll at all is at neither: nothing moves, and the centre rule stands.
|
|
186
|
+
*/
|
|
187
|
+
export declare function scrollEdge(scrollY: number, viewportHeight: number, scrollHeight: number): "top" | "bottom" | null;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live preview protocol, site side. Pure: no DOM, no globals, so it can be
|
|
3
|
+
* tested under plain `node --test`.
|
|
4
|
+
*
|
|
5
|
+
* The Capa admin frames the site and the two talk over `postMessage`. Every
|
|
6
|
+
* message is a plain object `{ source, v: 1, type, ...fields }`:
|
|
7
|
+
*
|
|
8
|
+
* admin -> site (source "capa-admin")
|
|
9
|
+
* hello {} sent on the frame's load
|
|
10
|
+
* highlight { entryId, field: string | null } entryId "" clears
|
|
11
|
+
* outline { on: boolean } outline every tagged element
|
|
12
|
+
* refresh {} re-render the draft
|
|
13
|
+
* patch { entryId, fields: [{ field, value, saved }] }
|
|
14
|
+
* unsaved text, as the editor types it
|
|
15
|
+
*
|
|
16
|
+
* site -> admin (source "capa")
|
|
17
|
+
* ready { path, entries, features } after hello, and after every navigation.
|
|
18
|
+
* `features`: what this overlay can do
|
|
19
|
+
* beyond v1 (`SITE_FEATURES`)
|
|
20
|
+
* select { entryId, field } a tagged element was clicked
|
|
21
|
+
* hover { entryId, field } | { entryId: "", field: null }
|
|
22
|
+
* visible { entryId, field } the tagged element at the centre of the
|
|
23
|
+
* viewport changed (throttled, on scroll)
|
|
24
|
+
*
|
|
25
|
+
* `visible` arrived in 1.0.0-next.2 and is OPTIONAL: it is still `v: 1`,
|
|
26
|
+
* because an admin that does not know it drops an unknown type, and an older
|
|
27
|
+
* overlay simply never sends it. Additive messages keep the version; only a
|
|
28
|
+
* change to an existing shape would move it. `patch` and `features` are the
|
|
29
|
+
* same kind of addition: an admin sends `patch` only to an overlay whose
|
|
30
|
+
* `ready` lists "patch", and an older admin never reads `features`.
|
|
31
|
+
*
|
|
32
|
+
* A PATCH IS TEXT ONLY, and only where the page shows the field verbatim. The
|
|
33
|
+
* overlay changes a tagged element's text while it reads exactly `saved` (the
|
|
34
|
+
* value the page was rendered from) or what this page last patched it to, so
|
|
35
|
+
* a field the site formats (a price, a date, Markdown) is left alone until the
|
|
36
|
+
* save re-renders it (`patchedText`).
|
|
37
|
+
*
|
|
38
|
+
* The admin keeps its own copy of these shapes (`apps/admin`, no dependency on
|
|
39
|
+
* this package). If you change one side, change the other.
|
|
40
|
+
*/
|
|
41
|
+
export const PROTOCOL_VERSION = 1;
|
|
42
|
+
export const ADMIN_SOURCE = "capa-admin";
|
|
43
|
+
export const SITE_SOURCE = "capa";
|
|
44
|
+
/** What this overlay can do beyond v1, listed in its `ready`. */
|
|
45
|
+
export const SITE_FEATURES = ["patch"];
|
|
46
|
+
/** A `patch` past these is refused whole: no form has that many text fields, or that much text in one. */
|
|
47
|
+
export const PATCH_MAX_FIELDS = 500;
|
|
48
|
+
export const PATCH_MAX_LENGTH = 100_000;
|
|
49
|
+
/**
|
|
50
|
+
* `https://admin.example.com/` and `https://admin.example.com` are one origin,
|
|
51
|
+
* and a configured value that is not a URL at all is dropped rather than
|
|
52
|
+
* compared as a string, because `"*"` must never mean "anyone".
|
|
53
|
+
*/
|
|
54
|
+
export function normaliseOrigins(origins) {
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const raw of origins) {
|
|
57
|
+
if (typeof raw !== "string" || raw.trim() === "")
|
|
58
|
+
continue;
|
|
59
|
+
try {
|
|
60
|
+
const origin = new URL(raw.trim()).origin;
|
|
61
|
+
if (origin !== "null" && !out.includes(origin))
|
|
62
|
+
out.push(origin);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Not a URL: never an origin anybody can send from.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The one gate every incoming message goes through. A message is accepted only
|
|
72
|
+
* when all of these hold, and is `null` otherwise:
|
|
73
|
+
*
|
|
74
|
+
* - it came from the window that framed this page (`parent`), not from a
|
|
75
|
+
* popup, a sibling frame or this page itself
|
|
76
|
+
* - its origin is one the site configured as its admin
|
|
77
|
+
* - it says it is from the admin, speaks version 1, and has a known type
|
|
78
|
+
* whose fields have the right shapes
|
|
79
|
+
*
|
|
80
|
+
* `allowedOrigins` is expected already normalised (`normaliseOrigins`).
|
|
81
|
+
*/
|
|
82
|
+
export function acceptMessage(event, allowedOrigins, parent) {
|
|
83
|
+
if (!event || event.source !== parent || parent == null)
|
|
84
|
+
return null;
|
|
85
|
+
if (typeof event.origin !== "string" || !allowedOrigins.includes(event.origin))
|
|
86
|
+
return null;
|
|
87
|
+
const data = event.data;
|
|
88
|
+
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
89
|
+
return null;
|
|
90
|
+
const m = data;
|
|
91
|
+
if (m.source !== ADMIN_SOURCE || m.v !== PROTOCOL_VERSION)
|
|
92
|
+
return null;
|
|
93
|
+
switch (m.type) {
|
|
94
|
+
case "hello":
|
|
95
|
+
return { source: ADMIN_SOURCE, v: 1, type: "hello" };
|
|
96
|
+
case "refresh":
|
|
97
|
+
return { source: ADMIN_SOURCE, v: 1, type: "refresh" };
|
|
98
|
+
case "outline":
|
|
99
|
+
if (typeof m.on !== "boolean")
|
|
100
|
+
return null;
|
|
101
|
+
return { source: ADMIN_SOURCE, v: 1, type: "outline", on: m.on };
|
|
102
|
+
case "highlight":
|
|
103
|
+
if (typeof m.entryId !== "string")
|
|
104
|
+
return null;
|
|
105
|
+
if (m.field !== null && typeof m.field !== "string")
|
|
106
|
+
return null;
|
|
107
|
+
return { source: ADMIN_SOURCE, v: 1, type: "highlight", entryId: m.entryId, field: m.field };
|
|
108
|
+
case "patch": {
|
|
109
|
+
if (typeof m.entryId !== "string" || m.entryId === "")
|
|
110
|
+
return null;
|
|
111
|
+
if (!Array.isArray(m.fields) || m.fields.length > PATCH_MAX_FIELDS)
|
|
112
|
+
return null;
|
|
113
|
+
const fields = [];
|
|
114
|
+
for (const raw of m.fields) {
|
|
115
|
+
if (typeof raw !== "object" || raw === null)
|
|
116
|
+
return null;
|
|
117
|
+
const f = raw;
|
|
118
|
+
if (typeof f.field !== "string" || f.field === "")
|
|
119
|
+
return null;
|
|
120
|
+
if (typeof f.value !== "string" || typeof f.saved !== "string")
|
|
121
|
+
return null;
|
|
122
|
+
if (f.value.length > PATCH_MAX_LENGTH || f.saved.length > PATCH_MAX_LENGTH)
|
|
123
|
+
return null;
|
|
124
|
+
fields.push({ field: f.field, value: f.value, saved: f.saved });
|
|
125
|
+
}
|
|
126
|
+
return { source: ADMIN_SOURCE, v: 1, type: "patch", entryId: m.entryId, fields };
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The text a node should read after a patch, or null to leave it alone.
|
|
134
|
+
*
|
|
135
|
+
* Only while its text (trimmed) is the saved value or what this page last
|
|
136
|
+
* patched it to, so text the site formats is never touched. The whitespace a
|
|
137
|
+
* template put around it stays. Null too when there is nothing to change.
|
|
138
|
+
*/
|
|
139
|
+
export function patchedText(current, change, lastPatched, around = textAround(current)) {
|
|
140
|
+
const text = current.trim();
|
|
141
|
+
const ours = text === change.saved.trim() || (lastPatched !== null && text === lastPatched.trim());
|
|
142
|
+
if (!ours || text === change.value.trim())
|
|
143
|
+
return null;
|
|
144
|
+
return `${around.lead}${change.value}${around.trail}`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The whitespace around a node's text, which a patch keeps. A node that is
|
|
148
|
+
* only whitespace keeps it in front. The overlay remembers this from the
|
|
149
|
+
* first patch, so text typed away to nothing and back lands where it was.
|
|
150
|
+
*/
|
|
151
|
+
export function textAround(current) {
|
|
152
|
+
const text = current.trim();
|
|
153
|
+
if (text === "")
|
|
154
|
+
return { lead: current, trail: "" };
|
|
155
|
+
return {
|
|
156
|
+
lead: current.slice(0, current.length - current.trimStart().length),
|
|
157
|
+
trail: current.slice(current.trimEnd().length),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export function readyMessage(path, entries, features) {
|
|
161
|
+
return features
|
|
162
|
+
? { source: SITE_SOURCE, v: 1, type: "ready", path, entries, features: [...features] }
|
|
163
|
+
: { source: SITE_SOURCE, v: 1, type: "ready", path, entries };
|
|
164
|
+
}
|
|
165
|
+
export function selectMessage(entryId, field) {
|
|
166
|
+
return { source: SITE_SOURCE, v: 1, type: "select", entryId, field };
|
|
167
|
+
}
|
|
168
|
+
export function hoverMessage(entryId, field) {
|
|
169
|
+
return entryId === ""
|
|
170
|
+
? { source: SITE_SOURCE, v: 1, type: "hover", entryId: "", field: null }
|
|
171
|
+
: { source: SITE_SOURCE, v: 1, type: "hover", entryId, field };
|
|
172
|
+
}
|
|
173
|
+
export function visibleMessage(entryId, field) {
|
|
174
|
+
return { source: SITE_SOURCE, v: 1, type: "visible", entryId, field };
|
|
175
|
+
}
|
|
176
|
+
/** Above any element height, so a box that misses the centre line never outranks one that crosses it. */
|
|
177
|
+
const MISS_FLOOR = 1e9;
|
|
178
|
+
/**
|
|
179
|
+
* The tagged element a reader is looking at: the one that contains the
|
|
180
|
+
* viewport's horizontal centre line, and when several do (a field inside a
|
|
181
|
+
* card that is also tagged) the SMALLEST, because the innermost element is
|
|
182
|
+
* the specific one. When none crosses the line, the nearest one that is at
|
|
183
|
+
* least partly on screen. Null when nothing tagged is on screen at all.
|
|
184
|
+
*
|
|
185
|
+
* `edge` is where the page is scrolled to. A heading near the top of a page
|
|
186
|
+
* can never reach the centre line, because the page cannot scroll above its
|
|
187
|
+
* top; at the top the topmost visible element is the one being read, and at
|
|
188
|
+
* the bottom the bottommost. The same rule a table of contents' scroll spy
|
|
189
|
+
* uses.
|
|
190
|
+
*
|
|
191
|
+
* Pure, so the admin's "follow the page" can be tested without a DOM.
|
|
192
|
+
*/
|
|
193
|
+
export function pickCentred(boxes, viewportHeight, edge = null) {
|
|
194
|
+
const centre = viewportHeight / 2;
|
|
195
|
+
let best = null;
|
|
196
|
+
let bestScore = Infinity;
|
|
197
|
+
for (const b of boxes) {
|
|
198
|
+
if (!b.entryId || !b.field)
|
|
199
|
+
continue;
|
|
200
|
+
if (b.bottom <= 0 || b.top >= viewportHeight || b.bottom <= b.top)
|
|
201
|
+
continue;
|
|
202
|
+
const height = b.bottom - b.top;
|
|
203
|
+
let score;
|
|
204
|
+
if (edge === "top") {
|
|
205
|
+
// Topmost first; of two that start together, the smaller (inner) one.
|
|
206
|
+
score = b.top * MISS_FLOOR + height;
|
|
207
|
+
}
|
|
208
|
+
else if (edge === "bottom") {
|
|
209
|
+
score = (viewportHeight - b.bottom) * MISS_FLOOR + height;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Crossing the line scores by height (smaller wins) and always beats a
|
|
213
|
+
// box that misses it, which scores by its distance from the line on top
|
|
214
|
+
// of a floor no height can reach.
|
|
215
|
+
score =
|
|
216
|
+
b.top <= centre && b.bottom >= centre
|
|
217
|
+
? height
|
|
218
|
+
: MISS_FLOOR + Math.min(Math.abs(b.top - centre), Math.abs(b.bottom - centre));
|
|
219
|
+
}
|
|
220
|
+
if (score < bestScore) {
|
|
221
|
+
best = b;
|
|
222
|
+
bestScore = score;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return best;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Which edge a scroll position is at, for `pickCentred`. A page that does not
|
|
229
|
+
* scroll at all is at neither: nothing moves, and the centre rule stands.
|
|
230
|
+
*/
|
|
231
|
+
export function scrollEdge(scrollY, viewportHeight, scrollHeight) {
|
|
232
|
+
const max = scrollHeight - viewportHeight;
|
|
233
|
+
if (max <= 1)
|
|
234
|
+
return null;
|
|
235
|
+
if (scrollY <= 1)
|
|
236
|
+
return "top";
|
|
237
|
+
if (scrollY >= max - 1)
|
|
238
|
+
return "bottom";
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graphql-codegen.ts — `capa-codegen --graphql` and `capa persist`, the pure half.
|
|
3
|
+
*
|
|
4
|
+
* Reads a key's schema (introspection JSON) and a project's GraphQL documents,
|
|
5
|
+
* validates every document against that schema, and writes ONE TypeScript
|
|
6
|
+
* module holding:
|
|
7
|
+
*
|
|
8
|
+
* - the schema as types, for `createClient<CapaQuery>()` and the typed
|
|
9
|
+
* builder (`client.graphql.query`);
|
|
10
|
+
* - one `TypedDocument` constant per named operation, so
|
|
11
|
+
* `client.graphql(ArticlesPageDocument, vars)` infers the result and the
|
|
12
|
+
* variables with no cast, and beside it `<Name>Models`, the models it
|
|
13
|
+
* reads, for its Next.js cache tags;
|
|
14
|
+
* - for each document written as a literal (`#graphql`, `/* capa *\/` or
|
|
15
|
+
* gql(`...`)), an entry in `CapaDocuments` keyed by its exact text, so
|
|
16
|
+
* `client.graphql(LITERAL, vars)` infers them too, with no import;
|
|
17
|
+
* - `capaTreeLayout`, what `toTree` reads of the schema, so a server lays a
|
|
18
|
+
* builder result out in REST's shape without reading the schema first.
|
|
19
|
+
*
|
|
20
|
+
* The file walk and the network live in `bin/`; everything here is a function
|
|
21
|
+
* of its inputs, so it is tested without either.
|
|
22
|
+
*
|
|
23
|
+
* `graphql` (graphql-js) is loaded on first use, never at import: it is an
|
|
24
|
+
* optional peer dependency that only these commands need, and a site that
|
|
25
|
+
* only calls `client.graphql()` must not pay for it.
|
|
26
|
+
*/
|
|
27
|
+
import type * as GraphQLJs from "graphql";
|
|
28
|
+
import type { CapaIntrospection } from "./next/graphql/introspection";
|
|
29
|
+
/** graphql-js, or an error that says how to get it. */
|
|
30
|
+
export declare function loadGraphQL(): typeof GraphQLJs;
|
|
31
|
+
/** One GraphQL document found in a project, with where it came from. */
|
|
32
|
+
export interface DocumentSource {
|
|
33
|
+
file: string;
|
|
34
|
+
/** 1-based line of the document's first character in `file`. */
|
|
35
|
+
line: number;
|
|
36
|
+
/** The document's text as the program holds it at run time: a template's escapes are applied. */
|
|
37
|
+
text: string;
|
|
38
|
+
/**
|
|
39
|
+
* True for a literal whose type is its text (`#graphql`, `/* capa *\/`,
|
|
40
|
+
* gql(`...`)): it is sent exactly as written, and codegen keys its types by
|
|
41
|
+
* that text.
|
|
42
|
+
*/
|
|
43
|
+
literal?: true;
|
|
44
|
+
/** The variable a literal is assigned to (`const CARD = ...`), which another literal's `${CARD}` names. */
|
|
45
|
+
name?: string;
|
|
46
|
+
/**
|
|
47
|
+
* For a literal with `${NAME}` in it: the text around each `${}` and the
|
|
48
|
+
* names in them, resolved against the project's other literals before the
|
|
49
|
+
* literal is read. `text` holds the template with its `${NAME}`s meanwhile.
|
|
50
|
+
*/
|
|
51
|
+
template?: {
|
|
52
|
+
segments: string[];
|
|
53
|
+
names: string[];
|
|
54
|
+
asConst: boolean;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** A template codegen does not read, and why: the CLI prints each one, so none is dropped in silence. */
|
|
58
|
+
export interface SkippedDocument extends DocumentSource {
|
|
59
|
+
reason: string;
|
|
60
|
+
}
|
|
61
|
+
/** A template's text at run time, from its source text: line ends as LF, escapes applied. */
|
|
62
|
+
export declare function cookTemplate(raw: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* The GraphQL documents in one file: the whole file for `.graphql` and `.gql`,
|
|
65
|
+
* and elsewhere every template that is marked as one: a `#graphql` first
|
|
66
|
+
* line, a `/* capa *\/` comment before it, or `gql` as a tag or a function.
|
|
67
|
+
* One on a comment line is an example, not a document.
|
|
68
|
+
*
|
|
69
|
+
* `skipped` names every template codegen will not read that looks meant for
|
|
70
|
+
* it, with the reason: one with `${}` inside (its text is only known at run
|
|
71
|
+
* time, so it cannot be validated or hashed ahead of it), a named operation
|
|
72
|
+
* left unmarked, and `/nextjs`'s `graphql` used as a tag.
|
|
73
|
+
*/
|
|
74
|
+
export declare function extractDocuments(file: string, text: string): {
|
|
75
|
+
documents: DocumentSource[];
|
|
76
|
+
skipped: SkippedDocument[];
|
|
77
|
+
};
|
|
78
|
+
export interface CodegenProblem {
|
|
79
|
+
file: string;
|
|
80
|
+
line: number;
|
|
81
|
+
column: number;
|
|
82
|
+
message: string;
|
|
83
|
+
}
|
|
84
|
+
export interface GeneratedOperation {
|
|
85
|
+
name: string;
|
|
86
|
+
/** The exact text of its `<Name>Document`, and hashed for persisted queries. */
|
|
87
|
+
document: string;
|
|
88
|
+
sha256: string;
|
|
89
|
+
/** The namespaces of the models it reads, written as `<Name>Models` for its cache tags. */
|
|
90
|
+
models: string[];
|
|
91
|
+
/** For a document written as a literal: the literal's text, which is what a call with it sends, and its hash. */
|
|
92
|
+
literal?: {
|
|
93
|
+
document: string;
|
|
94
|
+
sha256: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export interface GraphQLCodegenResult {
|
|
98
|
+
/** The module to write. Null when a document failed. */
|
|
99
|
+
source: string | null;
|
|
100
|
+
operations: GeneratedOperation[];
|
|
101
|
+
problems: CodegenProblem[];
|
|
102
|
+
/**
|
|
103
|
+
* Each use of a deprecated field, argument, input field or enum value, with
|
|
104
|
+
* the reason the schema gives: it still works, and a later Capa-Version
|
|
105
|
+
* removes it.
|
|
106
|
+
*/
|
|
107
|
+
warnings: CodegenProblem[];
|
|
108
|
+
/** Literals with a `${}` that names no literal of the project, so their text is only known at run time. */
|
|
109
|
+
skipped: SkippedDocument[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The generated module for a schema and a project's documents. Problems (a
|
|
113
|
+
* syntax error, a field the key cannot read, an unnamed operation, a name used
|
|
114
|
+
* twice) are returned with their file and line, and no module is produced
|
|
115
|
+
* while any remain.
|
|
116
|
+
*/
|
|
117
|
+
export declare function generateGraphQLModule(introspection: CapaIntrospection, sources: DocumentSource[]): GraphQLCodegenResult;
|