@uniweb/kit 0.10.14 → 0.10.16
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 +3 -3
- package/src/index.js +5 -1
- package/src/search/client.js +27 -2
- package/src/search/providers/endpoint-provider.js +7 -6
- package/src/utils/index.js +1 -0
- package/src/utils/services.js +148 -0
- package/src/utils/submitTarget.js +29 -87
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.16",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"fuse.js": "^7.0.0",
|
|
44
44
|
"shiki": "^3.0.0",
|
|
45
45
|
"tailwind-merge": "^3.6.0",
|
|
46
|
-
"@uniweb/
|
|
46
|
+
"@uniweb/core": "0.8.2",
|
|
47
47
|
"@uniweb/scene": "0.1.2",
|
|
48
|
-
"@uniweb/
|
|
48
|
+
"@uniweb/semantic-parser": "1.2.1"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"react": "^19.0.0",
|
package/src/index.js
CHANGED
|
@@ -126,7 +126,11 @@ export {
|
|
|
126
126
|
submitForm,
|
|
127
127
|
deriveSummary,
|
|
128
128
|
resolveSubmitTarget,
|
|
129
|
-
NO_SUBMIT_TARGET_REASON
|
|
129
|
+
NO_SUBMIT_TARGET_REASON,
|
|
130
|
+
// Site services (search / submit / assistant / anything a host offers)
|
|
131
|
+
resolveService,
|
|
132
|
+
resolveServiceUrl,
|
|
133
|
+
NO_SERVICE_REASON
|
|
130
134
|
} from './utils/index.js'
|
|
131
135
|
|
|
132
136
|
// ============================================================================
|
package/src/search/client.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { emptyResult } from './providers/result.js'
|
|
31
|
+
import { resolveService } from '../utils/services.js'
|
|
31
32
|
|
|
32
33
|
// Re-exported so existing importers of these keep working; they live with the
|
|
33
34
|
// provider that owns them now.
|
|
@@ -92,7 +93,24 @@ export function createSearchClient(website, options = {}) {
|
|
|
92
93
|
} = options
|
|
93
94
|
|
|
94
95
|
const searchConfig = website.getSearchConfig?.() || {}
|
|
95
|
-
|
|
96
|
+
|
|
97
|
+
// Where search is answered: the site's own `search.endpoint`, else the host's
|
|
98
|
+
// `services.search`, else nothing. Same rule every site service resolves by.
|
|
99
|
+
const service = resolveService(website, 'search')
|
|
100
|
+
|
|
101
|
+
// The AUTHORED provider, read from config rather than from getSearchConfig(),
|
|
102
|
+
// which fills in 'index' before kit sees it (core/website.js). That default
|
|
103
|
+
// would make "the author chose index" and "the author said nothing"
|
|
104
|
+
// indistinguishable — and the difference is precisely what decides whether a
|
|
105
|
+
// host's offer applies. An author who picked a provider means it; one who
|
|
106
|
+
// picked nothing gets whatever the host serves, and the local index if the
|
|
107
|
+
// host serves none.
|
|
108
|
+
const authoredProvider = website?.config?.search?.provider
|
|
109
|
+
|
|
110
|
+
const declared =
|
|
111
|
+
providerOverride ||
|
|
112
|
+
authoredProvider ||
|
|
113
|
+
(service.source === 'host' ? 'endpoint' : 'index')
|
|
96
114
|
|
|
97
115
|
// One in-flight resolution shared by every caller.
|
|
98
116
|
let providerPromise = null
|
|
@@ -115,7 +133,14 @@ export function createSearchClient(website, options = {}) {
|
|
|
115
133
|
activeName = 'index'
|
|
116
134
|
}
|
|
117
135
|
|
|
118
|
-
|
|
136
|
+
// `service.url` is already joined to the base; the endpoint provider
|
|
137
|
+
// joins again, which is a no-op because the join is idempotent. Falls
|
|
138
|
+
// back to the raw authored value, and to the provider's own default when
|
|
139
|
+
// an author selects `endpoint` without naming one.
|
|
140
|
+
return factory(website, {
|
|
141
|
+
...providerOptions,
|
|
142
|
+
endpoint: service.url || searchConfig.endpoint,
|
|
143
|
+
})
|
|
119
144
|
})()
|
|
120
145
|
|
|
121
146
|
return providerPromise
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { emptyResult } from './result.js'
|
|
17
|
+
import { resolveServiceUrl } from '../../utils/services.js'
|
|
17
18
|
|
|
18
19
|
/** Path used when a site declares `provider: endpoint` without an `endpoint:`. */
|
|
19
20
|
const DEFAULT_ENDPOINT = '_search'
|
|
@@ -31,17 +32,17 @@ const DEFAULT_ENDPOINT = '_search'
|
|
|
31
32
|
* An absolute URL is passed through untouched, for a search service on another
|
|
32
33
|
* origin.
|
|
33
34
|
*
|
|
35
|
+
* The join itself is the shared one every site service uses — this function is
|
|
36
|
+
* now only the *default*, which is the one thing search has that the others must
|
|
37
|
+
* not: a form submission with no declared target has nowhere to go, whereas
|
|
38
|
+
* search with no declared endpoint has a conventional one.
|
|
39
|
+
*
|
|
34
40
|
* @param {string} endpoint - Declared endpoint (relative or absolute)
|
|
35
41
|
* @param {string} basePath - `website.basePath`, normalized without a trailing slash
|
|
36
42
|
* @returns {string}
|
|
37
43
|
*/
|
|
38
44
|
export function resolveEndpointUrl(endpoint, basePath = '') {
|
|
39
|
-
|
|
40
|
-
if (/^https?:\/\//i.test(path)) return path
|
|
41
|
-
|
|
42
|
-
const base = (basePath || '').replace(/\/+$/, '')
|
|
43
|
-
const rel = path.replace(/^\/+/, '')
|
|
44
|
-
return `${base}/${rel}`
|
|
45
|
+
return resolveServiceUrl(endpoint || DEFAULT_ENDPOINT, basePath)
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
package/src/utils/index.js
CHANGED
|
@@ -249,6 +249,7 @@ export function detectMediaType(url) {
|
|
|
249
249
|
|
|
250
250
|
export { submitForm, deriveSummary } from './submitForm.js'
|
|
251
251
|
export { resolveSubmitTarget, NO_SUBMIT_TARGET_REASON } from './submitTarget.js'
|
|
252
|
+
export { resolveService, resolveServiceUrl, NO_SERVICE_REASON } from './services.js'
|
|
252
253
|
|
|
253
254
|
/**
|
|
254
255
|
* The text of a ProseMirror node, flattened.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { applyBasePath } from './href.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Site services — where a site's search, form submissions, assistant, or
|
|
5
|
+
* anything else of that shape actually go.
|
|
6
|
+
*
|
|
7
|
+
* ## The one idea
|
|
8
|
+
*
|
|
9
|
+
* A component must never name a host. Whether this site's search is answered by
|
|
10
|
+
* a prebuilt index, a server endpoint, or a vendor API is a *deployment* fact,
|
|
11
|
+
* and a foundation that hardcodes it is coupled to one deployment. So the
|
|
12
|
+
* address comes from configuration, and there are exactly two places it can
|
|
13
|
+
* come from:
|
|
14
|
+
*
|
|
15
|
+
* 1. **The site**, authored — `search:`, `submit:`, `assistant:` in site.yml.
|
|
16
|
+
* The operator's own declaration, and it wins.
|
|
17
|
+
* 2. **The host**, served — `config.services.<name>` in the payload. What the
|
|
18
|
+
* deployment offers, which the site never had to know about.
|
|
19
|
+
*
|
|
20
|
+
* Absent from both means the site has no such service, and the component says so
|
|
21
|
+
* rather than guessing an address. That is the same rule for every service, and
|
|
22
|
+
* it is why this module exists: it was previously implemented three times — the
|
|
23
|
+
* search provider, the submit resolver, and a hand-rolled copy inside a
|
|
24
|
+
* foundation — with three slightly different base-joining rules between them.
|
|
25
|
+
*
|
|
26
|
+
* ## The registry is open, not an enum
|
|
27
|
+
*
|
|
28
|
+
* `resolveService(website, name)` takes a *name*, and the framework has no list
|
|
29
|
+
* of permitted ones. It ships **clients** only for what it already implements
|
|
30
|
+
* (search, form submission); it ships **resolution** for anything. A foundation
|
|
31
|
+
* that invents `assistant`, `booking` or `translate` gets the same precedence,
|
|
32
|
+
* the same base handling and the same absent-means-absent behaviour, and a host
|
|
33
|
+
* can fill the slot without a framework change.
|
|
34
|
+
*
|
|
35
|
+
* This is deliberate and it is the same shape as `fetcher.transports`: the
|
|
36
|
+
* framework owns the seam, not the catalogue.
|
|
37
|
+
*
|
|
38
|
+
* ## What this deliberately does not model
|
|
39
|
+
*
|
|
40
|
+
* **Entitlement.** A host that will not serve a service either omits it or
|
|
41
|
+
* supplies a `reason`, which is relayed to the UI verbatim. The framework never
|
|
42
|
+
* learns why — no plan names, no tiers, no "paid" anywhere. That is not
|
|
43
|
+
* squeamishness: `@uniweb/kit` is public, and a framework that encodes which
|
|
44
|
+
* capabilities cost money ships the business model into open source.
|
|
45
|
+
*
|
|
46
|
+
* **The site's own base.** `config.base` is where the site *lives*, not a
|
|
47
|
+
* service it consumes — it is load-bearing for routing and asset URLs too. It
|
|
48
|
+
* stays where it is and is an input here, not an entry.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/** Anything with a scheme, or protocol-relative — never joined to a base. */
|
|
52
|
+
const ABSOLUTE_URL_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i
|
|
53
|
+
|
|
54
|
+
/** Shown when nothing supplies a service and the caller gives no wording. */
|
|
55
|
+
export const NO_SERVICE_REASON = 'This site has no such service configured.'
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read an endpoint out of either declaration form.
|
|
59
|
+
*
|
|
60
|
+
* A site may write the shorthand (`submit: /forms`) or the object
|
|
61
|
+
* (`submit: { endpoint: /forms }`); a host emits JSON and normally writes the
|
|
62
|
+
* object. Both are accepted from both sides — one reader, no per-side rules to
|
|
63
|
+
* remember.
|
|
64
|
+
*
|
|
65
|
+
* @param {*} declaration
|
|
66
|
+
* @returns {string} the endpoint, or '' when there is none
|
|
67
|
+
*/
|
|
68
|
+
function readEndpoint(declaration) {
|
|
69
|
+
if (typeof declaration === 'string') return declaration.trim()
|
|
70
|
+
if (typeof declaration?.endpoint === 'string') return declaration.endpoint.trim()
|
|
71
|
+
return ''
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Join a service endpoint to the site's base path.
|
|
76
|
+
*
|
|
77
|
+
* Three cases, and the middle one is why this is not simply `applyBasePath`:
|
|
78
|
+
*
|
|
79
|
+
* - **Absolute** (`https://…`, `//host/…`, any scheme) — passed through. A
|
|
80
|
+
* service on another origin is not the site's to relocate.
|
|
81
|
+
* - **Bare relative** (`_search`) — rooted first. This spelling is documented
|
|
82
|
+
* and in use, and `applyBasePath` alone would leave it untouched, silently
|
|
83
|
+
* producing a request relative to whatever page the visitor is on.
|
|
84
|
+
* - **Root-relative** (`/forms`) — the ordinary case.
|
|
85
|
+
*
|
|
86
|
+
* The join itself goes through `applyBasePath` rather than concatenation,
|
|
87
|
+
* because that is where the invariant "a base is only ever joined to a path that
|
|
88
|
+
* starts at the site root" is enforced, and it is idempotent — an
|
|
89
|
+
* already-based path is not based twice.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} endpoint
|
|
92
|
+
* @param {string} [basePath] - `website.basePath`
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
export function resolveServiceUrl(endpoint, basePath = '') {
|
|
96
|
+
if (!endpoint) return ''
|
|
97
|
+
if (ABSOLUTE_URL_RE.test(endpoint)) return endpoint
|
|
98
|
+
|
|
99
|
+
const rooted = endpoint.startsWith('/') ? endpoint : `/${endpoint}`
|
|
100
|
+
// `applyBasePath` concatenates and documents its input as carrying no
|
|
101
|
+
// trailing slash, so normalizing is the caller's job — skip it and
|
|
102
|
+
// `base: /docs/` yields `/docs//forms`.
|
|
103
|
+
const base = (basePath || '').replace(/\/+$/, '')
|
|
104
|
+
return applyBasePath(rooted, base)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve where a named service lives for this site.
|
|
109
|
+
*
|
|
110
|
+
* ```js
|
|
111
|
+
* const { url, reason } = resolveService(website, 'submit')
|
|
112
|
+
* if (!url) return renderDisabled(reason)
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* @param {object} website - the active Website
|
|
116
|
+
* @param {string} name - service name, e.g. 'submit' · 'search' · 'assistant'
|
|
117
|
+
* @param {object} [options]
|
|
118
|
+
* @param {string} [options.reason] - wording when nothing supplies it
|
|
119
|
+
* @returns {{ url: string|null, reason: string|null, source: 'site'|'host'|null }}
|
|
120
|
+
* `source` says which declaration answered — useful in diagnostics, and the
|
|
121
|
+
* thing to check when a host's value appears not to be taking effect.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveService(website, name, options = {}) {
|
|
124
|
+
const config = website?.config
|
|
125
|
+
const basePath = website?.basePath
|
|
126
|
+
|
|
127
|
+
// 1 — the site's own declaration wins. An operator who named an endpoint
|
|
128
|
+
// means it, including on a host that offers one.
|
|
129
|
+
const authored = readEndpoint(config?.[name])
|
|
130
|
+
if (authored) {
|
|
131
|
+
return { url: resolveServiceUrl(authored, basePath), reason: null, source: 'site' }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 2 — what the host says it offers.
|
|
135
|
+
const hostDeclaration = config?.services?.[name]
|
|
136
|
+
const hostEndpoint = readEndpoint(hostDeclaration)
|
|
137
|
+
if (hostEndpoint) {
|
|
138
|
+
return { url: resolveServiceUrl(hostEndpoint, basePath), reason: null, source: 'host' }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A host that declines may say why, and that reaches the visitor unaltered.
|
|
142
|
+
const hostReason =
|
|
143
|
+
typeof hostDeclaration?.reason === 'string' ? hostDeclaration.reason.trim() : ''
|
|
144
|
+
if (hostReason) return { url: null, reason: hostReason, source: 'host' }
|
|
145
|
+
|
|
146
|
+
// 3 — nobody supplied one.
|
|
147
|
+
return { url: null, reason: options.reason || NO_SERVICE_REASON, source: null }
|
|
148
|
+
}
|
|
@@ -1,38 +1,21 @@
|
|
|
1
|
+
import { resolveService, resolveServiceUrl } from './services.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* A site declares its endpoint under `submit:` in site.yml, and the value
|
|
5
|
-
* arrives here as `website.config.submit` — the same passthrough every other
|
|
6
|
-
* site.yml key rides (`collections:`, `search:`, `fetcher:`). Two spellings,
|
|
7
|
-
* because one destination is the common case and the object form leaves room
|
|
8
|
-
* to grow:
|
|
9
|
-
*
|
|
10
|
-
* submit: /forms # shorthand
|
|
11
|
-
* submit: { endpoint: /forms } # object form
|
|
12
|
-
* submit: https://forms.example.com/intake
|
|
4
|
+
* Where this site's form submissions go.
|
|
13
5
|
*
|
|
14
|
-
* A
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
6
|
+
* A thin naming of the general rule in `./services.js` — a site declares
|
|
7
|
+
* `submit:` in site.yml, a host may offer one under `config.services.submit`,
|
|
8
|
+
* and absent from both means the form renders disabled rather than posting a
|
|
9
|
+
* visitor's answers to an address nobody claimed.
|
|
18
10
|
*
|
|
19
|
-
* ##
|
|
11
|
+
* ## There is deliberately no default
|
|
20
12
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* 3. Neither — `url: null` plus a generic reason, so the form renders disabled.
|
|
28
|
-
*
|
|
29
|
-
* What the framework must never do is invent one. It cannot know where a given
|
|
30
|
-
* site can accept a submission, and guessing is worse than having none: on a
|
|
31
|
-
* static host, or any deployment with nothing listening, the form POSTs a
|
|
32
|
-
* visitor's data into a 404 and nothing here can tell. The asymmetry with
|
|
33
|
-
* reading matters — a fetch that 404s degrades to `[]` and the page still
|
|
34
|
-
* renders, which is a documented guarantee, but a write that 404s loses what
|
|
35
|
-
* someone typed.
|
|
13
|
+
* The framework does not know where a given site can accept a submission, and
|
|
14
|
+
* picking a path on the site's behalf is worse than having none: on a static
|
|
15
|
+
* host, or any deployment with nothing listening, the form POSTs into a 404 and
|
|
16
|
+
* nothing here can tell. The asymmetry with reading matters — a fetch that 404s
|
|
17
|
+
* degrades to `[]` and the page still renders, which is a documented guarantee,
|
|
18
|
+
* but a write that 404s loses what someone typed.
|
|
36
19
|
*
|
|
37
20
|
* Note that a host supplying a site-local path is NOT the same as the framework
|
|
38
21
|
* defaulting to one, even when the two produce an identical request: a host only
|
|
@@ -41,17 +24,17 @@
|
|
|
41
24
|
*
|
|
42
25
|
* ## Simulating a host locally
|
|
43
26
|
*
|
|
44
|
-
* The bundle lane spreads all of site.yml into the payload config, so writing
|
|
45
|
-
* `
|
|
27
|
+
* The bundle lane spreads all of site.yml into the payload config, so writing a
|
|
28
|
+
* `services:` block in site.yml exercises the host tier end to end with no
|
|
46
29
|
* framework change. It cannot leak into a real deployment: the sync lane is an
|
|
47
|
-
* explicit allowlist and does not carry `
|
|
30
|
+
* explicit allowlist and does not carry `services`, so a synced site's value can
|
|
48
31
|
* only have come from its host.
|
|
49
32
|
*/
|
|
50
33
|
|
|
51
34
|
/**
|
|
52
|
-
* Shown when a site declares no endpoint. English, and a
|
|
53
|
-
* ignore it and render its own copy — check `url` for the
|
|
54
|
-
* as a default rather than a string to translate.
|
|
35
|
+
* Shown when a site declares no endpoint and no host offers one. English, and a
|
|
36
|
+
* foundation is free to ignore it and render its own copy — check `url` for the
|
|
37
|
+
* yes/no and treat this as a default rather than a string to translate.
|
|
55
38
|
*/
|
|
56
39
|
export const NO_SUBMIT_TARGET_REASON =
|
|
57
40
|
'This site has no form submission endpoint configured.'
|
|
@@ -60,65 +43,24 @@ export const NO_SUBMIT_TARGET_REASON =
|
|
|
60
43
|
* Resolve the submission target for a site.
|
|
61
44
|
*
|
|
62
45
|
* @param {object} website - The active Website instance
|
|
63
|
-
* @returns {{ url: string|null, reason: string|null }}
|
|
64
|
-
* `url` is the resolved
|
|
65
|
-
*
|
|
46
|
+
* @returns {{ url: string|null, reason: string|null, source: 'site'|'host'|null }}
|
|
47
|
+
* `url` is the resolved endpoint, or null when nothing supplies one. `reason`
|
|
48
|
+
* is set exactly when `url` is null.
|
|
66
49
|
*/
|
|
67
50
|
export function resolveSubmitTarget(website) {
|
|
68
|
-
|
|
69
|
-
const basePath = website?.basePath
|
|
70
|
-
|
|
71
|
-
// Tier 1 — the site operator's own declaration (`submit:` in site.yml).
|
|
72
|
-
const declared = config?.submit
|
|
73
|
-
const declaredEndpoint =
|
|
74
|
-
typeof declared === 'string'
|
|
75
|
-
? declared.trim()
|
|
76
|
-
: typeof declared?.endpoint === 'string'
|
|
77
|
-
? declared.endpoint.trim()
|
|
78
|
-
: ''
|
|
79
|
-
|
|
80
|
-
if (declaredEndpoint) {
|
|
81
|
-
return { url: resolveAgainstBase(declaredEndpoint, basePath), reason: null }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Tier 2 — a destination the HOST reports, under `forms` in the served
|
|
85
|
-
// payload config. Named for the capability rather than the action so it
|
|
86
|
-
// cannot be misread as the authored `submit` sitting beside it in the same
|
|
87
|
-
// flat object.
|
|
88
|
-
const host = config?.forms
|
|
89
|
-
const hostEndpoint = typeof host?.endpoint === 'string' ? host.endpoint.trim() : ''
|
|
90
|
-
if (hostEndpoint) {
|
|
91
|
-
return { url: resolveAgainstBase(hostEndpoint, basePath), reason: null }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// A host that reports WHY it cannot accept submissions gets that relayed
|
|
95
|
-
// verbatim. The framework does not interpret, reword, or second-guess it: it
|
|
96
|
-
// has no standing to judge why a given site can or cannot accept them, and
|
|
97
|
-
// guessing at the reason in public code would mean encoding someone else's
|
|
98
|
-
// policy here.
|
|
99
|
-
const hostReason = typeof host?.reason === 'string' ? host.reason.trim() : ''
|
|
100
|
-
if (hostReason) return { url: null, reason: hostReason }
|
|
101
|
-
|
|
102
|
-
// Tier 3 — nobody supplied one.
|
|
103
|
-
return { url: null, reason: NO_SUBMIT_TARGET_REASON }
|
|
51
|
+
return resolveService(website, 'submit', { reason: NO_SUBMIT_TARGET_REASON })
|
|
104
52
|
}
|
|
105
53
|
|
|
106
54
|
/**
|
|
107
|
-
* Join
|
|
55
|
+
* Join an endpoint to the site's base path.
|
|
108
56
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* when given nothing, which is precisely the behaviour a submission target must
|
|
112
|
-
* not have (see the header). Three lines of duplication is the cheaper mistake.
|
|
57
|
+
* Retained as a named export because it shipped as one. Prefer
|
|
58
|
+
* `resolveServiceUrl` from `./services.js` — this is that function.
|
|
113
59
|
*
|
|
114
60
|
* @param {string} endpoint - Declared endpoint, relative or absolute
|
|
115
|
-
* @param {string} [basePath] - `website.basePath
|
|
61
|
+
* @param {string} [basePath] - `website.basePath`
|
|
116
62
|
* @returns {string}
|
|
117
63
|
*/
|
|
118
64
|
export function resolveAgainstBase(endpoint, basePath = '') {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const base = (basePath || '').replace(/\/+$/, '')
|
|
122
|
-
const rel = endpoint.replace(/^\/+/, '')
|
|
123
|
-
return `${base}/${rel}`
|
|
65
|
+
return resolveServiceUrl(endpoint, basePath)
|
|
124
66
|
}
|