@uniweb/core 0.28.0 → 0.29.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 +4 -3
- package/src/block.js +7 -0
- package/src/data-keys.js +155 -0
- package/src/data-paths.js +7 -7
- package/src/datastore.js +28 -13
- package/src/detail-url.js +23 -15
- package/src/entity-store.js +215 -136
- package/src/fetch-config.js +255 -81
- package/src/fetcher-dispatcher.js +9 -1
- package/src/index.js +5 -1
- package/src/query-evaluation.js +87 -0
- package/src/route-match.js +3 -2
- package/src/sort.js +2 -3
- package/src/website.js +70 -41
package/src/website.js
CHANGED
|
@@ -12,7 +12,8 @@ import ObservableState from './observable-state.js'
|
|
|
12
12
|
import { normalizeSeo } from './seo.js'
|
|
13
13
|
import { resolveDefaultLocale, localeLabel } from './locale-config.js'
|
|
14
14
|
import { matchDynamicRoute, decodeRouteValue, matchesRouteParam, routeBinding, routeParamName, parentRouteOf } from './route-match.js'
|
|
15
|
-
import { resolveFetchConfigs,
|
|
15
|
+
import { resolveFetchConfigs, pageRouteQuery, recordPages, routeSelection, siteReaches } from './fetch-config.js'
|
|
16
|
+
import { declaredKeys } from './data-keys.js'
|
|
16
17
|
import { buildDetailConfig } from './detail-url.js'
|
|
17
18
|
import { findLayoutEntry } from './layout-name.js'
|
|
18
19
|
import { resolveService } from './services.js'
|
|
@@ -87,6 +88,8 @@ export default class Website {
|
|
|
87
88
|
this.notFoundPage = null
|
|
88
89
|
this._dynamicPageData = new Map()
|
|
89
90
|
this._dynamicPageCache = new Map()
|
|
91
|
+
// query name → the page its records link to (`recordPageFor`), worked out on first ask
|
|
92
|
+
this._recordPages = null
|
|
90
93
|
this.pages = []
|
|
91
94
|
this.activePage = null
|
|
92
95
|
this.pageRoutes = []
|
|
@@ -161,6 +164,7 @@ export default class Website {
|
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
this._dynamicPageCache = new Map()
|
|
167
|
+
this._recordPages = null
|
|
164
168
|
|
|
165
169
|
this.pages = regularPages.map((page, index) => new Page(page, index, this))
|
|
166
170
|
this.buildPageHierarchy()
|
|
@@ -573,7 +577,7 @@ export default class Website {
|
|
|
573
577
|
const { paramName, paramValue, variables } = routeBinding(templatePage.route, params, originalData.paramName)
|
|
574
578
|
|
|
575
579
|
// Store dynamic context for components to access. ⛔ No `schema`: the key the
|
|
576
|
-
// URL narrows is worked out where it is read (`EntityStore.
|
|
580
|
+
// URL narrows is worked out where it is read (`EntityStore._route`), from
|
|
577
581
|
// the page, never stored beside it (deleted 2026-09-11).
|
|
578
582
|
pageData.dynamicContext = {
|
|
579
583
|
templateRoute: templatePage.route,
|
|
@@ -622,6 +626,10 @@ export default class Website {
|
|
|
622
626
|
// visitor's page — the "write key ≠ read key" failure.
|
|
623
627
|
let items = []
|
|
624
628
|
let currentItem = null
|
|
629
|
+
// ⭐ On the records service the record's own question checks the route query's
|
|
630
|
+
// set (`buildDetailConfig`), so once it has answered, its answer is definitive —
|
|
631
|
+
// the record, or `[]` for not found — with no list to consult.
|
|
632
|
+
let recordAnswered = false
|
|
625
633
|
|
|
626
634
|
if (this.fetcher) {
|
|
627
635
|
// The same sources the store walks for a section that declares nothing,
|
|
@@ -654,10 +662,13 @@ export default class Website {
|
|
|
654
662
|
const raw = detailCached?.data
|
|
655
663
|
const record = Array.isArray(raw) ? raw[0] : raw
|
|
656
664
|
if (record && typeof record === 'object') currentItem = record
|
|
665
|
+
if (fetchConfig.ask && detailCached && Array.isArray(raw)) recordAnswered = true
|
|
657
666
|
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
667
|
+
// ⭐ The route query's set (`routeSelection`) — the query as saved, its `limit`
|
|
668
|
+
// included, never a list a fetch narrowed: a record past a fetch's `limit` is a
|
|
669
|
+
// record, and one past the query's is not (ruled 2026-09-14 [Diego]). ⛔ Until
|
|
670
|
+
// 2026-09-13 this read the cut list and declared a record past a list's
|
|
671
|
+
// `limit` "Not found".
|
|
661
672
|
const cached = this.fetcher.peek(routeSelection(fetchConfig), ctx)
|
|
662
673
|
items = Array.isArray(cached?.data) ? cached.data : []
|
|
663
674
|
}
|
|
@@ -672,8 +683,9 @@ export default class Website {
|
|
|
672
683
|
if (currentItem.description || currentItem.excerpt) {
|
|
673
684
|
pageData.description = currentItem.description || currentItem.excerpt
|
|
674
685
|
}
|
|
675
|
-
} else if (items.length > 0) {
|
|
676
|
-
// The
|
|
686
|
+
} else if (items.length > 0 || recordAnswered) {
|
|
687
|
+
// The set is loaded and this ID isn't in it, or the record question answered
|
|
688
|
+
// `[]` — definitive not found
|
|
677
689
|
pageData.title = 'Not found'
|
|
678
690
|
pageData.notFound = true
|
|
679
691
|
}
|
|
@@ -685,7 +697,7 @@ export default class Website {
|
|
|
685
697
|
// components via content.data, the others via
|
|
686
698
|
// `fetch: { query, current: exclude }`). The local
|
|
687
699
|
// `currentItem`/`items` above drive title/description/notFound.
|
|
688
|
-
pageData._recordsLoaded = items.length > 0 || currentItem !== null
|
|
700
|
+
pageData._recordsLoaded = items.length > 0 || currentItem !== null || recordAnswered
|
|
689
701
|
}
|
|
690
702
|
|
|
691
703
|
// Create the page instance
|
|
@@ -875,43 +887,60 @@ export default class Website {
|
|
|
875
887
|
}
|
|
876
888
|
|
|
877
889
|
/**
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
*
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
890
|
+
* ⭐ THE `content.data` KEYS A COMPONENT RECEIVES — its `data:` (`meta.data`, as the build
|
|
891
|
+
* leans it), then its foundation's `main.js` `data:`, each with its schema ref
|
|
892
|
+
* (`declaredKeys`, `./data-keys.js`; ruled 2026-09-14 [Diego]). A component with no
|
|
893
|
+
* `data:` receives only its foundation's keys, which reach every section because its
|
|
894
|
+
* handlers run for every section.
|
|
895
|
+
*
|
|
896
|
+
* @param {Object|null} meta - the component's runtime meta (`uniweb.getComponentMeta`)
|
|
897
|
+
* @returns {Array<[string, string|null]>}
|
|
898
|
+
*/
|
|
899
|
+
declaredKeys(meta) {
|
|
900
|
+
return declaredKeys(meta?.data, this._foundation?.default?.capabilities?.data)
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* ⭐ THE PAGE A QUERY'S RECORDS LINK TO — `{ route, paramName }`: the parametric page
|
|
905
|
+
* whose route query IS the query, by name, the first in page order (`recordPages`,
|
|
906
|
+
* `./fetch-config.js`), with its route translated for the active locale. Null when the
|
|
907
|
+
* site has no such page, and then a record of the query has no link.
|
|
908
|
+
*
|
|
909
|
+
* This is what fills a delivered record's `$route` (`EntityStore`) when its fetch picks
|
|
910
|
+
* no `detailPage`, and how a caller outside a parametric page learns which record field
|
|
911
|
+
* the page's URL is built on: `kit`'s `useWholeRecord` asks it so a hover card and the
|
|
912
|
+
* page it links to address the record by the SAME field.
|
|
913
|
+
*
|
|
914
|
+
* ⛔ Replaces `detailTemplateFor(name)` (2026-09-14), which matched a binding key OR a
|
|
915
|
+
* query name — so a page whose route query merely landed under the same key answered
|
|
916
|
+
* for another query's records.
|
|
917
|
+
*
|
|
918
|
+
* @param {string} query - a query's name
|
|
895
919
|
* @returns {{ route: string, paramName: string } | null}
|
|
896
920
|
*/
|
|
897
|
-
|
|
898
|
-
if (!
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
921
|
+
recordPageFor(query) {
|
|
922
|
+
if (typeof query !== 'string' || !query) return null
|
|
923
|
+
if (!this._recordPages) {
|
|
924
|
+
// ⭐ Every page, by the route PATTERN it answers to — the entity store's reading
|
|
925
|
+
// (`_route`). A static build replaces a parametric page with one concrete page per
|
|
926
|
+
// record (`expandDynamicPages`), each carrying its template's route as
|
|
927
|
+
// `dynamicContext.templateRoute`; the payload it prerenders and hydrates holds those,
|
|
928
|
+
// and no template. ⛔ Read from the templates alone, this found no page there, and
|
|
929
|
+
// no record of a prerendered site had a link (measured on the `dynamic` template).
|
|
930
|
+
this._recordPages = recordPages(this.pages, {
|
|
931
|
+
routeOf: (page) => page?.dynamicContext?.templateRoute ?? page?.route,
|
|
932
|
+
parentOf: (page) => page?.parent ?? null,
|
|
933
|
+
fetchOf: (page) => page?.fetch,
|
|
934
|
+
sectionsOf: (page) => page?._bodySections,
|
|
935
|
+
site: this.config?.fetch,
|
|
908
936
|
})
|
|
909
|
-
if (!route) continue
|
|
910
|
-
if (route.key === name || route.config?.query === name) {
|
|
911
|
-
return { route: data.route, paramName: routeParamName(data.route, data.paramName) }
|
|
912
|
-
}
|
|
913
937
|
}
|
|
914
|
-
|
|
938
|
+
const found = this._recordPages.get(query)
|
|
939
|
+
if (!found) return null
|
|
940
|
+
return {
|
|
941
|
+
route: this.translateRoute(found.route),
|
|
942
|
+
paramName: routeParamName(found.route, found.page?.dynamicContext?.paramName ?? found.page?.paramName),
|
|
943
|
+
}
|
|
915
944
|
}
|
|
916
945
|
|
|
917
946
|
/**
|