@scalar/import 0.5.2 → 0.5.3
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/CHANGELOG.md +6 -0
- package/dist/index.js +1 -5
- package/dist/resolve.js +294 -214
- package/package.json +6 -12
- package/dist/index.js.map +0 -7
- package/dist/resolve.js.map +0 -7
package/CHANGELOG.md
CHANGED
package/dist/index.js
CHANGED
package/dist/resolve.js
CHANGED
|
@@ -1,237 +1,317 @@
|
|
|
1
|
-
import { makeUrlAbsolute } from
|
|
2
|
-
import { parse } from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (value?.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
const sandboxUrl = value.match(/https:\/\/sandbox\.scalar\.com\/(p|e)\/([a-zA-Z0-9]+)/);
|
|
16
|
-
if (sandboxUrl?.[2]) {
|
|
17
|
-
return `https://sandbox.scalar.com/files/${sandboxUrl[2]}/openapi.yaml`;
|
|
18
|
-
}
|
|
19
|
-
try {
|
|
20
|
-
let result = await (options?.fetch ? options.fetch(value) : fetch(value, {
|
|
21
|
-
cache: "no-cache"
|
|
22
|
-
}));
|
|
23
|
-
if (!result.ok && options?.fetch) {
|
|
24
|
-
result = await fetch(value, {
|
|
25
|
-
cache: "no-cache"
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
if (result.ok) {
|
|
29
|
-
const content = await result.text();
|
|
30
|
-
const forwardedHost = result.headers.get("X-Forwarded-Host");
|
|
31
|
-
try {
|
|
32
|
-
const jsonContent = JSON.parse(content);
|
|
33
|
-
if (jsonContent.openapi || jsonContent.swagger) {
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
} catch {
|
|
37
|
-
try {
|
|
38
|
-
const yamlContent = parse(content);
|
|
39
|
-
if (yamlContent?.openapi || yamlContent?.swagger) {
|
|
40
|
-
return value;
|
|
41
|
-
}
|
|
42
|
-
} catch {
|
|
43
|
-
}
|
|
1
|
+
import { makeUrlAbsolute } from '@scalar/helpers/url/make-url-absolute';
|
|
2
|
+
import { parse } from 'yaml';
|
|
3
|
+
/**
|
|
4
|
+
* Find an OpenAPI document URL in the HTML of @scalar/api-reference and other places.
|
|
5
|
+
* This is useful to open the OpenAPI document from basically any source.
|
|
6
|
+
*/
|
|
7
|
+
export async function resolve(value, options) {
|
|
8
|
+
// URLs
|
|
9
|
+
if (value?.startsWith('http://') || value?.startsWith('https://')) {
|
|
10
|
+
// Transform GitHub URLs to raw file URLs
|
|
11
|
+
const githubRawUrl = transformGitHubUrl(value);
|
|
12
|
+
if (githubRawUrl) {
|
|
13
|
+
return githubRawUrl;
|
|
44
14
|
}
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
return JSON.parse(urlOrPathOrDocument);
|
|
49
|
-
} catch {
|
|
50
|
-
try {
|
|
51
|
-
const yaml = parse(urlOrPathOrDocument);
|
|
52
|
-
if (typeof yaml === "object") {
|
|
53
|
-
return yaml;
|
|
54
|
-
}
|
|
55
|
-
} catch {
|
|
56
|
-
}
|
|
57
|
-
}
|
|
15
|
+
// https://*.json
|
|
16
|
+
if (value?.toLowerCase().endsWith('.json')) {
|
|
17
|
+
return value;
|
|
58
18
|
}
|
|
59
|
-
|
|
60
|
-
|
|
19
|
+
// https://*.yaml
|
|
20
|
+
if (value?.toLowerCase().endsWith('.yaml') || value?.toLowerCase().endsWith('.yml')) {
|
|
21
|
+
return value;
|
|
61
22
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
23
|
+
// https://sandbox.scalar.com
|
|
24
|
+
const sandboxUrl = value.match(/https:\/\/sandbox\.scalar\.com\/(p|e)\/([a-zA-Z0-9]+)/);
|
|
25
|
+
if (sandboxUrl?.[2]) {
|
|
26
|
+
return `https://sandbox.scalar.com/files/${sandboxUrl[2]}/openapi.yaml`;
|
|
66
27
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
28
|
+
// Fetch URL
|
|
29
|
+
try {
|
|
30
|
+
let result = await (options?.fetch
|
|
31
|
+
? options.fetch(value)
|
|
32
|
+
: fetch(value, {
|
|
33
|
+
cache: 'no-cache',
|
|
34
|
+
}));
|
|
35
|
+
// If the custom fetch failed, try again with regular fetch
|
|
36
|
+
if (!result.ok && options?.fetch) {
|
|
37
|
+
result = await fetch(value, {
|
|
38
|
+
cache: 'no-cache',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (result.ok) {
|
|
42
|
+
const content = await result.text();
|
|
43
|
+
const forwardedHost = result.headers.get('X-Forwarded-Host');
|
|
44
|
+
// Check if content is directly JSON/YAML
|
|
45
|
+
try {
|
|
46
|
+
// Try parsing as JSON
|
|
47
|
+
const jsonContent = JSON.parse(content);
|
|
48
|
+
if (jsonContent.openapi || jsonContent.swagger) {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Try parsing as YAML
|
|
54
|
+
try {
|
|
55
|
+
const yamlContent = parse(content);
|
|
56
|
+
if (yamlContent?.openapi || yamlContent?.swagger) {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Not YAML
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const urlOrPathOrDocument = parseHtml(content);
|
|
65
|
+
// Document (string)
|
|
66
|
+
if (typeof urlOrPathOrDocument === 'string') {
|
|
67
|
+
try {
|
|
68
|
+
// JSON?
|
|
69
|
+
return JSON.parse(urlOrPathOrDocument);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// No JSON
|
|
73
|
+
try {
|
|
74
|
+
// YAML?
|
|
75
|
+
const yaml = parse(urlOrPathOrDocument);
|
|
76
|
+
if (typeof yaml === 'object') {
|
|
77
|
+
return yaml;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Not YAML
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Document (object)
|
|
86
|
+
if (typeof urlOrPathOrDocument === 'object') {
|
|
87
|
+
return urlOrPathOrDocument;
|
|
88
|
+
}
|
|
89
|
+
// Relative or absolute URL
|
|
90
|
+
if (urlOrPathOrDocument) {
|
|
91
|
+
return makeUrlAbsolute(urlOrPathOrDocument, {
|
|
92
|
+
baseUrl: forwardedHost || value,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Check for configuration attribute URL
|
|
96
|
+
const configUrl = getConfigurationAttributeUrl(content);
|
|
97
|
+
if (configUrl) {
|
|
98
|
+
return makeUrlAbsolute(configUrl, {
|
|
99
|
+
baseUrl: forwardedHost || value,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Check for embedded OpenAPI document
|
|
103
|
+
const embeddedSpec = parseEmbeddedOpenApi(content);
|
|
104
|
+
if (embeddedSpec) {
|
|
105
|
+
return embeddedSpec;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
console.warn(`[@scalar/import] Failed to fetch ${value}`);
|
|
110
|
+
}
|
|
72
111
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return embeddedSpec;
|
|
112
|
+
catch (error) {
|
|
113
|
+
console.error(`[@scalar/import] Failed to fetch ${value}`, error);
|
|
76
114
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
} catch (error) {
|
|
81
|
-
console.error(`[@scalar/import] Failed to fetch ${value}`, error);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return void 0;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
85
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Go through the HTML and try to find the OpenAPI document URL
|
|
120
|
+
*/
|
|
86
121
|
function parseHtml(html) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
// Check whether it could be HTML
|
|
123
|
+
if (!html?.includes('<')) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
// id="api-reference" data-url="*"
|
|
127
|
+
const dataUrlMatch = html
|
|
128
|
+
.match(/id=["']api-reference["'][\s\S]*?data-url=["']([^"']+)["']/)
|
|
129
|
+
?.slice(1)
|
|
130
|
+
.find(Boolean);
|
|
131
|
+
if (dataUrlMatch) {
|
|
132
|
+
return dataUrlMatch;
|
|
133
|
+
}
|
|
134
|
+
// spec-url="*"
|
|
135
|
+
const specUrlMatch = html.match(/spec-url=["']([^"']+)["']/);
|
|
136
|
+
if (specUrlMatch?.[1]) {
|
|
137
|
+
return specUrlMatch[1];
|
|
138
|
+
}
|
|
139
|
+
// Redoc.init('*')
|
|
140
|
+
const redocInit = html.match(/Redoc.init\(["']([^"']+)["']/);
|
|
141
|
+
if (redocInit?.[1]) {
|
|
142
|
+
return redocInit[1];
|
|
143
|
+
}
|
|
144
|
+
// &quot;url&quot;:&quot;MY_CUSTOM_URL&quot;
|
|
145
|
+
const doubleEncodedConfigurationUrl = html.match(/&quot;url&quot;:&quot;([^;]+)&quot;/);
|
|
146
|
+
if (doubleEncodedConfigurationUrl?.[1]) {
|
|
147
|
+
return doubleEncodedConfigurationUrl[1];
|
|
148
|
+
}
|
|
149
|
+
// &quot;url&quot;:&quot;MY_CUSTOM_URL&quot;
|
|
150
|
+
const encodedConfigurationUrl = html.match(/"url":"([^;]+)"/);
|
|
151
|
+
if (encodedConfigurationUrl?.[1]) {
|
|
152
|
+
return encodedConfigurationUrl[1];
|
|
153
|
+
}
|
|
154
|
+
// Try to find embedded OpenAPI document in script tag first
|
|
155
|
+
const scriptContent = parseScriptContent(html);
|
|
156
|
+
if (scriptContent) {
|
|
157
|
+
return scriptContent;
|
|
158
|
+
}
|
|
159
|
+
// Check for configuration in script tag
|
|
160
|
+
const scriptConfigMatch = html.match(/url:\s*["']([^"']+)["']/i);
|
|
161
|
+
if (scriptConfigMatch?.[1]) {
|
|
162
|
+
return scriptConfigMatch?.[1];
|
|
163
|
+
}
|
|
164
|
+
// Check for OpenAPI URLs in the HTML
|
|
165
|
+
const linkMatch = html.match(/<a[^>]*href=["']([^"']+\.(?:yaml|yml|json))["'][^>]*>/i);
|
|
166
|
+
if (linkMatch?.[1]) {
|
|
167
|
+
return linkMatch[1];
|
|
168
|
+
}
|
|
169
|
+
// Check for URLs in escaped JS objects
|
|
170
|
+
const escapedJsonMatch = html.match(/\\"spec\\":\{.*?\\"url\\":\\"([^"\\]+)\\"/);
|
|
171
|
+
if (escapedJsonMatch?.[1]) {
|
|
172
|
+
return escapedJsonMatch[1];
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
127
175
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Get the content between the script tags
|
|
178
|
+
*
|
|
179
|
+
* @example <script id="api-reference">console.log("Hello, world!");</script>
|
|
180
|
+
*/
|
|
181
|
+
export function getContentOfScriptTag(html) {
|
|
182
|
+
const patterns = [
|
|
183
|
+
// Double quotes
|
|
184
|
+
/<script[^>]*id="api-reference[\s\S]*?">([\s\S]*?)<\/script>/i,
|
|
185
|
+
// Single quotes
|
|
186
|
+
/<script[^>]*id='api-reference[\s\S]*?'>([\s\S]*?)<\/script>/i,
|
|
187
|
+
];
|
|
188
|
+
for (const pattern of patterns) {
|
|
189
|
+
const match = html.match(pattern);
|
|
190
|
+
if (match?.[1]) {
|
|
191
|
+
return match[1].trim();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
142
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Parse OpenAPI document directly from script tag content
|
|
198
|
+
*/
|
|
143
199
|
function parseScriptContent(html) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
200
|
+
const content = getContentOfScriptTag(html);
|
|
201
|
+
try {
|
|
202
|
+
if (content) {
|
|
203
|
+
try {
|
|
204
|
+
// JSON
|
|
205
|
+
return JSON.parse(content);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
try {
|
|
209
|
+
// JSON with escaped whitespace
|
|
210
|
+
const sanitizedContent = content.replace(/\\s/g, '\\\\s');
|
|
211
|
+
return JSON.parse(sanitizedContent);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// YAML
|
|
215
|
+
return parse(content);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
155
218
|
}
|
|
156
|
-
}
|
|
157
219
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
220
|
+
catch (error) {
|
|
221
|
+
console.error('[@scalar/import] Failed to parse script content:', error);
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
162
224
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Get the configuration attribute from the script tag
|
|
227
|
+
*
|
|
228
|
+
* @example <script id="api-reference" data-configuration="{"spec":{"content":"foo"}}"></script>
|
|
229
|
+
*/
|
|
230
|
+
export function getConfigurationAttribute(html) {
|
|
231
|
+
const patterns = [
|
|
232
|
+
// Double quotes
|
|
233
|
+
/<script[^>]*id="api-reference"[^>]*data-configuration=["]([^"]+)["][^>]*>(.*?)<\/script>/s,
|
|
234
|
+
// Single quotes
|
|
235
|
+
/<script[^>]*id='api-reference'[^>]*data-configuration=[']([^']+)['][^>]*>(.*?)<\/script>/s,
|
|
236
|
+
// Mix quote single first
|
|
237
|
+
/<script[^>]*id='api-reference'[^>]*data-configuration=["]([^"]+)["][^>]*>(.*?)<\/script>/s,
|
|
238
|
+
// Mix quote double first
|
|
239
|
+
/<script[^>]*id="api-reference"[^>]*data-configuration=[']([^']+)['][^>]*>(.*?)<\/script>/s,
|
|
240
|
+
];
|
|
241
|
+
for (const pattern of patterns) {
|
|
242
|
+
const match = html.match(pattern);
|
|
243
|
+
if (match?.[1]) {
|
|
244
|
+
return match[1];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return undefined;
|
|
181
248
|
}
|
|
249
|
+
/** Grab the URL from the configuration data attribute */
|
|
182
250
|
const getConfigurationAttributeUrl = (html) => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
251
|
+
const configString = getConfigurationAttribute(html);
|
|
252
|
+
if (!configString) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
const config = JSON.parse(decodeHtmlEntities(configString));
|
|
256
|
+
return config.url || config.spec?.url;
|
|
189
257
|
};
|
|
258
|
+
/**
|
|
259
|
+
* Parse embedded OpenAPI document from HTML
|
|
260
|
+
*/
|
|
190
261
|
function parseEmbeddedOpenApi(html) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
262
|
+
const configString = getConfigurationAttribute(html);
|
|
263
|
+
if (!configString) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const config = JSON.parse(decodeHtmlEntities(configString));
|
|
268
|
+
// Handle the old spec format while we transition to the new one
|
|
269
|
+
const content = config.content || config.spec?.content;
|
|
270
|
+
// Handle both direct JSON content and YAML content
|
|
271
|
+
if (content) {
|
|
272
|
+
// If content is a string, assume it's YAML
|
|
273
|
+
if (typeof content === 'string') {
|
|
274
|
+
return parse(content);
|
|
275
|
+
}
|
|
276
|
+
// If content is an object, return it directly
|
|
277
|
+
return content;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
console.error('[@scalar/import] Failed to parse embedded OpenAPI document:', error);
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
208
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Decode HTML entities in a string
|
|
287
|
+
*/
|
|
209
288
|
function decodeHtmlEntities(text) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
289
|
+
const entities = {
|
|
290
|
+
'&': '&',
|
|
291
|
+
'<': '<',
|
|
292
|
+
'>': '>',
|
|
293
|
+
'"': '"',
|
|
294
|
+
''': "'",
|
|
295
|
+
};
|
|
296
|
+
const updatedText = text
|
|
297
|
+
.replace(new RegExp(Object.keys(entities).join('|'), 'g'), (match) => entities[match])
|
|
298
|
+
.replace(/\n/g, '\\n')
|
|
299
|
+
.trim();
|
|
300
|
+
if (updatedText.startsWith('{"')) {
|
|
301
|
+
// console.log('ok', updatedText)
|
|
302
|
+
return decodeHtmlEntities(updatedText);
|
|
303
|
+
}
|
|
304
|
+
return updatedText;
|
|
222
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Transform GitHub URLs to raw file URLs, preserving the branch information
|
|
308
|
+
*/
|
|
223
309
|
function transformGitHubUrl(url) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
310
|
+
const githubRegex = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/;
|
|
311
|
+
const match = url.match(githubRegex);
|
|
312
|
+
if (match) {
|
|
313
|
+
const [, owner, repo, branch, path] = match;
|
|
314
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/refs/heads/${branch}/${path}`;
|
|
315
|
+
}
|
|
316
|
+
return undefined;
|
|
231
317
|
}
|
|
232
|
-
export {
|
|
233
|
-
getConfigurationAttribute,
|
|
234
|
-
getContentOfScriptTag,
|
|
235
|
-
resolve
|
|
236
|
-
};
|
|
237
|
-
//# sourceMappingURL=resolve.js.map
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"postman",
|
|
17
17
|
"scalar"
|
|
18
18
|
],
|
|
19
|
-
"version": "0.5.
|
|
19
|
+
"version": "0.5.3",
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=22"
|
|
22
22
|
},
|
|
@@ -36,19 +36,13 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"yaml": "^2.8.0",
|
|
39
|
-
"@scalar/helpers": "0.4.
|
|
40
|
-
},
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"vite": "^7.3.1",
|
|
43
|
-
"@scalar/build-tooling": "0.5.0"
|
|
39
|
+
"@scalar/helpers": "0.4.2"
|
|
44
40
|
},
|
|
41
|
+
"devDependencies": {},
|
|
45
42
|
"scripts": {
|
|
46
|
-
"build": "
|
|
47
|
-
"dev": "
|
|
48
|
-
"lint:check": "eslint .",
|
|
49
|
-
"lint:fix": "eslint . --fix",
|
|
43
|
+
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
44
|
+
"dev": "tsx watch playground/index.ts",
|
|
50
45
|
"test": "vitest",
|
|
51
|
-
"types:
|
|
52
|
-
"types:check": "scalar-types-check"
|
|
46
|
+
"types:check": "tsc --noEmit"
|
|
53
47
|
}
|
|
54
48
|
}
|
package/dist/index.js.map
DELETED
package/dist/resolve.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/resolve.ts"],
|
|
4
|
-
"sourcesContent": ["import { makeUrlAbsolute } from '@scalar/helpers/url/make-url-absolute'\nimport { parse } from 'yaml'\n\n/**\n * Find an OpenAPI document URL in the HTML of @scalar/api-reference and other places.\n * This is useful to open the OpenAPI document from basically any source.\n */\nexport async function resolve(\n value?: string | null,\n options?: {\n /**\n * Fetch function to use instead of the global fetch. Use this to intercept requests.\n */\n fetch?: (url: string) => Promise<Response>\n },\n): Promise<string | Record<string, any> | undefined> {\n // URLs\n if (value?.startsWith('http://') || value?.startsWith('https://')) {\n // Transform GitHub URLs to raw file URLs\n const githubRawUrl = transformGitHubUrl(value)\n\n if (githubRawUrl) {\n return githubRawUrl\n }\n\n // https://*.json\n if (value?.toLowerCase().endsWith('.json')) {\n return value\n }\n\n // https://*.yaml\n if (value?.toLowerCase().endsWith('.yaml') || value?.toLowerCase().endsWith('.yml')) {\n return value\n }\n\n // https://sandbox.scalar.com\n const sandboxUrl = value.match(/https:\\/\\/sandbox\\.scalar\\.com\\/(p|e)\\/([a-zA-Z0-9]+)/)\n\n if (sandboxUrl?.[2]) {\n return `https://sandbox.scalar.com/files/${sandboxUrl[2]}/openapi.yaml`\n }\n\n // Fetch URL\n try {\n let result = await (options?.fetch\n ? options.fetch(value)\n : fetch(value, {\n cache: 'no-cache',\n }))\n\n // If the custom fetch failed, try again with regular fetch\n if (!result.ok && options?.fetch) {\n result = await fetch(value, {\n cache: 'no-cache',\n })\n }\n\n if (result.ok) {\n const content = await result.text()\n const forwardedHost = result.headers.get('X-Forwarded-Host')\n\n // Check if content is directly JSON/YAML\n try {\n // Try parsing as JSON\n const jsonContent = JSON.parse(content)\n if (jsonContent.openapi || jsonContent.swagger) {\n return value\n }\n } catch {\n // Try parsing as YAML\n try {\n const yamlContent = parse(content)\n if (yamlContent?.openapi || yamlContent?.swagger) {\n return value\n }\n } catch {\n // Not YAML\n }\n }\n\n const urlOrPathOrDocument = parseHtml(content)\n\n // Document (string)\n if (typeof urlOrPathOrDocument === 'string') {\n try {\n // JSON?\n return JSON.parse(urlOrPathOrDocument)\n } catch {\n // No JSON\n try {\n // YAML?\n const yaml = parse(urlOrPathOrDocument)\n\n if (typeof yaml === 'object') {\n return yaml\n }\n } catch {\n // Not YAML\n }\n }\n }\n\n // Document (object)\n if (typeof urlOrPathOrDocument === 'object') {\n return urlOrPathOrDocument\n }\n\n // Relative or absolute URL\n if (urlOrPathOrDocument) {\n return makeUrlAbsolute(urlOrPathOrDocument, {\n baseUrl: forwardedHost || value,\n })\n }\n\n // Check for configuration attribute URL\n const configUrl = getConfigurationAttributeUrl(content)\n if (configUrl) {\n return makeUrlAbsolute(configUrl, {\n baseUrl: forwardedHost || value,\n })\n }\n\n // Check for embedded OpenAPI document\n const embeddedSpec = parseEmbeddedOpenApi(content)\n if (embeddedSpec) {\n return embeddedSpec\n }\n } else {\n console.warn(`[@scalar/import] Failed to fetch ${value}`)\n }\n } catch (error) {\n console.error(`[@scalar/import] Failed to fetch ${value}`, error)\n }\n }\n\n return undefined\n}\n\n/**\n * Go through the HTML and try to find the OpenAPI document URL\n */\nfunction parseHtml(html?: string) {\n // Check whether it could be HTML\n if (!html?.includes('<')) {\n return undefined\n }\n\n // id=\"api-reference\" data-url=\"*\"\n const dataUrlMatch = html\n .match(/id=[\"']api-reference[\"'][\\s\\S]*?data-url=[\"']([^\"']+)[\"']/)\n ?.slice(1)\n .find(Boolean)\n\n if (dataUrlMatch) {\n return dataUrlMatch\n }\n\n // spec-url=\"*\"\n const specUrlMatch = html.match(/spec-url=[\"']([^\"']+)[\"']/)\n if (specUrlMatch?.[1]) {\n return specUrlMatch[1]\n }\n\n // Redoc.init('*')\n const redocInit = html.match(/Redoc.init\\([\"']([^\"']+)[\"']/)\n\n if (redocInit?.[1]) {\n return redocInit[1]\n }\n\n // &quot;url&quot;:&quot;MY_CUSTOM_URL&quot;\n const doubleEncodedConfigurationUrl = html.match(/&quot;url&quot;:&quot;([^;]+)&quot;/)\n\n if (doubleEncodedConfigurationUrl?.[1]) {\n return doubleEncodedConfigurationUrl[1]\n }\n\n // &quot;url&quot;:&quot;MY_CUSTOM_URL&quot;\n const encodedConfigurationUrl = html.match(/"url":"([^;]+)"/)\n if (encodedConfigurationUrl?.[1]) {\n return encodedConfigurationUrl[1]\n }\n\n // Try to find embedded OpenAPI document in script tag first\n const scriptContent = parseScriptContent(html)\n if (scriptContent) {\n return scriptContent\n }\n\n // Check for configuration in script tag\n const scriptConfigMatch = html.match(/url:\\s*[\"']([^\"']+)[\"']/i)\n\n if (scriptConfigMatch?.[1]) {\n return scriptConfigMatch?.[1]\n }\n\n // Check for OpenAPI URLs in the HTML\n const linkMatch = html.match(/<a[^>]*href=[\"']([^\"']+\\.(?:yaml|yml|json))[\"'][^>]*>/i)\n\n if (linkMatch?.[1]) {\n return linkMatch[1]\n }\n\n // Check for URLs in escaped JS objects\n const escapedJsonMatch = html.match(/\\\\\"spec\\\\\":\\{.*?\\\\\"url\\\\\":\\\\\"([^\"\\\\]+)\\\\\"/)\n\n if (escapedJsonMatch?.[1]) {\n return escapedJsonMatch[1]\n }\n\n return undefined\n}\n\n/**\n * Get the content between the script tags\n *\n * @example <script id=\"api-reference\">console.log(\"Hello, world!\");</script>\n */\nexport function getContentOfScriptTag(html: string): string | undefined {\n const patterns = [\n // Double quotes\n /<script[^>]*id=\"api-reference[\\s\\S]*?\">([\\s\\S]*?)<\\/script>/i,\n // Single quotes\n /<script[^>]*id='api-reference[\\s\\S]*?'>([\\s\\S]*?)<\\/script>/i,\n ]\n\n for (const pattern of patterns) {\n const match = html.match(pattern)\n\n if (match?.[1]) {\n return match[1].trim()\n }\n }\n\n return undefined\n}\n\n/**\n * Parse OpenAPI document directly from script tag content\n */\nfunction parseScriptContent(html: string): Record<string, any> | undefined {\n const content = getContentOfScriptTag(html)\n\n try {\n if (content) {\n try {\n // JSON\n return JSON.parse(content)\n } catch {\n try {\n // JSON with escaped whitespace\n const sanitizedContent = content.replace(/\\\\s/g, '\\\\\\\\s')\n\n return JSON.parse(sanitizedContent)\n } catch {\n // YAML\n return parse(content)\n }\n }\n }\n } catch (error) {\n console.error('[@scalar/import] Failed to parse script content:', error)\n }\n\n return undefined\n}\n\n/**\n * Get the configuration attribute from the script tag\n *\n * @example <script id=\"api-reference\" data-configuration=\"{"spec":{"content":"foo"}}\"></script>\n */\nexport function getConfigurationAttribute(html: string): string | undefined {\n const patterns = [\n // Double quotes\n /<script[^>]*id=\"api-reference\"[^>]*data-configuration=[\"]([^\"]+)[\"][^>]*>(.*?)<\\/script>/s,\n // Single quotes\n /<script[^>]*id='api-reference'[^>]*data-configuration=[']([^']+)['][^>]*>(.*?)<\\/script>/s,\n // Mix quote single first\n /<script[^>]*id='api-reference'[^>]*data-configuration=[\"]([^\"]+)[\"][^>]*>(.*?)<\\/script>/s,\n // Mix quote double first\n /<script[^>]*id=\"api-reference\"[^>]*data-configuration=[']([^']+)['][^>]*>(.*?)<\\/script>/s,\n ]\n\n for (const pattern of patterns) {\n const match = html.match(pattern)\n\n if (match?.[1]) {\n return match[1]\n }\n }\n\n return undefined\n}\n\n/** Grab the URL from the configuration data attribute */\nconst getConfigurationAttributeUrl = (html: string): string | undefined => {\n const configString = getConfigurationAttribute(html)\n if (!configString) {\n return undefined\n }\n\n const config = JSON.parse(decodeHtmlEntities(configString))\n return config.url || config.spec?.url\n}\n\n/**\n * Parse embedded OpenAPI document from HTML\n */\nfunction parseEmbeddedOpenApi(html: string): object | undefined {\n const configString = getConfigurationAttribute(html)\n if (!configString) {\n return undefined\n }\n\n try {\n const config = JSON.parse(decodeHtmlEntities(configString))\n\n // Handle the old spec format while we transition to the new one\n const content = config.content || config.spec?.content\n\n // Handle both direct JSON content and YAML content\n if (content) {\n // If content is a string, assume it's YAML\n if (typeof content === 'string') {\n return parse(content)\n }\n // If content is an object, return it directly\n return content\n }\n } catch (error) {\n console.error('[@scalar/import] Failed to parse embedded OpenAPI document:', error)\n }\n\n return undefined\n}\n\n/**\n * Decode HTML entities in a string\n */\nfunction decodeHtmlEntities(text: string): string {\n const entities = {\n '&': '&',\n '<': '<',\n '>': '>',\n '"': '\"',\n ''': \"'\",\n } as const\n\n const updatedText = text\n .replace(new RegExp(Object.keys(entities).join('|'), 'g'), (match) => entities[match as keyof typeof entities])\n .replace(/\\n/g, '\\\\n')\n .trim()\n\n if (updatedText.startsWith('{"')) {\n // console.log('ok', updatedText)\n return decodeHtmlEntities(updatedText)\n }\n\n return updatedText\n}\n\n/**\n * Transform GitHub URLs to raw file URLs, preserving the branch information\n */\nfunction transformGitHubUrl(url: string): string | undefined {\n const githubRegex = /^https:\\/\\/github\\.com\\/([^/]+)\\/([^/]+)\\/blob\\/([^/]+)\\/(.+)$/\n const match = url.match(githubRegex)\n\n if (match) {\n const [, owner, repo, branch, path] = match\n return `https://raw.githubusercontent.com/${owner}/${repo}/refs/heads/${branch}/${path}`\n }\n\n return undefined\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|