mechanica-shared 2.0.0-alpha.9 → 2.0.2

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.
@@ -1,422 +0,0 @@
1
- import { Document, isCollection, isMap, isScalar, isSeq, parse as parseYaml } from 'yaml'
2
- import type { ContentBlock } from './types'
3
-
4
- /**
5
- * A parsed page document — the page envelope (everything but `content`) plus the
6
- * content tree. This is the in-memory shape the `.page.md` codec maps to/from;
7
- * it mirrors the dev store's `PageFile`.
8
- */
9
- export interface PageDoc {
10
- name?: string
11
- /**
12
- * A work-in-progress page. Drafts render and edit normally in the dev server
13
- * but are hidden from queries (`usePages`/`usePagination`) and skipped by the
14
- * static export. Absent/false for published pages.
15
- */
16
- draft?: boolean
17
- meta?: Record<string, unknown>
18
- /** Page-scoped data overrides (defineData). */
19
- data: Record<string, unknown>
20
- content: ContentBlock[]
21
- order?: number
22
- orderAfter?: string | null
23
- path?: string
24
- }
25
-
26
- /**
27
- * Adapter that lets the codec store rich-text fields as Markdown on disk while
28
- * keeping them as vuewrite `Block[]` JSON in page state. Supplied by the caller
29
- * (dev server / CLI) so this module stays DOM- and vuewrite-free.
30
- */
31
- export interface RichTextCodec {
32
- /** Whether a top-level prop of the given block is a rich-text field. */
33
- isRichText(blockId: string, prop: string): boolean
34
- /** vuewrite `Block[]` → Markdown (written to disk). */
35
- toMarkdown(blocks: unknown): string
36
- /** Markdown → vuewrite `Block[]` (loaded into state). */
37
- toBlocks(markdown: string): unknown
38
- }
39
-
40
- /** Options for {@link parsePage} / {@link serializePage}. */
41
- export interface PageCodecOptions {
42
- /** Rich-text ⇄ Markdown adapter; when omitted, regions stay plain strings. */
43
- richText?: RichTextCodec
44
- }
45
-
46
- /** Thrown by {@link parsePage} with the 1-based source line of the problem. */
47
- export class PageParseError extends Error {
48
- constructor(
49
- message: string,
50
- public readonly line: number,
51
- ) {
52
- super(`${message} (line ${line})`)
53
- this.name = 'PageParseError'
54
- Object.setPrototypeOf(this, PageParseError.prototype)
55
- }
56
- }
57
-
58
- // ── Tokenizer ────────────────────────────────────────────────────────────────
59
- // Structural tokens are recognized ONLY at column 0 (no leading whitespace), so
60
- // indented YAML block-scalar / nested-mapping content in a head is never mistaken
61
- // for a fence. Nesting comes from `:::` pairing, not indentation.
62
-
63
- type Token =
64
- | { kind: 'open'; blockId: string; rest: string }
65
- | { kind: 'close'; id?: string }
66
- | { kind: 'field'; name: string }
67
-
68
- const CLOSE_BARE = /^:::[ \t]*$/
69
- const CLOSE_LABELED = /^:::[ \t]*\/[ \t]*([A-Za-z][\w-]*)?[ \t]*$/
70
- const OPEN = /^:::[ \t]+([A-Za-z][\w-]*)[ \t]*(.*)$/
71
- const FIELD = /^@([A-Za-z][\w.-]*)[ \t]*$/
72
-
73
- /** Classify a line as a structural token, or null when it's head/region content. */
74
- function classify(line: string): Token | null {
75
- if (/^[ \t]/.test(line)) return null // indented → content, never a token
76
- let m: RegExpExecArray | null
77
- if (CLOSE_BARE.test(line)) return { kind: 'close' }
78
- if ((m = CLOSE_LABELED.exec(line))) return { kind: 'close', id: m[1] || undefined }
79
- if ((m = OPEN.exec(line))) return { kind: 'open', blockId: m[1]!, rest: m[2]! }
80
- if ((m = FIELD.exec(line))) return { kind: 'field', name: m[1]! }
81
- return null
82
- }
83
-
84
- /** Leading run of backticks/tildes that opens or closes a Markdown code fence. */
85
- function matchFence(line: string): string | null {
86
- const m = /^[ \t]*([`~]{3,})/.exec(line)
87
- return m ? m[1]! : null
88
- }
89
-
90
- /** Strip the common leading indentation from a block of lines (blank lines ignored). */
91
- function dedent(lines: string[]): string[] {
92
- let min = Infinity
93
- for (const line of lines) {
94
- if (line.trim() === '') continue
95
- min = Math.min(min, /^[ \t]*/.exec(line)![0].length)
96
- }
97
- if (!isFinite(min) || min === 0) return lines.slice()
98
- return lines.map((line) => (line.trim() === '' ? line.trimStart() : line.slice(min)))
99
- }
100
-
101
- /** Set `data[a.b.c] = value`, creating intermediate objects. */
102
- function setPath(data: Record<string, unknown>, path: string, value: unknown): void {
103
- const parts = path.split('.')
104
- let target = data
105
- for (let i = 0; i < parts.length - 1; i++) {
106
- const key = parts[i]!
107
- if (typeof target[key] !== 'object' || target[key] === null) target[key] = {}
108
- target = target[key] as Record<string, unknown>
109
- }
110
- target[parts[parts.length - 1]!] = value
111
- }
112
-
113
- interface Frame {
114
- block: ContentBlock
115
- headBuf: string[]
116
- region: { name: string; lines: string[]; fence: string | null } | null
117
- }
118
-
119
- /** Parse a `.page.md` document into a {@link PageDoc}. */
120
- export function parsePage(text: string, options?: PageCodecOptions): PageDoc {
121
- const lines = text.replace(/\r\n/g, '\n').split('\n')
122
- let i = 0
123
-
124
- // 1. Frontmatter (the page envelope).
125
- let envelope: Record<string, unknown> = {}
126
- if (lines[0] === '---') {
127
- let j = 1
128
- while (j < lines.length && lines[j] !== '---') j++
129
- if (j >= lines.length) throw new PageParseError('Unterminated frontmatter (missing closing ---)', 1)
130
- try {
131
- envelope = (parseYaml(lines.slice(1, j).join('\n')) as Record<string, unknown>) ?? {}
132
- } catch (error) {
133
- throw new PageParseError(`Invalid frontmatter YAML: ${(error as Error).message}`, 1)
134
- }
135
- i = j + 1
136
- }
137
-
138
- // 2. Block fences (the content tree).
139
- const root: ContentBlock[] = []
140
- const stack: Frame[] = []
141
- const counter = { n: 0 }
142
-
143
- const flushHead = (frame: Frame, line: number): void => {
144
- if (frame.headBuf.length === 0) return
145
- const yamlText = dedent(frame.headBuf).join('\n')
146
- frame.headBuf = []
147
- if (yamlText.trim() === '') return
148
- let parsed: unknown
149
- try {
150
- parsed = parseYaml(yamlText)
151
- } catch (error) {
152
- throw new PageParseError(`Invalid block head YAML: ${(error as Error).message}`, line)
153
- }
154
- if (parsed && typeof parsed === 'object') Object.assign(frame.block.data, parsed)
155
- }
156
-
157
- const finalizeRegion = (frame: Frame): void => {
158
- const region = frame.region!
159
- const body = dedent(region.lines)
160
- while (body.length && body[0]!.trim() === '') body.shift()
161
- while (body.length && body[body.length - 1]!.trim() === '') body.pop()
162
- const raw = body.join('\n')
163
- const value = options?.richText?.isRichText(frame.block.blockId, region.name)
164
- ? options.richText.toBlocks(raw)
165
- : raw
166
- setPath(frame.block.data, region.name, value)
167
- frame.region = null
168
- }
169
-
170
- for (; i < lines.length; i++) {
171
- const line = lines[i]!
172
- const frame = stack[stack.length - 1]
173
-
174
- // Region capture: a @field is open on the current block.
175
- if (frame?.region) {
176
- const region = frame.region
177
- const fence = matchFence(line)
178
- if (region.fence !== null) {
179
- region.lines.push(line)
180
- if (fence && fence.length >= region.fence.length && line.trimStart().startsWith(region.fence)) {
181
- region.fence = null
182
- }
183
- continue
184
- }
185
- if (fence) {
186
- region.lines.push(line)
187
- region.fence = fence
188
- continue
189
- }
190
- if (line.startsWith('\\:::') || /^\\@[A-Za-z]/.test(line)) {
191
- region.lines.push(line.slice(1)) // de-escape a literal line-initial token
192
- continue
193
- }
194
- if (classify(line) === null) {
195
- region.lines.push(line)
196
- continue
197
- }
198
- finalizeRegion(frame) // a structural token ends the region; fall through to handle it
199
- }
200
-
201
- const token = classify(line)
202
- if (token === null) {
203
- if (!frame) {
204
- if (line.trim() === '') continue
205
- throw new PageParseError(`Content outside any block: "${line.trim()}"`, i + 1)
206
- }
207
- frame.headBuf.push(line)
208
- continue
209
- }
210
-
211
- if (token.kind === 'open') {
212
- if (frame) flushHead(frame, i + 1)
213
- const { id, slot, v } = parseAttrs(token.rest, i + 1)
214
- const block: ContentBlock = { id: id ?? `auto${++counter.n}`, blockId: token.blockId, data: {} }
215
- if (v !== undefined) block.v = v
216
- attach(root, frame, block, slot, i + 1)
217
- stack.push({ block, headBuf: [], region: null })
218
- } else if (token.kind === 'field') {
219
- if (!frame) throw new PageParseError(`@${token.name} outside any block`, i + 1)
220
- flushHead(frame, i + 1)
221
- frame.region = { name: token.name, lines: [], fence: null }
222
- } else {
223
- if (!frame) throw new PageParseError('Close ::: with no open block', i + 1)
224
- flushHead(frame, i + 1)
225
- if (token.id && token.id !== frame.block.blockId) {
226
- throw new PageParseError(
227
- `Labeled close "::: /${token.id}" does not match open block "::: ${frame.block.blockId}"`,
228
- i + 1,
229
- )
230
- }
231
- stack.pop()
232
- }
233
- }
234
-
235
- if (stack.length) {
236
- throw new PageParseError(`Unclosed block "::: ${stack[stack.length - 1]!.block.blockId}"`, lines.length)
237
- }
238
-
239
- return {
240
- ...(typeof envelope.name === 'string' ? { name: envelope.name } : {}),
241
- ...(envelope.draft === true ? { draft: true } : {}),
242
- ...(envelope.meta !== undefined ? { meta: envelope.meta as Record<string, unknown> } : {}),
243
- ...(typeof envelope.order === 'number' ? { order: envelope.order } : {}),
244
- ...(envelope.orderAfter != null ? { orderAfter: envelope.orderAfter as string } : {}),
245
- ...(typeof envelope.path === 'string' ? { path: envelope.path } : {}),
246
- data: (envelope.data as Record<string, unknown>) ?? {},
247
- content: root,
248
- }
249
- }
250
-
251
- /** Parse the `#id` and `key=value` attributes after a block id on an open fence. */
252
- function parseAttrs(rest: string, line: number): { id?: string; slot?: string; v?: number } {
253
- const out: { id?: string; slot?: string; v?: number } = {}
254
- for (const part of rest.trim().split(/[ \t]+/).filter(Boolean)) {
255
- if (part.startsWith('#')) out.id = part.slice(1)
256
- else if (part.startsWith('slot=')) out.slot = part.slice(5)
257
- else if (/^v=\d+$/.test(part)) out.v = Number(part.slice(2))
258
- else throw new PageParseError(`Unknown block attribute "${part}"`, line)
259
- }
260
- return out
261
- }
262
-
263
- /** Place a freshly-opened block into its parent slot (or the page root). */
264
- function attach(
265
- root: ContentBlock[],
266
- parent: Frame | undefined,
267
- block: ContentBlock,
268
- slot: string | undefined,
269
- line: number,
270
- ): void {
271
- if (!parent) {
272
- if (slot) throw new PageParseError('A root-level block cannot have a slot', line)
273
- root.push(block)
274
- return
275
- }
276
- const owner = parent.block
277
- if (slot) {
278
- if (owner.children == null) owner.children = {}
279
- if (Array.isArray(owner.children)) {
280
- throw new PageParseError(`Block "::: ${owner.blockId}" mixes default and named slots`, line)
281
- }
282
- const map = owner.children as Record<string, ContentBlock[]>
283
- ;(map[slot] ??= []).push(block)
284
- } else {
285
- if (owner.children == null) owner.children = []
286
- if (!Array.isArray(owner.children)) {
287
- throw new PageParseError(`Block "::: ${owner.blockId}" mixes default and named slots`, line)
288
- }
289
- owner.children.push(block)
290
- }
291
- }
292
-
293
- // ── Serializer ───────────────────────────────────────────────────────────────
294
-
295
- /** A top-level string prop becomes an `@field` region past this length, or on any newline. */
296
- const REGION_THRESHOLD = 80
297
- const FLOW_MAX = 72
298
-
299
- /** Serialize a {@link PageDoc} into canonical `.page.md` text. */
300
- export function serializePage(doc: PageDoc, options?: PageCodecOptions): string {
301
- const envelope: Record<string, unknown> = {}
302
- if (doc.name !== undefined) envelope.name = doc.name
303
- // Only emit `draft` when true — published pages stay clean (like `order`).
304
- if (doc.draft) envelope.draft = true
305
- if (doc.meta !== undefined) envelope.meta = doc.meta
306
- envelope.data = doc.data ?? {}
307
- if (doc.order !== undefined) envelope.order = doc.order
308
- if (doc.orderAfter != null) envelope.orderAfter = doc.orderAfter
309
- if (doc.path !== undefined) envelope.path = doc.path
310
-
311
- const out: string[] = ['---', emitYaml(envelope), '---', '']
312
- for (const block of doc.content) emitBlock(block, undefined, out, options)
313
- return out.join('\n').replace(/\n+$/, '') + '\n'
314
- }
315
-
316
- function emitBlock(
317
- block: ContentBlock,
318
- slot: string | undefined,
319
- out: string[],
320
- options?: PageCodecOptions,
321
- ): void {
322
- let open = `::: ${block.blockId}`
323
- if (block.id) open += ` #${block.id}`
324
- if (slot) open += ` slot=${slot}`
325
- if (block.v !== undefined) open += ` v=${block.v}`
326
- out.push(open)
327
-
328
- const head: Record<string, unknown> = {}
329
- const regions: [string, string][] = []
330
- for (const [key, value] of Object.entries(block.data)) {
331
- // Rich-text fields are always written as a Markdown region (vuewrite Block[]
332
- // → Markdown), regardless of length, so they read as prose on disk.
333
- if (options?.richText?.isRichText(block.blockId, key) && Array.isArray(value)) {
334
- regions.push([key, options.richText.toMarkdown(value)])
335
- } else if (typeof value === 'string' && (value.includes('\n') || value.length > REGION_THRESHOLD)) {
336
- regions.push([key, value])
337
- } else {
338
- head[key] = value
339
- }
340
- }
341
- if (Object.keys(head).length) out.push(emitYaml(head))
342
- for (const [name, value] of regions) {
343
- out.push(`@${name}`)
344
- out.push(escapeRegion(value))
345
- }
346
-
347
- let hasChildren = false
348
- const children = block.children
349
- if (Array.isArray(children)) {
350
- for (const child of children) emitBlock(child, undefined, out, options)
351
- hasChildren = children.length > 0
352
- } else if (children && typeof children === 'object') {
353
- for (const [slotName, list] of Object.entries(children)) {
354
- for (const child of list) emitBlock(child, slotName, out, options)
355
- if (list.length) hasChildren = true
356
- }
357
- }
358
-
359
- out.push(hasChildren ? `::: /${block.blockId}` : ':::')
360
- out.push('')
361
- }
362
-
363
- /** Backslash-escape any line-initial structural token in raw region text (outside code fences). */
364
- function escapeRegion(text: string): string {
365
- let fence: string | null = null
366
- return text
367
- .split('\n')
368
- .map((line) => {
369
- const mark = matchFence(line)
370
- if (fence !== null) {
371
- if (mark && mark.length >= fence.length && line.trimStart().startsWith(fence)) fence = null
372
- return line
373
- }
374
- if (mark) {
375
- fence = mark
376
- return line
377
- }
378
- return classify(line) !== null ? '\\' + line : line
379
- })
380
- .join('\n')
381
- }
382
-
383
- /**
384
- * Emit a value as YAML. The root mapping always stays block style (one prop per
385
- * line); only nested collections collapse to flow when short and all-scalar.
386
- */
387
- function emitYaml(value: unknown): string {
388
- const doc = new Document(value)
389
- const root = doc.contents as YamlNode
390
- if (isSeq(root)) for (const item of root.items) applyFlow(item)
391
- else if (isMap(root)) for (const pair of root.items) applyFlow(pair.value)
392
- return doc.toString({ lineWidth: 0 }).replace(/\n$/, '')
393
- }
394
-
395
- type YamlNode = any
396
-
397
- function applyFlow(node: YamlNode): void {
398
- if (isSeq(node)) {
399
- for (const item of node.items) if (isCollection(item)) applyFlow(item)
400
- node.flow = node.items.length === 0 || (node.items.every(isScalar) && inlineLen(node) <= FLOW_MAX)
401
- } else if (isMap(node)) {
402
- for (const pair of node.items) if (isCollection(pair.value)) applyFlow(pair.value)
403
- node.flow =
404
- node.items.length === 0 ||
405
- (node.items.every((pair: YamlNode) => isScalar(pair.value)) && inlineLen(node) <= FLOW_MAX)
406
- }
407
- }
408
-
409
- function inlineLen(node: YamlNode): number {
410
- if (isScalar(node)) return String(node.value ?? 'null').length + 2
411
- if (isSeq(node)) return 4 + node.items.reduce((sum: number, item: YamlNode) => sum + inlineLen(item) + 2, 0)
412
- if (isMap(node)) {
413
- return (
414
- 4 +
415
- node.items.reduce(
416
- (sum: number, pair: YamlNode) => sum + String(pair.key?.value ?? pair.key).length + 2 + inlineLen(pair.value) + 2,
417
- 0,
418
- )
419
- )
420
- }
421
- return 0
422
- }
@@ -1,175 +0,0 @@
1
- import { getValueByPath } from './schema'
2
-
3
- /**
4
- * The query engine: resolves the runtime's query keys (`usePages`,
5
- * `usePagination`, `useFetch`) against an abstract {@link QuerySource}. One
6
- * implementation, three callers — the dev server (live `.mech` store), the
7
- * static export (resolved at build time and baked into `window.state.query`),
8
- * and a future hosted backend (its own source). Pure and DOM/fs-free; the
9
- * source supplies all I/O.
10
- */
11
-
12
- /** A page entry as the engine consumes it (requested data entries embedded). */
13
- export interface PageQueryItem {
14
- path: string
15
- name: string
16
- folderPath?: string | null
17
- order?: number
18
- orderAfter?: string | null
19
- /** Draft pages are dropped from query results (see {@link resolvePagesQuery}). */
20
- draft?: boolean
21
- [dataId: string]: unknown
22
- }
23
-
24
- /** What a caller must supply for the engine to resolve queries. */
25
- export interface QuerySource {
26
- /**
27
- * List all pages, with the requested page-scoped data entries embedded. With
28
- * a `locale` (a non-default code) only pages translated to it are listed, and
29
- * each item's data/name come from the translation — so a translated listing
30
- * shows translated content and never links to an untranslated page.
31
- */
32
- listPages(options: { data?: { id: string }[]; locale?: string }): PageQueryItem[]
33
- /** Fetch external JSON (`useFetch`). Omit to disable fetch queries. */
34
- fetchJson?(options: { url: string } & Record<string, unknown>): Promise<unknown>
35
- }
36
-
37
- /** Arguments of a `getPages` query (the JSON part of its key). */
38
- export interface PagesQueryArgs {
39
- /** Only pages inside this folder path (e.g. `'blog'`). */
40
- folderName?: string
41
- /** Page-scoped data entries to embed in each result. */
42
- data?: { id: string }[]
43
- /**
44
- * Sort field: `'name'`, `'path'`, or a dotted path into an included data
45
- * entry (e.g. `'postMeta.date'`). Default: the store's page order.
46
- */
47
- sort?: { by: string; dir?: 'asc' | 'desc' }
48
- /** Cap the number of results (non-paginated queries). */
49
- limit?: number
50
- /** Split results into pages of this size — the query becomes paginated. */
51
- pageSize?: number
52
- }
53
-
54
- /** The shape a paginated `getPages` query resolves to. */
55
- export interface PaginatedPagesResult {
56
- items: PageQueryItem[]
57
- /** Current page number (1-based, clamped to `pageCount`). */
58
- page: number
59
- pageCount: number
60
- pageSize: number
61
- total: number
62
- }
63
-
64
- /** Extra context for resolving a key (which paginated variant to slice). */
65
- export interface QueryContext {
66
- /** 1-based page number for paginated queries. Default 1. */
67
- page?: number
68
- /** The locale to resolve `getPages` against (multi-language sites); a non-default code. */
69
- locale?: string
70
- }
71
-
72
- /** Split a query key (`"<type>.<json-args>"`) into its type and parsed args. */
73
- export function parseQueryKey(key: string): { type: string; args: Record<string, unknown> } {
74
- const dot = key.indexOf('.')
75
- if (dot === -1) return { type: key, args: {} }
76
- const type = key.slice(0, dot)
77
- const raw = key.slice(dot + 1)
78
- try {
79
- const parsed: unknown = JSON.parse(raw || '{}')
80
- return { type, args: parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : {} }
81
- } catch {
82
- return { type, args: {} }
83
- }
84
- }
85
-
86
- /** Whether a key is a paginated `getPages` query (drives export page-splitting). */
87
- export function isPaginatedQuery(key: string): boolean {
88
- const { type, args } = parseQueryKey(key)
89
- return type === 'getPages' && typeof args.pageSize === 'number' && args.pageSize > 0
90
- }
91
-
92
- /** Compare possibly-missing values: numbers numerically, everything else as strings. */
93
- function compareValues(a: unknown, b: unknown): number {
94
- if (a == null && b == null) return 0
95
- if (a == null) return 1 // missing values sort last regardless of direction
96
- if (b == null) return -1
97
- if (typeof a === 'number' && typeof b === 'number') return a - b
98
- return String(a).localeCompare(String(b))
99
- }
100
-
101
- /**
102
- * Resolve a `getPages` query: filter by folder, sort, and either cap (`limit`)
103
- * or paginate (`pageSize`). Internal ordering fields are stripped from results.
104
- */
105
- export function resolvePagesQuery(
106
- source: QuerySource,
107
- args: PagesQueryArgs,
108
- context: QueryContext = {},
109
- ): PageQueryItem[] | PaginatedPagesResult {
110
- // Drafts are never visible to queries (`usePages`/`usePagination`), across
111
- // every caller (dev server, static export, future backend). On a translated
112
- // page the listing is resolved in that locale (translated data, translated
113
- // pages only).
114
- let pages = source.listPages({ data: args.data, locale: context.locale }).filter((page) => !page.draft)
115
-
116
- if (args.folderName) {
117
- // "Pages in this folder" means its children — the folder's own index page
118
- // (usually the page doing the listing) is excluded.
119
- const indexPath = '/' + args.folderName.replace(/^\/+|\/+$/g, '')
120
- pages = pages.filter((page) => page.folderPath === args.folderName && page.path !== indexPath)
121
- }
122
-
123
- if (args.sort?.by) {
124
- const { by, dir } = args.sort
125
- const sign = dir === 'desc' ? -1 : 1
126
- pages = pages
127
- .map((page) => ({ page, field: getValueByPath(page, by) }))
128
- .sort((a, b) => {
129
- // Missing values sort last in either direction.
130
- if (a.field == null || b.field == null) return compareValues(a.field, b.field)
131
- return compareValues(a.field, b.field) * sign
132
- })
133
- .map((entry) => entry.page)
134
- }
135
-
136
- const items = pages.map(({ order, orderAfter, folderPath, draft, ...rest }) => rest)
137
-
138
- if (args.pageSize && args.pageSize > 0) {
139
- const total = items.length
140
- const pageCount = Math.max(1, Math.ceil(total / args.pageSize))
141
- const page = Math.min(Math.max(1, context.page ?? 1), pageCount)
142
- return {
143
- items: items.slice((page - 1) * args.pageSize, page * args.pageSize),
144
- page,
145
- pageCount,
146
- pageSize: args.pageSize,
147
- total,
148
- }
149
- }
150
-
151
- return typeof args.limit === 'number' && args.limit >= 0 ? items.slice(0, args.limit) : items
152
- }
153
-
154
- /**
155
- * Resolve any query key against a source. Unknown types resolve to `{}` (the
156
- * runtime containers keep their initial shape). `fetch` keys require the
157
- * source to provide `fetchJson`.
158
- */
159
- export async function resolveQueryKey(
160
- source: QuerySource,
161
- key: string,
162
- context: QueryContext = {},
163
- ): Promise<unknown> {
164
- const { type, args } = parseQueryKey(key)
165
-
166
- if (type === 'getPages') return resolvePagesQuery(source, args as PagesQueryArgs, context)
167
-
168
- if (type === 'fetch') {
169
- if (!source.fetchJson) return {}
170
- if (typeof args.url !== 'string' || !args.url) return {}
171
- return source.fetchJson(args as { url: string })
172
- }
173
-
174
- return {}
175
- }