@uniweb/runtime 0.13.7 → 0.14.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/dist/ssr.js +500 -4
- package/dist/ssr.js.map +1 -1
- package/package.json +3 -3
- package/src/default-fetcher.js +47 -23
- package/src/prefetch.js +146 -0
- package/src/setup.js +4 -1
- package/src/ssr.js +10 -0
- package/src/wire-foundation.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Minimal runtime for loading Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"node": ">=20.19"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@uniweb/core": "^0.
|
|
39
|
+
"@uniweb/core": "^0.19.0",
|
|
40
40
|
"@uniweb/theming": "^0.1.15"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0",
|
|
45
45
|
"vite": "^7.3.1",
|
|
46
46
|
"vitest": "^4.1.7",
|
|
47
|
-
"@uniweb/build": "0.
|
|
47
|
+
"@uniweb/build": "0.36.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": "^19.0.0",
|
package/src/default-fetcher.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* X-Tenant: acme
|
|
17
17
|
* Accept: application/vnd.example+json
|
|
18
18
|
* envelope:
|
|
19
|
-
*
|
|
19
|
+
* list: data.items
|
|
20
20
|
* item: data.article
|
|
21
21
|
* error: errors.0.message
|
|
22
22
|
*
|
|
@@ -64,10 +64,11 @@ import {
|
|
|
64
64
|
resolveServiceUrl,
|
|
65
65
|
} from '@uniweb/core'
|
|
66
66
|
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
67
|
+
// The request style is the wire dialect operators are encoded in. One
|
|
68
|
+
// ships — json-body, the framework's own — and `resolveRequestStyle` is
|
|
69
|
+
// loud on any other name: it throws in dev and logs once in production.
|
|
70
|
+
// Another dialect is a named transport, from the foundation or from an
|
|
71
|
+
// extension the site selects; it is never a second built-in style.
|
|
71
72
|
|
|
72
73
|
// Operators the default fetcher knows how to handle. When listed in
|
|
73
74
|
// `config.supports`, they're shipped to the source as part of the
|
|
@@ -86,11 +87,17 @@ const KNOWN_OPERATORS = new Set(['where', 'limit', 'sort'])
|
|
|
86
87
|
* `request.rename`. Unknown keys are ignored (foundations may use the
|
|
87
88
|
* same block for their own keys). Default behavior (empty config)
|
|
88
89
|
* matches today's plain GET + JSON.
|
|
89
|
-
* @param {boolean} [options.dev=false] - Enable dev-mode
|
|
90
|
-
* style
|
|
90
|
+
* @param {boolean} [options.dev=false] - Enable dev-mode diagnostics: an
|
|
91
|
+
* unknown request style throws; a rename entry for an operator the wire
|
|
92
|
+
* does not carry warns.
|
|
91
93
|
* @returns {{ resolve: (req: Object, ctx: Object) => Promise<{ data, error? }> }}
|
|
92
94
|
*/
|
|
93
|
-
export function createDefaultFetcher({ basePath = '', config = {}, dev = false } = {}) {
|
|
95
|
+
export function createDefaultFetcher({ basePath = '', config = {}, dev = false, records = null, fetch: fetchImpl = null } = {}) {
|
|
96
|
+
// The transport is injectable: a host executing fetches outside a browser (an SSR isolate)
|
|
97
|
+
// decides how a site-relative address such as `/_records/members` is dispatched — through its
|
|
98
|
+
// own origin or a service binding — and hands that in. Defaults to the global `fetch`, resolved
|
|
99
|
+
// at call time so a test stub installed later is honoured.
|
|
100
|
+
const doFetch = (input, init) => (fetchImpl || globalThis.fetch)(input, init)
|
|
94
101
|
const pathPrefix = basePath && basePath !== '/' ? basePath.replace(/\/$/, '') : ''
|
|
95
102
|
|
|
96
103
|
const baseUrl = typeof config?.baseUrl === 'string'
|
|
@@ -108,9 +115,10 @@ export function createDefaultFetcher({ basePath = '', config = {}, dev = false }
|
|
|
108
115
|
// default fetcher serving static files supports nothing natively.
|
|
109
116
|
const supports = normalizeSupports(config?.supports)
|
|
110
117
|
|
|
111
|
-
// Request style —
|
|
112
|
-
//
|
|
113
|
-
// to the
|
|
118
|
+
// Request style — the wire dialect operators are encoded in. Read from
|
|
119
|
+
// `site.yml fetcher.request.style`; `null`/absent and `json-body` both
|
|
120
|
+
// resolve to the one shipped style, and any other name is loud (see
|
|
121
|
+
// `resolveRequestStyle`).
|
|
114
122
|
const requestConfig = (config?.request && typeof config.request === 'object') ? config.request : {}
|
|
115
123
|
const styleName = typeof requestConfig.style === 'string' ? requestConfig.style : null
|
|
116
124
|
const style = resolveRequestStyle(styleName, { dev })
|
|
@@ -129,14 +137,30 @@ export function createDefaultFetcher({ basePath = '', config = {}, dev = false }
|
|
|
129
137
|
// - envelope.error — extract error text from non-2xx response body.
|
|
130
138
|
//
|
|
131
139
|
// Priority (highest wins): per-fetch request.envelope > site-level
|
|
132
|
-
// config.envelope > style.defaultEnvelope.
|
|
133
|
-
// envelope
|
|
134
|
-
//
|
|
140
|
+
// config.envelope > style.defaultEnvelope. json-body declares no
|
|
141
|
+
// envelope; the slot is the encoder's to fill, and a site-level value
|
|
142
|
+
// always wins over it.
|
|
135
143
|
const siteEnvelope = (config?.envelope && typeof config.envelope === 'object')
|
|
136
144
|
? config.envelope
|
|
137
145
|
: null
|
|
138
146
|
const envelope = { ...(style.defaultEnvelope || {}), ...(siteEnvelope || {}) }
|
|
139
147
|
|
|
148
|
+
// ⭐ The LIVE LANE's envelope is the backend's. `config.records` is stamped by the backend
|
|
149
|
+
// that answers a records request, so where the array sits in ITS response is its to
|
|
150
|
+
// declare: `records.envelope.records` — the KEY says what it holds, the VALUE is the JSON
|
|
151
|
+
// key the array sits under (`{ records: "entries" }` ⇒ body.entries). That spelling is the
|
|
152
|
+
// agreed one (2026-08-30: `collection` retired; ⛔ not `list`, which is a URL pattern on the
|
|
153
|
+
// same stamp). It applies only to a request that resolved to that lane (`endpoint` set)
|
|
154
|
+
// and wins over the site's own `fetcher.envelope`, which describes the author's backend.
|
|
155
|
+
// Ruled 2026-09-03 [Diego]: the backend sets `config.records`; the fetch comes from the
|
|
156
|
+
// runtime. Until this line the runtime resolved `list`/`record` off the stamp and ignored
|
|
157
|
+
// its envelope.
|
|
158
|
+
const stampedArrayKey = (records?.envelope && typeof records.envelope === 'object'
|
|
159
|
+
&& typeof records.envelope.records === 'string' && records.envelope.records.length)
|
|
160
|
+
? records.envelope.records
|
|
161
|
+
: null
|
|
162
|
+
const laneEnvelope = stampedArrayKey ? { list: stampedArrayKey } : null
|
|
163
|
+
|
|
140
164
|
return {
|
|
141
165
|
/**
|
|
142
166
|
* Cache-key function. The default-fetcher's cache key includes only
|
|
@@ -155,9 +179,8 @@ export function createDefaultFetcher({ basePath = '', config = {}, dev = false }
|
|
|
155
179
|
// style will actually push for this request. deriveCacheKey already
|
|
156
180
|
// covers the always-keyed fields.
|
|
157
181
|
//
|
|
158
|
-
// The key also
|
|
159
|
-
//
|
|
160
|
-
// their responses must not alias.
|
|
182
|
+
// The key also carries the style name. With one shipped style it is
|
|
183
|
+
// a constant segment, kept so that key shapes do not move.
|
|
161
184
|
const base = deriveCacheKey(request)
|
|
162
185
|
const projected = {}
|
|
163
186
|
for (const op of supports) {
|
|
@@ -230,10 +253,10 @@ export function createDefaultFetcher({ basePath = '', config = {}, dev = false }
|
|
|
230
253
|
// style didn't push get applied as a JS fallback after the response
|
|
231
254
|
// (see the post-fetch block below).
|
|
232
255
|
//
|
|
233
|
-
// The style owns the wire format
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
256
|
+
// The style owns the wire format: json-body encodes GET pushdown as
|
|
257
|
+
// `?_where=<JSON>&_limit=&_sort=` and POST pushdown as top-level keys
|
|
258
|
+
// merged into an object body. It is the only shipped wire — another
|
|
259
|
+
// dialect is a named transport.
|
|
237
260
|
const pushCandidates = new Set()
|
|
238
261
|
if (isRemote) {
|
|
239
262
|
for (const op of KNOWN_OPERATORS) {
|
|
@@ -285,7 +308,7 @@ export function createDefaultFetcher({ basePath = '', config = {}, dev = false }
|
|
|
285
308
|
if (Object.keys(headers).length) init.headers = headers
|
|
286
309
|
|
|
287
310
|
try {
|
|
288
|
-
const response = await
|
|
311
|
+
const response = await doFetch(target, init)
|
|
289
312
|
|
|
290
313
|
// Per-request envelope (set by object-form `detail:`) wins over
|
|
291
314
|
// site-level envelope. This lets a detail query declare its own
|
|
@@ -293,7 +316,8 @@ export function createDefaultFetcher({ basePath = '', config = {}, dev = false }
|
|
|
293
316
|
const requestEnvelope = (request.envelope && typeof request.envelope === 'object')
|
|
294
317
|
? request.envelope
|
|
295
318
|
: null
|
|
296
|
-
const effectiveEnvelope = requestEnvelope
|
|
319
|
+
const effectiveEnvelope = requestEnvelope
|
|
320
|
+
?? (endpoint && laneEnvelope ? { ...envelope, ...laneEnvelope } : envelope)
|
|
297
321
|
|
|
298
322
|
if (!response.ok) {
|
|
299
323
|
// If `envelope.error` is configured, try to extract a human message
|
package/src/prefetch.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side data prefetch — the runtime executing a page's fetches for a host.
|
|
3
|
+
*
|
|
4
|
+
* L2 (graph state, no React): reads a payload, resolves the fetch configs the way the
|
|
5
|
+
* entity store does at render time, executes them through the runtime's own default
|
|
6
|
+
* fetcher, and returns the `[{ config, data }]` list `hydrateDataStore` expects.
|
|
7
|
+
*
|
|
8
|
+
* ⭐ Why this exists — one implementation of the fetch, in the runtime. A host that renders
|
|
9
|
+
* pages in an isolate hands the isolate `fetchedData`. Until this module the host had to
|
|
10
|
+
* compute that itself: resolve the configs, issue the requests, unwrap the responses in the
|
|
11
|
+
* shape the datastore expects — a copy of the runtime's logic, in another repo, drifting
|
|
12
|
+
* (the records envelope went silently unread that way on 2026-09-02). [Diego, 2026-09-03]:
|
|
13
|
+
* *the backend sets `config.records`; the fetch comes from the runtime.* The host now calls
|
|
14
|
+
* this and carries no copy. Hosting agreed to exactly that shape the same day.
|
|
15
|
+
*
|
|
16
|
+
* ⛔ Contract with the host, deliberately small:
|
|
17
|
+
* - `content` the render payload (`site-content.json` / `__DATA__`), config included —
|
|
18
|
+
* `config.records`, `config.fetcher`, `config.base` are read from it.
|
|
19
|
+
* - `route` the page to prefetch for; a `[slug]` template resolves through the same
|
|
20
|
+
* matcher the SPA uses, so `/blog/post-1` finds `/blog/:slug`.
|
|
21
|
+
* - `fetch` how to dispatch a request. The runtime composes the address; the host
|
|
22
|
+
* decides how a site-relative one is reached (its origin, a binding).
|
|
23
|
+
* - `prerender` whether a fetch is tried — `'always'` (default) tries every config; `'author'`
|
|
24
|
+
* honours the author's `prerender: false`. ⛔ The default is `'always'` because this
|
|
25
|
+
* entry has exactly one kind of caller: an isolate rendering per request, where the
|
|
26
|
+
* flag means nothing and always prerendering is the product ([Diego, 2026-07-28 and
|
|
27
|
+
* 2026-09-03]: "`prerender: false` is not for the isolate"). The build lane, which
|
|
28
|
+
* bakes static artifacts and does honour the flag, uses its own executor
|
|
29
|
+
* (`build/src/prerender.js`) and never calls this. `'author'` is the explicit opt-in
|
|
30
|
+
* for a caller that bakes; omitting the option must not silently reproduce the
|
|
31
|
+
* 2026-07-28 outcome — prefetch a no-op on a live-data template, page still 200.
|
|
32
|
+
* - returns one entry per DECLARED config, `{ config, outcome, data, error? }`, keyed
|
|
33
|
+
* downstream by `deriveCacheKey(config)`. `outcome` is `fetched`, `failed`
|
|
34
|
+
* (transport or HTTP error, `error` says which) or `skipped` (the author
|
|
35
|
+
* deferred it to the browser with `prerender: false`). `hydrateDataStore`
|
|
36
|
+
* takes the list as-is and hydrates only `fetched` entries — a host reads the
|
|
37
|
+
* outcomes to tell "nothing was tried" from "everything tried failed", which
|
|
38
|
+
* is a different cache decision (hosting, 2026-09-03).
|
|
39
|
+
*
|
|
40
|
+
* It resolves nothing the host owns and models no host route layout: every address is
|
|
41
|
+
* `{base}/…` from the payload, or an endpoint the host itself published in `config.records`.
|
|
42
|
+
*/
|
|
43
|
+
import { resolveFetchConfigs } from '@uniweb/core/fetch-config'
|
|
44
|
+
import { deriveCacheKey } from '@uniweb/core/datastore'
|
|
45
|
+
import { routePatternToRegex } from '@uniweb/core/route-match'
|
|
46
|
+
import { resolveDefaultLocale } from '@uniweb/core/locale-config'
|
|
47
|
+
import { createDefaultFetcher } from './default-fetcher.js'
|
|
48
|
+
|
|
49
|
+
const isRefinement = (f) => f && typeof f === 'object' && f.refine === true
|
|
50
|
+
|
|
51
|
+
/** The page a route names — exact first, then the `[slug]` templates, like the SPA. */
|
|
52
|
+
export function findPageForRoute(content, route) {
|
|
53
|
+
const pages = content?.pages || []
|
|
54
|
+
const exact = pages.find((p) => p.route === route)
|
|
55
|
+
if (exact) return { page: exact, params: {} }
|
|
56
|
+
for (const page of pages) {
|
|
57
|
+
if (!page.isDynamic || !page.route) continue
|
|
58
|
+
const compiled = routePatternToRegex(page.route)
|
|
59
|
+
const m = compiled?.regex ? compiled.regex.exec(route) : null
|
|
60
|
+
if (m) return { page, params: Object.fromEntries((compiled.paramNames || []).map((n, i) => [n, m[i + 1]])) }
|
|
61
|
+
}
|
|
62
|
+
return { page: null, params: {} }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Every fetch config a page will need at render time, resolved once and de-duplicated by
|
|
67
|
+
* cache key: the site-level fetch, the page's, its parent's, and each section's own
|
|
68
|
+
* (including nested sections), each through `resolveFetchConfigs` — the same resolver the
|
|
69
|
+
* entity store uses, so a host prefetches exactly what the render will ask for.
|
|
70
|
+
*
|
|
71
|
+
* @returns {Object[]} resolved fetch configs
|
|
72
|
+
*/
|
|
73
|
+
export function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
|
|
74
|
+
const { page } = findPageForRoute(content, route)
|
|
75
|
+
if (!page) return []
|
|
76
|
+
const pages = content?.pages || []
|
|
77
|
+
const parent = page.parent ? pages.find((p) => p.route === page.parent) : null
|
|
78
|
+
const options = {
|
|
79
|
+
locale,
|
|
80
|
+
defaultLocale: resolveDefaultLocale(content?.config) ?? null,
|
|
81
|
+
queries: content?.config?.queries ?? null,
|
|
82
|
+
records: content?.config?.records ?? null,
|
|
83
|
+
}
|
|
84
|
+
const out = new Map()
|
|
85
|
+
const add = (sources) => {
|
|
86
|
+
for (const cfg of resolveFetchConfigs(sources, options).values()) {
|
|
87
|
+
const key = deriveCacheKey(cfg)
|
|
88
|
+
if (!out.has(key)) out.set(key, cfg)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// The cascade a block sees: its own fetch (unless a refinement), page, parent, site.
|
|
92
|
+
add([page.fetch ?? null, parent?.fetch ?? null, content?.config?.fetch ?? null])
|
|
93
|
+
const walk = (sections) => {
|
|
94
|
+
for (const s of sections || []) {
|
|
95
|
+
if (s?.fetch && !isRefinement(s.fetch)) add([s.fetch, page.fetch ?? null, parent?.fetch ?? null, content?.config?.fetch ?? null])
|
|
96
|
+
if (s?.subsections) walk(s.subsections)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
walk(page.sections)
|
|
100
|
+
return [...out.values()]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Execute resolved fetch configs through the runtime's default fetcher.
|
|
105
|
+
*
|
|
106
|
+
* @param {Object[]} configs resolved configs (from `resolvePageFetchConfigs` or the host's own
|
|
107
|
+
* call to `resolveFetchConfigs`)
|
|
108
|
+
* @param {Object} opts
|
|
109
|
+
* @param {Object} opts.content the payload — `config.base`, `config.fetcher`, `config.records`
|
|
110
|
+
* @param {Function} [opts.fetch] the transport; defaults to the global `fetch`
|
|
111
|
+
* @param {boolean} [opts.dev]
|
|
112
|
+
* @returns {Promise<Array<{ config: Object, outcome: 'fetched'|'failed'|'skipped', data: any, error?: string }>>}
|
|
113
|
+
*/
|
|
114
|
+
export async function executeFetchConfigs(configs, { content, fetch = null, dev = false, prerender = 'always' } = {}) {
|
|
115
|
+
if (prerender !== 'author' && prerender !== 'always') {
|
|
116
|
+
throw new Error(`executeFetchConfigs: prerender must be 'author' or 'always', got ${JSON.stringify(prerender)}`)
|
|
117
|
+
}
|
|
118
|
+
const fetcher = createDefaultFetcher({
|
|
119
|
+
basePath: content?.config?.base || '',
|
|
120
|
+
config: content?.config?.fetcher ?? {},
|
|
121
|
+
records: content?.config?.records ?? null,
|
|
122
|
+
dev,
|
|
123
|
+
fetch,
|
|
124
|
+
})
|
|
125
|
+
const ctx = { website: null }
|
|
126
|
+
const out = []
|
|
127
|
+
for (const config of configs || []) {
|
|
128
|
+
if (!config) continue
|
|
129
|
+
if (prerender === 'author' && config.prerender === false) {
|
|
130
|
+
// The author deferred this one to the browser and the caller honours that. Present, so a
|
|
131
|
+
// host can count what was declared against what was tried; not hydrated.
|
|
132
|
+
out.push({ config, outcome: 'skipped', data: null })
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
const result = await fetcher.resolve(config, ctx)
|
|
136
|
+
if (result?.error) out.push({ config, outcome: 'failed', data: null, error: result.error })
|
|
137
|
+
else out.push({ config, outcome: 'fetched', data: result?.data ?? null })
|
|
138
|
+
}
|
|
139
|
+
return out
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Resolve and execute in one call: what a host passes the isolate as `fetchedData`. */
|
|
143
|
+
export async function prefetchPageData({ content, route, locale = null, fetch = null, dev = false, prerender = 'always' }) {
|
|
144
|
+
const configs = resolvePageFetchConfigs(content, route, { locale })
|
|
145
|
+
return executeFetchConfigs(configs, { content, fetch, dev, prerender })
|
|
146
|
+
}
|
package/src/setup.js
CHANGED
|
@@ -244,8 +244,11 @@ function buildDefaultFetcher(content) {
|
|
|
244
244
|
// recognizes `baseUrl` and `envelope`; foundations with their own fetchers
|
|
245
245
|
// may read additional keys from the same block via ctx.website.config.fetcher.
|
|
246
246
|
const config = content?.config?.fetcher ?? {}
|
|
247
|
+
// The host's live-records stamp (`config.records`), when a backend set one: the fetcher
|
|
248
|
+
// reads its `envelope` for requests that resolved to that lane.
|
|
249
|
+
const records = content?.config?.records ?? null
|
|
247
250
|
const dev = !!(import.meta.env && import.meta.env.DEV)
|
|
248
|
-
return createDefaultFetcher({ basePath, config, dev })
|
|
251
|
+
return createDefaultFetcher({ basePath, config, dev, records })
|
|
249
252
|
}
|
|
250
253
|
|
|
251
254
|
/**
|
package/src/ssr.js
CHANGED
|
@@ -56,6 +56,16 @@ export {
|
|
|
56
56
|
generate404Html,
|
|
57
57
|
} from './ssr-renderer.js'
|
|
58
58
|
|
|
59
|
+
// Server-side prefetch — the runtime executing a page's fetches for a host, so an isolate
|
|
60
|
+
// receives `fetchedData` computed by our fetcher and the host carries no copy of it.
|
|
61
|
+
// [Diego, 2026-09-03]: the backend sets config.records; the fetch comes from the runtime.
|
|
62
|
+
export {
|
|
63
|
+
findPageForRoute,
|
|
64
|
+
resolvePageFetchConfigs,
|
|
65
|
+
executeFetchConfigs,
|
|
66
|
+
prefetchPageData,
|
|
67
|
+
} from './prefetch.js'
|
|
68
|
+
|
|
59
69
|
// Appearance. injectPageContent() already emits this for every prerendered
|
|
60
70
|
// page; exported for lanes that assemble a shell without a per-page render.
|
|
61
71
|
export { renderAppearanceBootScript } from './appearance.js'
|
package/src/wire-foundation.js
CHANGED
|
@@ -163,6 +163,9 @@ export function sliceContentForLocale(content, locale) {
|
|
|
163
163
|
export function hydrateDataStore(website, fetchedData) {
|
|
164
164
|
if (!website?.dataStore || !fetchedData?.length) return
|
|
165
165
|
for (const entry of fetchedData) {
|
|
166
|
+
// A `prefetchPageData` list carries every declared config with an `outcome`; only what was
|
|
167
|
+
// actually fetched enters the store. A list without outcomes (the SSG lane's) is all fetched.
|
|
168
|
+
if (entry.outcome && entry.outcome !== 'fetched') continue
|
|
166
169
|
website.dataStore.set(deriveCacheKey(entry.config), { data: entry.data })
|
|
167
170
|
}
|
|
168
171
|
}
|