@uniweb/build 0.9.5 → 0.10.1
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/package.json +8 -5
- package/src/dev-backend.js +247 -0
- package/src/site/collection-processor.js +59 -6
- package/src/site/data-fetcher.js +64 -7
- package/src/site/plugin.js +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"./search": "./src/search/index.js",
|
|
21
21
|
"./import-map-plugin": "./src/import-map-plugin.js"
|
|
22
22
|
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"uniweb-dev-backend": "./src/dev-backend.js"
|
|
25
|
+
},
|
|
23
26
|
"files": [
|
|
24
27
|
"src"
|
|
25
28
|
],
|
|
@@ -54,9 +57,9 @@
|
|
|
54
57
|
"@uniweb/theming": "0.1.3"
|
|
55
58
|
},
|
|
56
59
|
"optionalDependencies": {
|
|
57
|
-
"@uniweb/runtime": "0.
|
|
58
|
-
"@uniweb/
|
|
59
|
-
"@uniweb/
|
|
60
|
+
"@uniweb/runtime": "0.8.1",
|
|
61
|
+
"@uniweb/schemas": "0.2.1",
|
|
62
|
+
"@uniweb/content-reader": "1.1.4"
|
|
60
63
|
},
|
|
61
64
|
"peerDependencies": {
|
|
62
65
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -65,7 +68,7 @@
|
|
|
65
68
|
"@tailwindcss/vite": "^4.0.0",
|
|
66
69
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
67
70
|
"vite-plugin-svgr": "^4.0.0",
|
|
68
|
-
"@uniweb/core": "0.
|
|
71
|
+
"@uniweb/core": "0.7.0"
|
|
69
72
|
},
|
|
70
73
|
"peerDependenciesMeta": {
|
|
71
74
|
"vite": {
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Dev backend for testing Uniweb sites with `supports: [where, limit, sort]`.
|
|
4
|
+
*
|
|
5
|
+
* Reads a directory of YAML collections (each subfolder is a collection,
|
|
6
|
+
* each .yml file inside is a record) and exposes them via HTTP. Evaluates
|
|
7
|
+
* where-objects on the server side using @uniweb/core's matchWhere — the
|
|
8
|
+
* exact same evaluator the runtime uses as a fallback. This lets you
|
|
9
|
+
* develop a site against a "real" backend without standing up a database.
|
|
10
|
+
*
|
|
11
|
+
* Wire format matches the framework default fetcher's pushdown conventions
|
|
12
|
+
* (see framework/runtime/src/default-fetcher.js):
|
|
13
|
+
*
|
|
14
|
+
* GET /api/{collection} — full collection
|
|
15
|
+
* GET /api/{collection}?_where=<JSON> — filtered by where-object
|
|
16
|
+
* GET /api/{collection}?_limit=N — first N records
|
|
17
|
+
* GET /api/{collection}?_sort=field:dir — sorted
|
|
18
|
+
* POST /api/{collection} body: { where, ... } — operators in body
|
|
19
|
+
* GET /api/{collection}/{slug} — single record
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* node scripts/framework/dev-backend.js --collections <path> [--port N]
|
|
23
|
+
*
|
|
24
|
+
* Example (academic-metrics):
|
|
25
|
+
* node scripts/framework/dev-backend.js \
|
|
26
|
+
* --collections framework/templates/academic-metrics/site/collections \
|
|
27
|
+
* --port 8080
|
|
28
|
+
*
|
|
29
|
+
* Then in the site's site.yml:
|
|
30
|
+
* fetcher:
|
|
31
|
+
* baseUrl: http://localhost:8080
|
|
32
|
+
* supports: [where, limit, sort]
|
|
33
|
+
*
|
|
34
|
+
* And rewrite collection refs to URLs, e.g.:
|
|
35
|
+
* fetch: { url: /api/members, schema: members }
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { createServer } from 'node:http'
|
|
39
|
+
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
40
|
+
import { existsSync } from 'node:fs'
|
|
41
|
+
import { resolve, join, basename, extname } from 'node:path'
|
|
42
|
+
import { parseArgs } from 'node:util'
|
|
43
|
+
import yaml from 'js-yaml'
|
|
44
|
+
import { matchWhere } from '@uniweb/core'
|
|
45
|
+
|
|
46
|
+
const { values } = parseArgs({
|
|
47
|
+
options: {
|
|
48
|
+
collections: { type: 'string', short: 'c' },
|
|
49
|
+
port: { type: 'string', short: 'p', default: '8080' },
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
if (!values.collections) {
|
|
54
|
+
console.error('Usage: dev-backend.js --collections <path> [--port N]')
|
|
55
|
+
process.exit(1)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const COLLECTIONS_DIR = resolve(values.collections)
|
|
59
|
+
const PORT = Number(values.port)
|
|
60
|
+
|
|
61
|
+
if (!existsSync(COLLECTIONS_DIR)) {
|
|
62
|
+
console.error(`Collections directory not found: ${COLLECTIONS_DIR}`)
|
|
63
|
+
process.exit(1)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Load collections from disk ─────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
async function loadCollection(dir) {
|
|
69
|
+
const files = await readdir(dir)
|
|
70
|
+
const items = []
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
const ext = extname(file).toLowerCase()
|
|
73
|
+
if (!['.yml', '.yaml', '.json'].includes(ext)) continue
|
|
74
|
+
const filepath = join(dir, file)
|
|
75
|
+
const content = await readFile(filepath, 'utf8')
|
|
76
|
+
let data
|
|
77
|
+
try {
|
|
78
|
+
data = ext === '.json' ? JSON.parse(content) : yaml.load(content)
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.warn(`[dev-backend] Failed to parse ${filepath}: ${err.message}`)
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
if (data == null) continue
|
|
84
|
+
const slug = basename(file, ext)
|
|
85
|
+
if (Array.isArray(data)) {
|
|
86
|
+
// Array-form file: each element is a record.
|
|
87
|
+
for (const record of data) {
|
|
88
|
+
if (record && typeof record === 'object') items.push(record)
|
|
89
|
+
}
|
|
90
|
+
} else if (typeof data === 'object') {
|
|
91
|
+
items.push({ slug, ...data })
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return items
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function loadAllCollections() {
|
|
98
|
+
const entries = await readdir(COLLECTIONS_DIR)
|
|
99
|
+
const collections = {}
|
|
100
|
+
for (const name of entries) {
|
|
101
|
+
const fullPath = join(COLLECTIONS_DIR, name)
|
|
102
|
+
const s = await stat(fullPath)
|
|
103
|
+
if (!s.isDirectory()) continue
|
|
104
|
+
collections[name] = await loadCollection(fullPath)
|
|
105
|
+
console.log(`[dev-backend] Loaded ${collections[name].length} items from "${name}"`)
|
|
106
|
+
}
|
|
107
|
+
return collections
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Operator handling (mirrors default-fetcher pushdown wire format) ───────
|
|
111
|
+
|
|
112
|
+
function applyOperators(items, operators) {
|
|
113
|
+
let result = items
|
|
114
|
+
if (operators.where) {
|
|
115
|
+
result = matchWhere(operators.where, result)
|
|
116
|
+
}
|
|
117
|
+
if (operators.sort) {
|
|
118
|
+
result = applySort(result, operators.sort)
|
|
119
|
+
}
|
|
120
|
+
if (typeof operators.limit === 'number' && operators.limit > 0) {
|
|
121
|
+
result = result.slice(0, operators.limit)
|
|
122
|
+
}
|
|
123
|
+
return result
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function applySort(items, sortExpr) {
|
|
127
|
+
const sorts = String(sortExpr).split(',').map((s) => {
|
|
128
|
+
const [field, dir = 'asc'] = s.trim().split(/\s+/)
|
|
129
|
+
return { field, desc: dir.toLowerCase() === 'desc' }
|
|
130
|
+
})
|
|
131
|
+
return [...items].sort((a, b) => {
|
|
132
|
+
for (const { field, desc } of sorts) {
|
|
133
|
+
const av = a?.[field] ?? ''
|
|
134
|
+
const bv = b?.[field] ?? ''
|
|
135
|
+
if (av < bv) return desc ? 1 : -1
|
|
136
|
+
if (av > bv) return desc ? -1 : 1
|
|
137
|
+
}
|
|
138
|
+
return 0
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseOperatorsFromQuery(searchParams) {
|
|
143
|
+
const out = {}
|
|
144
|
+
if (searchParams.has('_where')) {
|
|
145
|
+
try {
|
|
146
|
+
out.where = JSON.parse(searchParams.get('_where'))
|
|
147
|
+
} catch (err) {
|
|
148
|
+
throw new Error(`Invalid _where JSON: ${err.message}`)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (searchParams.has('_limit')) {
|
|
152
|
+
out.limit = Number(searchParams.get('_limit'))
|
|
153
|
+
}
|
|
154
|
+
if (searchParams.has('_sort')) {
|
|
155
|
+
out.sort = searchParams.get('_sort')
|
|
156
|
+
}
|
|
157
|
+
return out
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function readJsonBody(req) {
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
let body = ''
|
|
163
|
+
req.on('data', (chunk) => { body += chunk })
|
|
164
|
+
req.on('end', () => {
|
|
165
|
+
if (!body) return resolve({})
|
|
166
|
+
try { resolve(JSON.parse(body)) }
|
|
167
|
+
catch (err) { reject(new Error(`Invalid JSON body: ${err.message}`)) }
|
|
168
|
+
})
|
|
169
|
+
req.on('error', reject)
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── HTTP server ────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
function send(res, status, body) {
|
|
176
|
+
res.writeHead(status, {
|
|
177
|
+
'Content-Type': 'application/json',
|
|
178
|
+
'Access-Control-Allow-Origin': '*',
|
|
179
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
180
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
181
|
+
})
|
|
182
|
+
res.end(typeof body === 'string' ? body : JSON.stringify(body))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function handleRequest(req, res, collections) {
|
|
186
|
+
if (req.method === 'OPTIONS') return send(res, 204, '')
|
|
187
|
+
|
|
188
|
+
const url = new URL(req.url, `http://${req.headers.host}`)
|
|
189
|
+
const match = url.pathname.match(/^\/api\/([^/]+)(?:\/([^/]+))?$/)
|
|
190
|
+
if (!match) return send(res, 404, { error: 'Not found' })
|
|
191
|
+
|
|
192
|
+
const [, collectionName, slug] = match
|
|
193
|
+
const items = collections[collectionName]
|
|
194
|
+
if (!items) return send(res, 404, { error: `Unknown collection: ${collectionName}` })
|
|
195
|
+
|
|
196
|
+
// Single record by slug.
|
|
197
|
+
if (slug) {
|
|
198
|
+
const item = items.find((r) => r?.slug === slug)
|
|
199
|
+
if (!item) return send(res, 404, { error: `No record with slug "${slug}"` })
|
|
200
|
+
return send(res, 200, item)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Collection — apply operators from query string (GET) or body (POST).
|
|
204
|
+
let operators
|
|
205
|
+
try {
|
|
206
|
+
operators = req.method === 'POST'
|
|
207
|
+
? await readJsonBody(req)
|
|
208
|
+
: parseOperatorsFromQuery(url.searchParams)
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return send(res, 400, { error: err.message })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let result
|
|
214
|
+
try {
|
|
215
|
+
result = applyOperators(items, operators)
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return send(res, 400, { error: `Operator evaluation failed: ${err.message}` })
|
|
218
|
+
}
|
|
219
|
+
return send(res, 200, result)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Boot ───────────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
const collections = await loadAllCollections()
|
|
225
|
+
const knownCollections = Object.keys(collections)
|
|
226
|
+
if (knownCollections.length === 0) {
|
|
227
|
+
console.warn('[dev-backend] No collections found.')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const server = createServer((req, res) => {
|
|
231
|
+
handleRequest(req, res, collections).catch((err) => {
|
|
232
|
+
console.error('[dev-backend] Request handler threw:', err)
|
|
233
|
+
send(res, 500, { error: 'Internal server error' })
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
server.listen(PORT, () => {
|
|
238
|
+
console.log(`[dev-backend] Listening on http://localhost:${PORT}`)
|
|
239
|
+
console.log(`[dev-backend] Collections: ${knownCollections.join(', ') || '(none)'}`)
|
|
240
|
+
console.log('[dev-backend] Endpoints:')
|
|
241
|
+
for (const name of knownCollections) {
|
|
242
|
+
console.log(` GET /api/${name} — full collection`)
|
|
243
|
+
console.log(` GET /api/${name}?_where=<JSON> — filtered`)
|
|
244
|
+
console.log(` GET /api/${name}/{slug} — single record`)
|
|
245
|
+
console.log(` POST /api/${name} body: { where } — operators in body`)
|
|
246
|
+
}
|
|
247
|
+
})
|
|
@@ -79,7 +79,8 @@ function parseCollectionConfig(name, config) {
|
|
|
79
79
|
sort: null,
|
|
80
80
|
filter: null,
|
|
81
81
|
limit: 0,
|
|
82
|
-
excerpt: { maxLength: 160 }
|
|
82
|
+
excerpt: { maxLength: 160 },
|
|
83
|
+
deferred: null,
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -93,7 +94,24 @@ function parseCollectionConfig(name, config) {
|
|
|
93
94
|
excerpt: {
|
|
94
95
|
maxLength: config.excerpt?.maxLength || 160,
|
|
95
96
|
field: config.excerpt?.field || null
|
|
96
|
-
}
|
|
97
|
+
},
|
|
98
|
+
// `deferred:` lists fields that are heavy (article body, full nested
|
|
99
|
+
// arrays). Those fields are stripped from the cascade payload that
|
|
100
|
+
// ships with `data: <name>` declarations, and per-record full files
|
|
101
|
+
// are emitted at public/data/<name>/<slug>.json. Components that
|
|
102
|
+
// need the full record fetch the per-record file on demand, either
|
|
103
|
+
// automatically on dynamic-route pages (entity-store routes the
|
|
104
|
+
// singular detail there) or via the kit's useEntityDetail hook.
|
|
105
|
+
deferred: Array.isArray(config.deferred) ? config.deferred.slice() : null,
|
|
106
|
+
// `queryable:` declares the queryable surface — which fields a
|
|
107
|
+
// foundation can offer for filtering UI, with their type and
|
|
108
|
+
// type-specific metadata (enum options, range bounds). Foundations
|
|
109
|
+
// read this metadata via the kit's useCollectionQueryable hook to
|
|
110
|
+
// render filter controls and compose where-objects from user
|
|
111
|
+
// interactions. The framework doesn't validate the shape here —
|
|
112
|
+
// foundations get whatever the author wrote; documentation defines
|
|
113
|
+
// the conventional types (enum/boolean/range/text).
|
|
114
|
+
queryable: (config.queryable && typeof config.queryable === 'object') ? config.queryable : null,
|
|
97
115
|
}
|
|
98
116
|
}
|
|
99
117
|
|
|
@@ -578,7 +596,7 @@ export async function processCollections(siteDir, collectionsConfig, collections
|
|
|
578
596
|
* })
|
|
579
597
|
* // Creates public/data/articles.json
|
|
580
598
|
*/
|
|
581
|
-
export async function writeCollectionFiles(siteDir, collections) {
|
|
599
|
+
export async function writeCollectionFiles(siteDir, collections, collectionsConfig = null) {
|
|
582
600
|
if (!collections || Object.keys(collections).length === 0) {
|
|
583
601
|
return
|
|
584
602
|
}
|
|
@@ -587,9 +605,44 @@ export async function writeCollectionFiles(siteDir, collections) {
|
|
|
587
605
|
await mkdir(dataDir, { recursive: true })
|
|
588
606
|
|
|
589
607
|
for (const [name, items] of Object.entries(collections)) {
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
608
|
+
const rawConfig = collectionsConfig?.[name]
|
|
609
|
+
const parsed = rawConfig ? parseCollectionConfig(name, rawConfig) : null
|
|
610
|
+
const deferred = parsed?.deferred
|
|
611
|
+
|
|
612
|
+
if (deferred && deferred.length > 0) {
|
|
613
|
+
// `deferred:` is set — emit two payloads:
|
|
614
|
+
// 1. The cascade JSON at /data/<name>.json with deferred fields stripped.
|
|
615
|
+
// This is what `data: <name>` declarations deliver everywhere.
|
|
616
|
+
// 2. Per-record full files at /data/<name>/<slug>.json with every field.
|
|
617
|
+
// Dynamic-route singular fetches and useEntityDetail hooks read these.
|
|
618
|
+
const recordsDir = join(dataDir, name)
|
|
619
|
+
await mkdir(recordsDir, { recursive: true })
|
|
620
|
+
|
|
621
|
+
let perRecordCount = 0
|
|
622
|
+
for (const item of items) {
|
|
623
|
+
if (!item || typeof item !== 'object' || !item.slug) continue
|
|
624
|
+
const recordPath = join(recordsDir, `${item.slug}.json`)
|
|
625
|
+
await writeFile(recordPath, JSON.stringify(item, null, 2))
|
|
626
|
+
perRecordCount++
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const stripped = items.map((item) => {
|
|
630
|
+
if (!item || typeof item !== 'object') return item
|
|
631
|
+
const out = { ...item }
|
|
632
|
+
for (const field of deferred) delete out[field]
|
|
633
|
+
return out
|
|
634
|
+
})
|
|
635
|
+
const cascadePath = join(dataDir, `${name}.json`)
|
|
636
|
+
await writeFile(cascadePath, JSON.stringify(stripped, null, 2))
|
|
637
|
+
console.log(
|
|
638
|
+
`[collection-processor] Generated ${cascadePath} (${items.length} items, ` +
|
|
639
|
+
`deferred: [${deferred.join(', ')}]) + ${perRecordCount} per-record files`
|
|
640
|
+
)
|
|
641
|
+
} else {
|
|
642
|
+
const filepath = join(dataDir, `${name}.json`)
|
|
643
|
+
await writeFile(filepath, JSON.stringify(items, null, 2))
|
|
644
|
+
console.log(`[collection-processor] Generated ${filepath} (${items.length} items)`)
|
|
645
|
+
}
|
|
593
646
|
}
|
|
594
647
|
}
|
|
595
648
|
|
package/src/site/data-fetcher.js
CHANGED
|
@@ -20,6 +20,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
20
20
|
import { join } from 'node:path'
|
|
21
21
|
import { existsSync } from 'node:fs'
|
|
22
22
|
import yaml from 'js-yaml'
|
|
23
|
+
import { matchWhere } from '@uniweb/core'
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Infer schema name from path or URL
|
|
@@ -157,19 +158,52 @@ export function applySort(items, sortExpr) {
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
/**
|
|
160
|
-
* Apply
|
|
161
|
+
* Apply a where-object predicate to an array of items.
|
|
162
|
+
*
|
|
163
|
+
* The where-object is the new query language (see @uniweb/core's
|
|
164
|
+
* matchWhere and the architecture doc at
|
|
165
|
+
* kb/framework/architecture/data-fetching.md). Structured JSON
|
|
166
|
+
* predicate; the runtime evaluator walks the object against each
|
|
167
|
+
* record. Same shape ships to backends that declare `supports: [where]`.
|
|
168
|
+
*
|
|
169
|
+
* @param {Array} items - Items to filter
|
|
170
|
+
* @param {object} where - Where-object predicate
|
|
171
|
+
* @returns {Array} Filtered items in source order
|
|
172
|
+
*/
|
|
173
|
+
export function applyWhere(items, where) {
|
|
174
|
+
if (!where || !Array.isArray(items)) return items
|
|
175
|
+
return matchWhere(where, items)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Apply post-processing to fetched data (where, filter, sort, limit)
|
|
180
|
+
*
|
|
181
|
+
* Order of operations:
|
|
182
|
+
* 1. where (where-object predicate, new) — narrows the record set
|
|
183
|
+
* 2. filter (legacy DSL string) — narrows further if both are set; deprecated
|
|
184
|
+
* 3. sort
|
|
185
|
+
* 4. limit
|
|
186
|
+
*
|
|
187
|
+
* `where:` and `filter:` may both appear during the deprecation window
|
|
188
|
+
* but in practice authors should pick one. Using `filter:` emits a dev
|
|
189
|
+
* warning at parse time (see parseFetchConfig).
|
|
161
190
|
*
|
|
162
191
|
* @param {any} data - Fetched data
|
|
163
|
-
* @param {object} config - Fetch config with optional filter, sort, limit
|
|
192
|
+
* @param {object} config - Fetch config with optional where, filter, sort, limit
|
|
164
193
|
* @returns {any} Processed data
|
|
165
194
|
*/
|
|
166
195
|
export function applyPostProcessing(data, config) {
|
|
167
196
|
if (!data || !Array.isArray(data)) return data
|
|
168
|
-
if (!config.filter && !config.sort && !config.limit) return data
|
|
197
|
+
if (!config.where && !config.filter && !config.sort && !config.limit) return data
|
|
169
198
|
|
|
170
199
|
let result = data
|
|
171
200
|
|
|
172
|
-
// Apply
|
|
201
|
+
// Apply where-object predicate first (new path)
|
|
202
|
+
if (config.where) {
|
|
203
|
+
result = applyWhere(result, config.where)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Apply legacy filter expression (deprecated)
|
|
173
207
|
if (config.filter) {
|
|
174
208
|
result = applyFilter(result, config.filter)
|
|
175
209
|
}
|
|
@@ -242,17 +276,20 @@ export function parseFetchConfig(fetch) {
|
|
|
242
276
|
'Accepted for one release; will be removed in the next minor.'
|
|
243
277
|
)
|
|
244
278
|
}
|
|
279
|
+
if (fetch.filter !== undefined) warnFilterDeprecated()
|
|
245
280
|
return {
|
|
246
281
|
refine: true,
|
|
247
282
|
...(fetch.detail !== undefined ? { detail: fetch.detail } : {}),
|
|
248
283
|
...(fetch.limit !== undefined ? { limit: fetch.limit } : {}),
|
|
249
284
|
...(fetch.sort !== undefined ? { sort: fetch.sort } : {}),
|
|
285
|
+
...(fetch.where !== undefined ? { where: fetch.where } : {}),
|
|
250
286
|
...(fetch.filter !== undefined ? { filter: fetch.filter } : {}),
|
|
251
287
|
}
|
|
252
288
|
}
|
|
253
289
|
|
|
254
290
|
// Collection reference: { collection: 'articles', limit: 3 }
|
|
255
291
|
if (fetch.collection) {
|
|
292
|
+
if (fetch.filter !== undefined) warnFilterDeprecated()
|
|
256
293
|
return {
|
|
257
294
|
path: `/data/${fetch.collection}.json`,
|
|
258
295
|
url: undefined,
|
|
@@ -260,9 +297,11 @@ export function parseFetchConfig(fetch) {
|
|
|
260
297
|
prerender: fetch.prerender ?? true,
|
|
261
298
|
merge: fetch.merge ?? false,
|
|
262
299
|
transform: fetch.transform,
|
|
263
|
-
//
|
|
300
|
+
// Query operators
|
|
301
|
+
where: fetch.where,
|
|
264
302
|
limit: fetch.limit,
|
|
265
303
|
sort: fetch.sort,
|
|
304
|
+
// Legacy post-processing (deprecated, see warning above)
|
|
266
305
|
filter: fetch.filter,
|
|
267
306
|
}
|
|
268
307
|
}
|
|
@@ -275,15 +314,19 @@ export function parseFetchConfig(fetch) {
|
|
|
275
314
|
merge = false,
|
|
276
315
|
transform,
|
|
277
316
|
detail,
|
|
278
|
-
//
|
|
317
|
+
// Query operators
|
|
318
|
+
where,
|
|
279
319
|
limit,
|
|
280
320
|
sort,
|
|
321
|
+
// Legacy post-processing (deprecated)
|
|
281
322
|
filter,
|
|
282
323
|
} = fetch
|
|
283
324
|
|
|
284
325
|
// Must have either path or url
|
|
285
326
|
if (!path && !url) return null
|
|
286
327
|
|
|
328
|
+
if (filter !== undefined) warnFilterDeprecated()
|
|
329
|
+
|
|
287
330
|
return {
|
|
288
331
|
path,
|
|
289
332
|
url,
|
|
@@ -292,13 +335,27 @@ export function parseFetchConfig(fetch) {
|
|
|
292
335
|
merge,
|
|
293
336
|
transform,
|
|
294
337
|
detail,
|
|
295
|
-
//
|
|
338
|
+
// Query operators
|
|
339
|
+
where,
|
|
296
340
|
limit,
|
|
297
341
|
sort,
|
|
342
|
+
// Legacy post-processing (deprecated)
|
|
298
343
|
filter,
|
|
299
344
|
}
|
|
300
345
|
}
|
|
301
346
|
|
|
347
|
+
let filterDeprecationWarned = false
|
|
348
|
+
function warnFilterDeprecated() {
|
|
349
|
+
if (filterDeprecationWarned) return
|
|
350
|
+
filterDeprecationWarned = true
|
|
351
|
+
console.warn(
|
|
352
|
+
"[uniweb] 'fetch: { filter: ... }' (DSL string) is deprecated; use 'where: { ... }' " +
|
|
353
|
+
'with a where-object. Example: where: { tags: \"featured\" } instead of ' +
|
|
354
|
+
"filter: 'tags contains featured'. " +
|
|
355
|
+
'Accepted for one release; will be removed in the next minor.'
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
302
359
|
/**
|
|
303
360
|
* Execute a fetch operation
|
|
304
361
|
*
|
package/src/site/plugin.js
CHANGED
|
@@ -556,7 +556,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
556
556
|
if (collectionsConfig) {
|
|
557
557
|
console.log('[site-content] Processing content collections...')
|
|
558
558
|
const collections = await processCollections(resolvedSitePath, collectionsConfig, resolvedCollectionsBase, basePath)
|
|
559
|
-
await writeCollectionFiles(resolvedSitePath, collections)
|
|
559
|
+
await writeCollectionFiles(resolvedSitePath, collections, collectionsConfig)
|
|
560
560
|
}
|
|
561
561
|
} catch (err) {
|
|
562
562
|
console.warn('[site-content] Early collection processing failed:', err.message)
|
|
@@ -593,7 +593,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
593
593
|
if (isProduction && siteContent.config?.collections) {
|
|
594
594
|
console.log('[site-content] Processing content collections...')
|
|
595
595
|
const collections = await processCollections(resolvedSitePath, siteContent.config.collections, resolvedCollectionsBase, basePath)
|
|
596
|
-
await writeCollectionFiles(resolvedSitePath, collections)
|
|
596
|
+
await writeCollectionFiles(resolvedSitePath, collections, siteContent.config.collections)
|
|
597
597
|
}
|
|
598
598
|
|
|
599
599
|
// Execute data fetches in dev mode
|
|
@@ -656,7 +656,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
656
656
|
const collections = collectionsConfig || siteContent?.config?.collections
|
|
657
657
|
if (collections) {
|
|
658
658
|
const processed = await processCollections(resolvedSitePath, collections, resolvedCollectionsBase, basePath)
|
|
659
|
-
await writeCollectionFiles(resolvedSitePath, processed)
|
|
659
|
+
await writeCollectionFiles(resolvedSitePath, processed, collections)
|
|
660
660
|
}
|
|
661
661
|
// Send full reload to client
|
|
662
662
|
server.ws.send({ type: 'full-reload' })
|