@ryusenkei/maezae 0.0.0-stage → 0.1.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.
@@ -0,0 +1,211 @@
1
+ import { C as FieldDef, D as PageEntry, E as PageDef, S as CountDef, _ as Media, a as CropRect, b as ContentModel, h as Hotspot, m as Crop, n as DateString, t as Clock, w as Publishing, x as ContentType, y as ContentFormat } from "./index-57nirXxn.js";
2
+ import { i as Body } from "./index-D11fUsoi.js";
3
+ /** 配信 API の1件の値(項目の key と id・updated_at) */
4
+ type Item = Readonly<Record<string, unknown>>;
5
+ /** 配信 API が解決した実体(hotspot・crop は元の画像に対する割合) */
6
+ type DeliveredAsset = Readonly<{
7
+ id: string;
8
+ kind: 'image' | 'video';
9
+ width: number;
10
+ height: number;
11
+ alt: string;
12
+ hotspot?: Hotspot;
13
+ crop?: Crop;
14
+ poster?: DeliveredAsset | null;
15
+ }>;
16
+ /** 項目の1つ分の値(複数の値を持つ項目は FieldValue が配列に、query.ts の絞り込みの値と共通) */
17
+ type SingleValue<F extends FieldDef> = F extends {
18
+ type: 'text' | 'textarea';
19
+ } ? string : F extends {
20
+ type: 'number';
21
+ } ? number : F extends {
22
+ type: 'checkbox';
23
+ } ? boolean : F extends {
24
+ type: 'date';
25
+ } ? DateString : F extends {
26
+ type: 'datetime';
27
+ } ? string : F extends {
28
+ type: 'select';
29
+ options: readonly (infer O extends {
30
+ value: string;
31
+ })[];
32
+ } ? O['value'] : F extends {
33
+ type: 'reference';
34
+ } ? string : F extends {
35
+ type: 'media';
36
+ } ? DeliveredAsset | null : F extends {
37
+ type: 'body';
38
+ } ? Body<DeliveredAsset | null> : F extends {
39
+ type: 'group';
40
+ fields: readonly (infer G extends FieldDef)[];
41
+ } ? GroupValue<G> : unknown;
42
+ /** まとまりの値(中の項目の key ごと) */
43
+ type GroupValue<G extends FieldDef> = { readonly [K in G as K['key']]: FieldValue<K>; };
44
+ /**
45
+ * 項目の値(種類ごと: 文字は string、日付は YYYY-MM-DD、日時は `ISO 8601` の UTC、参照は参照先の id、メディアは実体、本文は標準のブロック)
46
+ * - multiple の項目: 値の配列
47
+ * - 値なし: null
48
+ */
49
+ type FieldValue<F extends FieldDef> = (F extends {
50
+ multiple: true;
51
+ } ? readonly SingleValue<F>[] : SingleValue<F>) | null;
52
+ /** 型の項目(コアの項目と追加の項目) */
53
+ type FieldsOf<T extends ContentType> = (T['core'] extends readonly (infer C extends FieldDef)[] ? C : never) | T['fields'][number];
54
+ /** 型の宣言から導く1件の値(id・更新日時と、項目の key ごとの値) */
55
+ type ItemOf<T extends ContentType> = Readonly<{
56
+ id: string;
57
+ updated_at: string;
58
+ } & { [F in FieldsOf<T> as F['key']]: FieldValue<F>; }>;
59
+ /** 範囲の指定(両端を含む) */
60
+ type Range<V> = Readonly<{
61
+ gte?: V;
62
+ lte?: V;
63
+ }>;
64
+ type Scalar<F extends FieldDef> = NonNullable<SingleValue<F>>;
65
+ /** 項目の種類ごとの絞り込みの値(選択肢・参照は一致か in、チェックボックスは一致、数値・日付は一致か範囲、日時は範囲) */
66
+ type FilterValue<F extends FieldDef> = F extends {
67
+ private: true;
68
+ } ? never : F extends {
69
+ type: 'select' | 'reference';
70
+ } ? Scalar<F> | Readonly<{
71
+ in: readonly Scalar<F>[];
72
+ }> : F extends {
73
+ type: 'checkbox';
74
+ } ? boolean : F extends {
75
+ type: 'number' | 'date';
76
+ } ? Scalar<F> | Range<Scalar<F>> : F extends {
77
+ type: 'datetime';
78
+ } ? Range<string> : never;
79
+ /** 並べ替えできる項目の key(単数の文字・数値・日付・日時・選択肢・チェックボックス) */
80
+ type SortableKey<T extends ContentType> = FieldsOf<T> extends (infer F) ? (F extends {
81
+ private: true;
82
+ } | {
83
+ multiple: true;
84
+ } ? never : F extends {
85
+ type: 'text' | 'number' | 'date' | 'datetime' | 'select' | 'checkbox';
86
+ key: infer K extends string;
87
+ } ? K : never) : never;
88
+ /** 並べ替えの key(項目・公開日時・更新日時・id) */
89
+ type OrderKey<T extends ContentType> = SortableKey<T> | 'published_at' | 'updated_at' | 'id';
90
+ /** 絞り込みの指定(項目の key ごと、公開日時・更新日時は範囲) */
91
+ type ListFilter<T extends ContentType = ContentType> = Readonly<{ [F in FieldsOf<T> as [FilterValue<F>] extends [never] ? never : F['key']]?: FilterValue<F>; } & {
92
+ published_at?: Range<string>;
93
+ updated_at?: Range<string>;
94
+ }>;
95
+ /**
96
+ * 型の一覧の指定
97
+ * - limit: 1ページの件数(サーバーの上限は100)
98
+ * - cursor: 前のページの next
99
+ * - order: 並べ替えの key の配列(`-` の接頭辞で降順、最後に id を自動で追加)
100
+ * - filter: 項目の key ごとの絞り込み(複数の値の項目は値を含む行)
101
+ */
102
+ type ListQuery<T extends ContentType = ContentType> = Readonly<{
103
+ limit?: number;
104
+ cursor?: string | null;
105
+ order?: readonly (OrderKey<T> | `-${OrderKey<T>}`)[];
106
+ filter?: ListFilter<T>;
107
+ }>;
108
+ /** ページ送りの応答(next は次のページの cursor、最後のページは null) */
109
+ type Page<I = Item> = Readonly<{
110
+ items: readonly I[];
111
+ next: string | null;
112
+ }>;
113
+ /** どの型の一覧の指定も受け取る形(型の検査は ListQuery の側) */
114
+ type QueryShape = Readonly<{
115
+ limit?: number;
116
+ cursor?: string | null;
117
+ order?: readonly string[];
118
+ filter?: Readonly<Record<string, unknown>>;
119
+ }>;
120
+ /** 一覧の指定をクエリの組に(find・get・subPath の params、一式の path に使用) */
121
+ export declare function listParams(query: QueryShape): Record<string, string>;
122
+ /** 配信 API のパスの前半 */
123
+ export declare const DELIVERY_PATH = "/content/v1";
124
+ /** 配信 API への接続(path は /content/v1 から始まるパスとクエリ) */
125
+ type Delivery = (path: string, init?: RequestInit) => Promise<Response>;
126
+ /** 一式の経路ごとの結果(body は経路の応答の JSON) */
127
+ type BundleResult = Readonly<{
128
+ status: number;
129
+ body: unknown;
130
+ }>;
131
+ /**
132
+ * 一式の取得の結果
133
+ * - notModified: ETag が一致、etag: 応答の ETag、stale: 失敗のため最後に成功した値
134
+ * - partial: 一部の経路が 5xx の一式 x-partial(最後に成功した値や描画の結果として保存しない)
135
+ */
136
+ type Prefetched = Readonly<{
137
+ notModified: boolean;
138
+ etag: string | null;
139
+ stale: boolean;
140
+ partial: boolean;
141
+ }>;
142
+ /** 配信 API の取得の失敗(call: 失敗した経路、status: 応答のステータス) */
143
+ export declare class DeliveryError extends Error {
144
+ readonly call: string;
145
+ readonly status: number;
146
+ constructor(call: string, status: number, body: string);
147
+ }
148
+ /** 配信 API の中のパスとクエリ(一式の path と結果のキー) */
149
+ export declare const subPath: (path: string, params?: Readonly<Record<string, string>>) => string;
150
+ /**
151
+ * 配信 API の読み取り
152
+ * - drafts: 下書き・予約なども含める(プレビュー用、署名は接続の側で付与)
153
+ * - at: 公開の基準の日時(なしは今)
154
+ */
155
+ export declare function createDeliveryClient(delivery: Delivery, { drafts, at }?: {
156
+ drafts?: boolean;
157
+ at?: string;
158
+ }): {
159
+ find: <T>(path: string, params?: Readonly<Record<string, string>>) => Promise<T | undefined>;
160
+ get: <T>(path: string, params?: Readonly<Record<string, string>>) => Promise<T>;
161
+ page: <I = Readonly<Record<string, unknown>>, T extends ContentType = Readonly<{
162
+ id: string;
163
+ label: string;
164
+ description?: string;
165
+ format?: ContentFormat;
166
+ publishing?: Publishing;
167
+ table?: string;
168
+ core?: readonly FieldDef[];
169
+ fields: readonly FieldDef[];
170
+ pages?: readonly PageDef[];
171
+ counts?: readonly CountDef[];
172
+ og?: string;
173
+ }>>(path: string, query: ListQuery<T> & Readonly<{
174
+ limit: number;
175
+ }>) => Promise<Page<I>>;
176
+ prefetch: (paths: readonly string[], { ifNoneMatch }?: {
177
+ ifNoneMatch?: string;
178
+ }) => Promise<Prefetched>;
179
+ };
180
+ /** 配信 API の読み取り(createDeliveryClient の戻り値) */
181
+ type DeliveryClient = ReturnType<typeof createDeliveryClient>;
182
+ /** 配信する画像の URL(元の URL と、元画像の画素での切り抜きの範囲から、配信の側で変換する URL) */
183
+ type ImageUrl = (src: string, rect?: CropRect) => string;
184
+ /** 実体から表示の素材(見つからない実体はなし) */
185
+ type MediaOf = (asset: unknown) => Media | undefined;
186
+ /**
187
+ * 実体から表示の素材への変換(動画は代わりの画像も)
188
+ * - base: 実体の URL の前半(URL は base/<ID>)
189
+ * - 画像の変換: imageUrl があれば画像(動画の実体は除く)を変換の URL に、切り抜きは切り抜いた画像の URL・寸法・見せたい点
190
+ * - imageUrl なし: 元の画像のまま(手元の開発)
191
+ */
192
+ export declare function mediaOf(base: string, imageUrl?: ImageUrl): MediaOf;
193
+ /** 選択肢の項目から表示名の表(値 → 表示名、key はまとまりの中なら `links.kind` の形、選択肢の項目でなければ空) */
194
+ export declare const optionLabels: <K extends string = string>(schema: ContentModel, type: string, key: string) => Readonly<Record<K, string>>;
195
+ /** 型の項目(コアの項目と追加の項目から、なければ例外: 導入先の設定の誤り) */
196
+ export declare function fieldOf(schema: ContentModel, type: string, key: string): FieldDef;
197
+ /** 項目の宣言の上限(宣言がなければ例外) */
198
+ export declare function limitOf(field: FieldDef, name: 'maxItems' | 'maxLength'): number;
199
+ /** ページのパス(型の宣言 pages から、ページのない型は例外) */
200
+ export declare function pagePath(schema: ContentModel, type: string, entry: PageEntry): string;
201
+ /** 媒体のタイムゾーンでの日時の変換(表示用の日付と日時の組 dated を含む) */
202
+ type DeliveryClock = Clock & Readonly<{
203
+ /** 日時の項目から、表示用のタイムゾーンの日付と `ISO 8601` の日時(日時の形でない値はそのまま) */
204
+ dated: (value: string) => Readonly<{
205
+ date: DateString;
206
+ at: string;
207
+ }>;
208
+ }>;
209
+ /** 日時の変換を作成(timeZone は媒体の設定にある IANA の名前) */
210
+ export declare function createDeliveryClock(timeZone: string): DeliveryClock;
211
+ export type { BundleResult, DeliveredAsset, Delivery, DeliveryClient, DeliveryClock, FieldValue, ImageUrl, Item, ItemOf, ListFilter, ListQuery, MediaOf, Page, Prefetched };
@@ -0,0 +1,199 @@
1
+ import { c as croppedHotspot, d as createClock, i as typeOf, r as optionsOf, s as cropRect, t as pagePathOf } from "./page-UEKb7_gu.js";
2
+ // ../delivery-client/src/query.ts
3
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4
+ /** in の値の検査(コンマを含む値はクエリで区切れない) */
5
+ const joined = (values) => {
6
+ const parts = values.map(String);
7
+ const bad = parts.find((p) => p.includes(","));
8
+ if (bad !== void 0) throw new Error(`in の値にコンマは不可: ${bad}`);
9
+ return parts.join(",");
10
+ };
11
+ /** 一覧の指定をクエリの組に(find・get・subPath の params、一式の path に使用) */
12
+ function listParams(query) {
13
+ const params = {};
14
+ for (const [key, value] of Object.entries(query.filter ?? {})) {
15
+ if (value === void 0) continue;
16
+ if (!isRecord(value)) {
17
+ params[`filter[${key}]`] = String(value);
18
+ continue;
19
+ }
20
+ if ("in" in value) params[`filter[${key}][in]`] = joined(value.in);
21
+ for (const op of ["gte", "lte"]) if (value[op] !== void 0) params[`filter[${key}][${op}]`] = String(value[op]);
22
+ }
23
+ if (query.order?.length) params.order = query.order.join(",");
24
+ if (query.limit !== void 0) params.limit = String(query.limit);
25
+ if (query.cursor) params.cursor = query.cursor;
26
+ return params;
27
+ }
28
+ // ../delivery-client/src/client.ts
29
+ /** 配信 API のパスの前半 */
30
+ const DELIVERY_PATH = "/content/v1";
31
+ /** 配信 API の取得の失敗(call: 失敗した経路、status: 応答のステータス) */
32
+ var DeliveryError = class extends Error {
33
+ call;
34
+ status;
35
+ constructor(call, status, body) {
36
+ super(`配信 API ${call}: ${status}${body ? ` ${body}` : ""}`);
37
+ this.name = "DeliveryError";
38
+ this.call = call;
39
+ this.status = status;
40
+ }
41
+ };
42
+ /** 配信 API の中のパスとクエリ(一式の path と結果のキー) */
43
+ const subPath = (path, params = {}) => {
44
+ const q = new URLSearchParams(params).toString();
45
+ return `${path}${q ? `?${q}` : ""}`;
46
+ };
47
+ /**
48
+ * 配信 API の読み取り
49
+ * - drafts: 下書き・予約なども含める(プレビュー用、署名は接続の側で付与)
50
+ * - at: 公開の基準の日時(なしは今)
51
+ */
52
+ function createDeliveryClient(delivery, { drafts = false, at } = {}) {
53
+ const query = (params = {}) => new URLSearchParams({
54
+ ...params,
55
+ ...drafts ? { drafts: "1" } : {},
56
+ ...at ? { at } : {}
57
+ });
58
+ const url = (path, params) => {
59
+ const q = query(params).toString();
60
+ return `${DELIVERY_PATH}${path}${q ? `?${q}` : ""}`;
61
+ };
62
+ let bundled = /* @__PURE__ */ new Map();
63
+ /** 取得(404は undefined、ほかの失敗は例外、一式で取得済みならその結果) */
64
+ const find = async (path, params = {}) => {
65
+ const hit = bundled.get(subPath(path, params));
66
+ if (hit) {
67
+ if (hit.status === 404) return void 0;
68
+ if (hit.status < 200 || hit.status >= 300) throw new DeliveryError(path, hit.status, JSON.stringify(hit.body));
69
+ return hit.body;
70
+ }
71
+ const res = await delivery(url(path, params));
72
+ if (res.status === 404) return void 0;
73
+ if (!res.ok) throw new DeliveryError(path, res.status, await res.text());
74
+ return await res.json();
75
+ };
76
+ /** 取得(404も例外) */
77
+ const get = async (path, params) => {
78
+ const v = await find(path, params);
79
+ if (v === void 0) throw new DeliveryError(path, 404, "");
80
+ return v;
81
+ };
82
+ /**
83
+ * 型の一覧の1ページ(path は型の一覧の経路、例: /post)
84
+ * - query: 絞り込み・並べ替えと limit、次のページは前の応答の next を cursor に
85
+ * - 一式で同じ path とクエリを取得済みならその結果(一式の path は `subPath(path, listParams(query))`)
86
+ */
87
+ const page = (path, query) => get(path, listParams(query));
88
+ /**
89
+ * 経路の一覧を一式の1回で取得(paths は配信 API の中のパスとクエリ、`*` の区画は一覧の id ごとに展開)
90
+ * - ifNoneMatch: 前回の ETag(一致すれば notModified で、結果は未取得)
91
+ */
92
+ const prefetch = async (paths, { ifNoneMatch } = {}) => {
93
+ const q = query();
94
+ for (const p of paths) q.append("path", p);
95
+ const res = await delivery(`${DELIVERY_PATH}/bundle?${q}`, ifNoneMatch ? { headers: { "if-none-match": ifNoneMatch } } : {});
96
+ if (res.status === 304) return {
97
+ notModified: true,
98
+ etag: ifNoneMatch ?? null,
99
+ stale: false,
100
+ partial: false
101
+ };
102
+ if (!res.ok) throw new DeliveryError("/bundle", res.status, await res.text());
103
+ const { results } = await res.json();
104
+ bundled = new Map(Object.entries(results));
105
+ return {
106
+ notModified: false,
107
+ etag: res.headers.get("etag"),
108
+ stale: res.headers.has("x-stale"),
109
+ partial: res.headers.has("x-partial")
110
+ };
111
+ };
112
+ return {
113
+ find,
114
+ get,
115
+ page,
116
+ prefetch
117
+ };
118
+ }
119
+ // ../delivery-client/src/media.ts
120
+ /**
121
+ * 実体から表示の素材への変換(動画は代わりの画像も)
122
+ * - base: 実体の URL の前半(URL は base/<ID>)
123
+ * - 画像の変換: imageUrl があれば画像(動画の実体は除く)を変換の URL に、切り抜きは切り抜いた画像の URL・寸法・見せたい点
124
+ * - imageUrl なし: 元の画像のまま(手元の開発)
125
+ */
126
+ function mediaOf(base, imageUrl) {
127
+ const still = (a) => {
128
+ const src = `${base}/${a.id}`;
129
+ const alt = a.alt ?? "";
130
+ if (!imageUrl || a.kind !== "image") return {
131
+ src,
132
+ width: a.width,
133
+ height: a.height,
134
+ alt,
135
+ ...a.hotspot ? { hotspot: a.hotspot } : {}
136
+ };
137
+ if (!a.crop) return {
138
+ src: imageUrl(src),
139
+ width: a.width,
140
+ height: a.height,
141
+ alt,
142
+ ...a.hotspot ? { hotspot: a.hotspot } : {}
143
+ };
144
+ const rect = cropRect(a.width, a.height, a.crop);
145
+ return {
146
+ src: imageUrl(src, rect),
147
+ width: rect.width,
148
+ height: rect.height,
149
+ alt,
150
+ ...a.hotspot ? { hotspot: croppedHotspot(a.hotspot, a.crop) } : {}
151
+ };
152
+ };
153
+ return (v) => {
154
+ if (v === null || v === void 0) return void 0;
155
+ const a = v;
156
+ return a.kind === "video" && a.poster ? {
157
+ kind: "video",
158
+ ...still(a),
159
+ poster: still(a.poster)
160
+ } : {
161
+ kind: "image",
162
+ ...still(a)
163
+ };
164
+ };
165
+ }
166
+ // ../delivery-client/src/schema.ts
167
+ /** 選択肢の項目から表示名の表(値 → 表示名、key はまとまりの中なら `links.kind` の形、選択肢の項目でなければ空) */
168
+ const optionLabels = (schema, type, key) => Object.fromEntries(optionsOf(typeOf(schema, type), key).map((o) => [o.value, o.label]));
169
+ /** 型の項目(コアの項目と追加の項目から、なければ例外: 導入先の設定の誤り) */
170
+ function fieldOf(schema, type, key) {
171
+ const t = typeOf(schema, type);
172
+ const field = [...t.core ?? [], ...t.fields].find((f) => f.key === key);
173
+ if (!field) throw new Error(`型 ${type} に項目 ${key} がない`);
174
+ return field;
175
+ }
176
+ /** 項目の宣言の上限(宣言がなければ例外) */
177
+ function limitOf(field, name) {
178
+ const value = field[name];
179
+ if (value === void 0) throw new Error(`項目 ${field.key} に ${name} の宣言がない`);
180
+ return value;
181
+ }
182
+ /** ページのパス(型の宣言 pages から、ページのない型は例外) */
183
+ function pagePath(schema, type, entry) {
184
+ const path = pagePathOf(typeOf(schema, type), entry);
185
+ if (path === null) throw new Error(`サイトのページがない型: ${type}`);
186
+ return path;
187
+ }
188
+ /** 日時の変換を作成(timeZone は媒体の設定にある IANA の名前) */
189
+ function createDeliveryClock(timeZone) {
190
+ const clock = createClock(timeZone);
191
+ return {
192
+ ...clock,
193
+ dated: (value) => ({
194
+ date: clock.dateOf(value),
195
+ at: clock.toDateTime(value) ?? value
196
+ })
197
+ };
198
+ }
199
+ export { DELIVERY_PATH, DeliveryError, createDeliveryClient, createDeliveryClock, fieldOf, limitOf, listParams, mediaOf, optionLabels, pagePath, subPath };