Skip to content

Commit c0d6793

Browse files
authored
feat(api-reference): use path routing base path for relative document url resolution (#6339)
* feat: ensure we use the pathRouting base path for relative document url resolution * docs(changeset): feat: ensure we use the path routing base path for relative document url resolution * fix: new params
1 parent 4ea9dab commit c0d6793

File tree

6 files changed

+125
-44
lines changed

6 files changed

+125
-44
lines changed

.changeset/shiny-eggs-exist.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@scalar/api-reference': patch
3+
'@scalar/api-client': patch
4+
'@scalar/helpers': patch
5+
---
6+
7+
feat: ensure we use the path routing base path for relative document url resolution

packages/api-client/src/components/OpenApiClientButton.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const href = computed((): string | undefined => {
2626
const urlToImportFrom =
2727
url ?? (typeof window !== 'undefined' ? window.location.href : undefined)
2828
29+
if (!urlToImportFrom) {
30+
return undefined
31+
}
32+
2933
const absoluteUrl = makeUrlAbsolute(urlToImportFrom)
3034
3135
if (!absoluteUrl?.length) {

packages/api-reference/src/v2/ApiReferenceWorkspace.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
REFERENCE_LS_KEYS,
1919
safeLocalStorage,
2020
} from '@scalar/helpers/object/local-storage'
21+
import { makeUrlAbsolute } from '@scalar/helpers/url/make-url-absolute'
22+
import { combineUrlAndPath } from '@scalar/helpers/url/merge-urls'
2123
import { parseJsonOrYaml, redirectToProxy } from '@scalar/oas-utils/helpers'
2224
import type { AnyApiReferenceConfiguration } from '@scalar/types'
2325
import type { ClientId, TargetId } from '@scalar/types/snippetz'
@@ -136,7 +138,9 @@ const addDocument = (config: (typeof configs.value)[number]) => {
136138
if (config.url) {
137139
return store.addDocument({
138140
name: config.slug ?? 'default',
139-
url: config.url,
141+
url: makeUrlAbsolute(config.url, {
142+
basePath: selectedConfiguration.value.pathRouting?.basePath,
143+
}),
140144
fetch: proxy,
141145
})
142146
}
Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
import { describe, expect, it } from 'vitest'
1+
import { afterEach, describe, expect, it } from 'vitest'
22

33
import { makeUrlAbsolute } from './make-url-absolute'
44

55
/**
66
* @vitest-environment jsdom
77
*/
88
describe('makeUrlAbsolute', () => {
9-
it('returns undefined for undefined input', () => {
10-
expect(makeUrlAbsolute(undefined)).toBeUndefined()
9+
const originalOrigin = window.location.origin
10+
11+
afterEach(() => {
12+
// Restore original window.location.origin
13+
Object.defineProperty(window, 'location', {
14+
value: { origin: originalOrigin },
15+
writable: true,
16+
})
1117
})
1218

1319
it('returns the same URL for absolute URLs', () => {
@@ -17,70 +23,96 @@ describe('makeUrlAbsolute', () => {
1723

1824
it('converts relative URLs to absolute URLs', () => {
1925
// Mock window.location.href
20-
const originalHref = window.location.href
2126
Object.defineProperty(window, 'location', {
2227
value: { href: 'http://example.com/path/' },
2328
writable: true,
2429
})
2530

2631
expect(makeUrlAbsolute('relative')).toBe('http://example.com/path/relative')
2732
expect(makeUrlAbsolute('/absolute-path')).toBe('http://example.com/absolute-path')
28-
29-
// Restore original window.location.href
30-
Object.defineProperty(window, 'location', {
31-
value: { href: originalHref },
32-
writable: true,
33-
})
3433
})
3534

3635
it('handles base URLs without trailing slash', () => {
3736
// Mock window.location.href
38-
const originalHref = window.location.href
3937
Object.defineProperty(window, 'location', {
4038
value: { href: 'http://example.com/path' },
4139
writable: true,
4240
})
4341

4442
expect(makeUrlAbsolute('relative')).toBe('http://example.com/relative')
45-
46-
// Restore original window.location.href
47-
Object.defineProperty(window, 'location', {
48-
value: { href: originalHref },
49-
writable: true,
50-
})
5143
})
5244

5345
it('ignores query parameters and hash in base URL', () => {
5446
// Mock window.location.href
55-
const originalHref = window.location.href
5647
Object.defineProperty(window, 'location', {
5748
value: { href: 'http://example.com/path?query=1#hash' },
5849
writable: true,
5950
})
6051

6152
expect(makeUrlAbsolute('relative')).toBe('http://example.com/relative')
62-
63-
// Restore original window.location.href
64-
Object.defineProperty(window, 'location', {
65-
value: { href: originalHref },
66-
writable: true,
67-
})
6853
})
6954

7055
it('handles parent directory paths', () => {
7156
// Mock window.location.href
72-
const originalHref = window.location.href
7357
Object.defineProperty(window, 'location', {
7458
value: { href: 'http://example.com/path/to/current/' },
7559
writable: true,
7660
})
7761

7862
expect(makeUrlAbsolute('../openapi.json')).toBe('http://example.com/path/to/openapi.json')
63+
})
7964

80-
// Restore original window.location.href
81-
Object.defineProperty(window, 'location', {
82-
value: { href: originalHref },
83-
writable: true,
65+
it('handles base URLs with a path component', () => {
66+
expect(makeUrlAbsolute('examples/openapi.json', { baseUrl: 'http://localhost:5173/' })).toBe(
67+
'http://localhost:5173/examples/openapi.json',
68+
)
69+
expect(makeUrlAbsolute('examples/openapi.json', { baseUrl: 'http://localhost:5173' })).toBe(
70+
'http://localhost:5173/examples/openapi.json',
71+
)
72+
})
73+
74+
describe('basePath functionality', () => {
75+
it('combines basePath with window.location.origin when no baseUrl provided', () => {
76+
// Mock window.location.origin
77+
Object.defineProperty(window, 'location', {
78+
value: { origin: 'http://example.com' },
79+
writable: true,
80+
})
81+
82+
expect(makeUrlAbsolute('api/docs', { basePath: '/app' })).toBe('http://example.com/app/api/docs')
83+
})
84+
85+
it('combines basePath with provided baseUrl', () => {
86+
expect(
87+
makeUrlAbsolute('api/docs', {
88+
baseUrl: 'https://api.example.com',
89+
basePath: '/v1',
90+
}),
91+
).toBe('https://api.example.com/v1/api/docs')
92+
})
93+
94+
it('handles basePath without leading slash', () => {
95+
// Mock window.location.origin
96+
Object.defineProperty(window, 'location', {
97+
value: { origin: 'http://example.com' },
98+
writable: true,
99+
})
100+
101+
expect(makeUrlAbsolute('api/docs', { basePath: 'app' })).toBe('http://example.com/app/api/docs')
102+
})
103+
104+
it('handles basePath with trailing slash', () => {
105+
// Mock window.location.origin
106+
Object.defineProperty(window, 'location', {
107+
value: { origin: 'http://example.com' },
108+
writable: true,
109+
})
110+
111+
expect(makeUrlAbsolute('api/docs', { basePath: '/app/' })).toBe('http://example.com/app/api/docs')
112+
})
113+
114+
it('ignores basePath for absolute URLs', () => {
115+
expect(makeUrlAbsolute('https://example.com/api', { basePath: '/app' })).toBe('https://example.com/api')
84116
})
85117
})
86118
})
Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,51 @@
1+
import { combineUrlAndPath } from '@/url/merge-urls'
2+
13
/**
2-
* Pass an URL or a relative URL and get an absolute URL
4+
* Converts a relative URL to an absolute URL using the provided base URL or current window location.
5+
* @param url - The URL to make absolute
6+
* @param options - Configuration options
7+
* @param options.baseUrl - Optional base URL to resolve against (defaults to window.location.href)
8+
* @param options.basePath - If provided, combines with baseUrl or window.location.origin before resolving
9+
* @returns The absolute URL, or the original URL if it's already absolute or invalid
310
*/
4-
export const makeUrlAbsolute = (url?: string, baseUrl?: string) => {
5-
if (!url || url.startsWith('http://') || url.startsWith('https://') || (typeof window === 'undefined' && !baseUrl)) {
11+
export const makeUrlAbsolute = (
12+
url: string,
13+
{
14+
/** Optional base URL to resolve against (defaults to window.location.href) */
15+
baseUrl,
16+
/** If we have a basePath then we resolve against window.location.origin + basePath */
17+
basePath,
18+
}: {
19+
baseUrl?: string
20+
basePath?: string
21+
} = {},
22+
): string => {
23+
// If no base URL provided and we're not in a browser environment, return as-is
24+
if (typeof window === 'undefined' && !baseUrl) {
625
return url
726
}
827

9-
const base = baseUrl || window.location.href
28+
try {
29+
// If we can create a URL object without a base, it's already absolute
30+
new URL(url)
31+
return url
32+
} catch {
33+
// URL is relative, proceed with resolution
34+
}
1035

11-
// Remove any query parameters or hash from the base URL
12-
const cleanBaseUrl = base.split('?')[0]?.split('#')[0]
36+
// Use URL constructor which handles path normalization automatically
37+
try {
38+
let base = baseUrl || window.location.href
1339

14-
// For base URLs with a path component, we want to remove the last path segment
15-
// if it doesn't end with a slash
16-
const normalizedBaseUrl = cleanBaseUrl?.endsWith('/')
17-
? cleanBaseUrl
18-
: cleanBaseUrl?.substring(0, cleanBaseUrl?.lastIndexOf('/') + 1)
40+
// If basePath is provided, combine it with the base URL
41+
if (basePath) {
42+
const origin = baseUrl ? new URL(baseUrl).origin : window.location.origin
43+
base = combineUrlAndPath(origin, basePath + '/')
44+
}
1945

20-
return new URL(url, normalizedBaseUrl).toString()
46+
return new URL(url, base).toString()
47+
} catch {
48+
// If URL construction fails, return the original URL
49+
return url
50+
}
2151
}

packages/import/src/resolve.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,17 @@ export async function resolve(
107107

108108
// Relative or absolute URL
109109
if (urlOrPathOrDocument) {
110-
return makeUrlAbsolute(urlOrPathOrDocument, forwardedHost || value)
110+
return makeUrlAbsolute(urlOrPathOrDocument, {
111+
baseUrl: forwardedHost || value,
112+
})
111113
}
112114

113115
// Check for configuration attribute URL
114116
const configUrl = getConfigurationAttributeUrl(content)
115117
if (configUrl) {
116-
return makeUrlAbsolute(configUrl, forwardedHost || value)
118+
return makeUrlAbsolute(configUrl, {
119+
baseUrl: forwardedHost || value,
120+
})
117121
}
118122

119123
// Check for embedded OpenAPI document

0 commit comments

Comments
 (0)