@uniweb/core 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
@@ -688,9 +688,9 @@ function bindWhere(where, variables) {
688
688
 
689
689
  /**
690
690
  * ⭐ `scope:` IS ITS OWN FIELD, on both lanes. The service takes it natively; on
691
- * the compiled file the evaluators apply it to each record's placement (`path`,
692
- * the folder `records/folder.yml` put it in) — `@uniweb/core`'s `applyScope`, the one the
693
- * build uses too. ⛔ Until 2026-09-11 it was folded here into
691
+ * the compiled file the evaluators apply it to the branch each record is held with
692
+ * (`$branch`, the folder `records/folder.yml` put it in) — `@uniweb/core`'s `applyScope`,
693
+ * the one the build uses too. ⛔ Until 2026-09-11 it was folded here into
694
694
  * `where: { path: { under } }`, an authored form that is now retired in its
695
695
  * favour [Diego]: a branch is a scope, and `where` stays the author's predicate.
696
696
  *
package/src/index.js CHANGED
@@ -55,9 +55,9 @@ export {
55
55
  isDataUrl
56
56
  } from './data-paths.js'
57
57
  export { evaluate as evaluateWhere, match as matchWhere, whereOutsideLanguage } from './where.js'
58
- // A query's `scope:` over records that carry their placement — read by the runtime's
59
- // default fetcher and by `@uniweb/build` when it compiles a query's file.
60
- export { applyScope, withinScope } from './scope.js'
58
+ // A query's `scope:` over compiled records, which hold their branch under `BRANCH_KEY` —
59
+ // read by the runtime's default fetcher and by `@uniweb/build`, which writes it.
60
+ export { applyScope, withinScope, withoutBranch, BRANCH_KEY } from './scope.js'
61
61
  // The one sort evaluator and the one href encoder — both read by `@uniweb/build`
62
62
  // (materialization, the `route:` bake) and by `@uniweb/runtime` (the fallback),
63
63
  // which is what keeps the static and live lanes answering a query identically.
@@ -24,7 +24,7 @@
24
24
  */
25
25
 
26
26
  import { match as matchWhere } from './where.js'
27
- import { applyScope } from './scope.js'
27
+ import { applyScope, withoutBranch } from './scope.js'
28
28
  import { sortRecords, parseSort } from './sort.js'
29
29
  import { recordKeyValues } from './route-match.js'
30
30
 
@@ -42,7 +42,10 @@ import { recordKeyValues } from './route-match.js'
42
42
  * @returns {Array<Object>|*} the records the config selects, in its order
43
43
  */
44
44
  export function evaluateQuery(records, config, { locale = null, sort = sortRecords } = {}) {
45
- if (!Array.isArray(records) || !config || typeof config !== 'object') return records
45
+ if (!Array.isArray(records)) return records
46
+ // ⛔ What this returns is an ANSWER, and no answer carries a record's branch: `scope` reads
47
+ // the one a compiled file holds, and it goes no further (`withoutBranch`, ruled 2026-09-27).
48
+ if (!config || typeof config !== 'object') return withoutBranch(records)
46
49
  const order = (items, expr) => sort(items, expr, { locale })
47
50
 
48
51
  let out = records
@@ -52,12 +55,12 @@ export function evaluateQuery(records, config, { locale = null, sort = sortRecor
52
55
  if (isLimit(config.limit)) out = out.slice(0, config.limit)
53
56
 
54
57
  const narrow = config.narrow
55
- if (!narrow || typeof narrow !== 'object') return out
58
+ if (!narrow || typeof narrow !== 'object') return withoutBranch(out)
56
59
  if (narrow.where) out = matchWhere(narrow.where, out)
57
60
  if (narrow.match && typeof narrow.match === 'object') out = out.filter(matching(narrow.match))
58
61
  if (narrow.sort) out = order(out, narrow.sort)
59
62
  if (isLimit(narrow.limit)) out = out.slice(0, narrow.limit)
60
- return out
63
+ return withoutBranch(out)
61
64
  }
62
65
 
63
66
  /**
@@ -228,8 +228,10 @@ export function splitPathCapture(capture) {
228
228
  }
229
229
 
230
230
  /**
231
- * The inverse of `splitPathCapture` — a record's own URL path under a
232
- * `[...path]` template, from its placement and its handle. `dir` may be empty.
231
+ * The inverse of `splitPathCapture` — a capture from its directory and its last
232
+ * segment. `dir` may be empty. ⛔ Not a record's URL: a record carries no branch, so its
233
+ * own URL under `[...path]` is its handle alone (`fillRoutePattern`). Until 2026-09-27 this
234
+ * composed a record's placement and handle into one.
233
235
  *
234
236
  * @param {{ dir?: string|null, slug?: string|null }} parts
235
237
  * @returns {string|null} null when there is no slug to name the record by
@@ -587,18 +589,17 @@ export function fillRoutePattern(pattern, values, { field = null } = {}) {
587
589
  let tailHref = ''
588
590
  const tail = pattern.match(CATCH_ALL)
589
591
  if (tail) {
590
- // A catch-all is filled from the record's placement and handle — the split
591
- // rule in reverse (`joinPathCapture`) — with each SEGMENT encoded and the
592
- // slashes between them kept as structure. `dir` is the placement; a record
593
- // carries it as `path` (the folder `records/folder.yml` put it in), which is why
594
- // `path` here is read as the DIRECTORY and never as a composed capture.
592
+ // ⭐ A catch-all is filled with the record's handle ALONE — one segment, whatever it
593
+ // contains (a `/` inside it is a value). A record carries no branch [Diego,
594
+ // 2026-09-27], so its own URL under `[...path]` is the one it has under `[slug]`:
595
+ // `/logbook/<name>`. A URL that names a directory still reaches the page, and
596
+ // `scope: :dir` is what makes the directory count. ⛔ Until 2026-09-27 this prefixed the
597
+ // record's `path`, so one record was linked at `/logbook/field/river-survey` on a static
598
+ // site and at `/logbook/river-survey` where a records service, which answers no branch,
599
+ // served it.
595
600
  const [handle] = routeParamValues(values, 'slug', field)
596
- if (joinPathCapture({ dir: values.dir ?? values.path, slug: handle }) === null) return null
597
- const dir = String(values.dir ?? values.path ?? '')
598
- const segments = dir.split('/').filter(Boolean).map((seg) => encodeURIComponent(seg))
599
- // The handle is ONE segment whatever it contains: a `/` inside it is a value.
600
- segments.push(encodeURIComponent(String(handle)))
601
- tailHref = '/' + segments.join('/')
601
+ if (handle === undefined) return null
602
+ tailHref = '/' + encodeURIComponent(String(handle))
602
603
  head = pattern.slice(0, tail.index)
603
604
  }
604
605
  const href = head.replace(new RegExp(`:(${PARAM_NAME})`, 'g'), (_, name) => {
package/src/scope.js CHANGED
@@ -1,11 +1,19 @@
1
1
  /**
2
- * A query's `scope:` — the folder branch it reads — evaluated over records that
3
- * carry their placement.
2
+ * A query's `scope:` — the folder branch it reads — evaluated over compiled records,
3
+ * which hold their branch.
4
4
  *
5
- * A record's `path` is the folder `records/folder.yml` placed it in (`build/src/site/
6
- * query-processor.js`). A scope contains that folder and every folder below it, at
7
- * SEGMENT boundaries: `field` holds `field` and `field/2025`, never `fieldwork`.
8
- * The root (`''`) holds everything.
5
+ * A compiled record holds the folder `records/folder.yml` placed it in under
6
+ * `$branch` (`BRANCH_KEY`, written by `build/src/site/query-processor.js`). A scope
7
+ * contains that folder and every folder below it, at SEGMENT boundaries: `field`
8
+ * holds `field` and `field/2025`, never `fieldwork`. The root (`''`) holds everything.
9
+ *
10
+ * ⛔ A RECORD DOES NOT CARRY ITS BRANCH (ruled 2026-09-27 [Diego]: *"branches are to
11
+ * organize the content pool … queries can filter by branch using `scope`"*). `$branch`
12
+ * is the compiled file's own key, there because the browser evaluates `scope` over that
13
+ * file. `evaluateQuery` reads it and answers no record with it (`withoutBranch`), just as
14
+ * a records service answers none. ⛔ Until then every compiled record carried `path`,
15
+ * which reached components, overwrote an authored field named `path`, and made a
16
+ * `[...path]` record's link `<branch>/<name>`.
9
17
  *
10
18
  * ⭐ Why this is its own field and not a `where` operator: a branch is a scope,
11
19
  * and `where` stays the author's predicate over the records' own fields. Ruled
@@ -18,6 +26,31 @@
18
26
  * Zero-dependency leaf.
19
27
  */
20
28
 
29
+ /**
30
+ * The key a compiled record holds its branch under. The static lane's store only; no
31
+ * answer carries it.
32
+ */
33
+ export const BRANCH_KEY = '$branch'
34
+
35
+ /**
36
+ * Records as a query answers them: without the branch their compiled file holds. A list
37
+ * in which no record holds one is returned as it is.
38
+ *
39
+ * @param {Array<Object>} records
40
+ * @returns {Array<Object>}
41
+ */
42
+ export function withoutBranch(records) {
43
+ if (!Array.isArray(records)) return records
44
+ if (!records.some((r) => r && typeof r === 'object' && Object.prototype.hasOwnProperty.call(r, BRANCH_KEY))) {
45
+ return records
46
+ }
47
+ return records.map((r) => {
48
+ if (!r || typeof r !== 'object' || !Object.prototype.hasOwnProperty.call(r, BRANCH_KEY)) return r
49
+ const { [BRANCH_KEY]: _branch, ...rest } = r
50
+ return rest
51
+ })
52
+ }
53
+
21
54
  function trimSlashes(s) {
22
55
  return s.replace(/^\/+/, '').replace(/\/+$/, '')
23
56
  }
@@ -29,7 +62,7 @@ function trimSlashes(s) {
29
62
  * `2024`, the classic prefix bug, which silently returns records from a sibling
30
63
  * folder the author never named.
31
64
  *
32
- * @param {*} placement - a record's `path`
65
+ * @param {*} placement - a compiled record's branch (`BRANCH_KEY`)
33
66
  * @param {string} scope
34
67
  * @returns {boolean}
35
68
  */
@@ -52,5 +85,5 @@ export function withinScope(placement, scope) {
52
85
  export function applyScope(records, scope) {
53
86
  if (!Array.isArray(records)) return records
54
87
  if (typeof scope !== 'string' || trimSlashes(scope) === '') return records
55
- return records.filter((r) => r && typeof r === 'object' && withinScope(r.path, scope))
88
+ return records.filter((r) => r && typeof r === 'object' && withinScope(r[BRANCH_KEY], scope))
56
89
  }