@mintlify/prebuild 1.0.1189 → 1.0.1191

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.
@@ -0,0 +1,304 @@
1
+ import { generateSdkReference } from '@mintlify/scraping';
2
+ import { Unzip, UnzipInflate } from 'fflate';
3
+ import fse from 'fs-extra';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { getGeneratedRouteKey } from './generatedRouteCollisions.js';
7
+ import { loadRemoteFile } from './loadRemoteGraphqlSdl.js';
8
+ const DEFAULT_OUTPUT_DIR = 'sdk-reference';
9
+ const REMOTE_ARTIFACT_LABEL = 'SDK artifact';
10
+ const MAX_REMOTE_SDK_ARTIFACT_BYTES = 50 * 1024 * 1024;
11
+ const MAX_EXTRACTED_SDK_ARTIFACT_BYTES = 200 * 1024 * 1024;
12
+ const REMOTE_SDK_ARTIFACT_TIMEOUT_MS = 30_000;
13
+ export async function generateSdkDivisionsFromDocsConfig(docsConfig, loadReference) {
14
+ const references = new Map();
15
+ const pagesByRouteKey = new Map();
16
+ function load(config) {
17
+ const key = `${config.format}:${config.source}`;
18
+ let pending = references.get(key);
19
+ if (!pending) {
20
+ pending = loadReference(config);
21
+ references.set(key, pending);
22
+ }
23
+ return pending;
24
+ }
25
+ async function processNode(value) {
26
+ if (Array.isArray(value))
27
+ return Promise.all(value.map(processNode));
28
+ if (typeof value !== 'object' || value === null)
29
+ return value;
30
+ const node = value;
31
+ if ('tab' in node && 'sdk' in node) {
32
+ const { sdk, ...tab } = node;
33
+ const config = sdk;
34
+ const directory = config.directory ?? DEFAULT_OUTPUT_DIR;
35
+ const reference = await load(config);
36
+ const pageBySlug = new Map(reference.pages.map((page) => [page.slug, page]));
37
+ const referenceSlugs = new Set(reference.pages.map((page) => page.slug));
38
+ const groups = reference.groups.map((group) => ({
39
+ group: group.group,
40
+ pages: group.pages.map((slug) => path.posix.join(directory, slug)),
41
+ }));
42
+ for (const page of reference.pages) {
43
+ assertSafeSlug(page.slug, config);
44
+ const slug = path.posix.join(directory, page.slug);
45
+ const generatedPage = {
46
+ slug,
47
+ title: page.title,
48
+ ...(page.description ? { description: page.description } : {}),
49
+ ...(page.tag ? { tag: page.tag } : {}),
50
+ content: prefixInternalLinks(stripLeadingDescription(page.content, page.description), directory, referenceSlugs),
51
+ format: config.format,
52
+ source: config.source,
53
+ };
54
+ const routeKey = getGeneratedRouteKey(slug);
55
+ const existingPage = pagesByRouteKey.get(routeKey);
56
+ if (existingPage &&
57
+ (existingPage.slug !== slug ||
58
+ existingPage.format !== generatedPage.format ||
59
+ existingPage.source !== generatedPage.source)) {
60
+ throw new Error(`Multiple SDK references generate colliding routes "${existingPage.slug}" and "${slug}"`);
61
+ }
62
+ pagesByRouteKey.set(routeKey, generatedPage);
63
+ }
64
+ const orphanedSlugs = reference.pages
65
+ .map((page) => page.slug)
66
+ .filter((slug) => !reference.groups.some((group) => group.pages.includes(slug)));
67
+ if (orphanedSlugs.length > 0 && pageBySlug.size > 0) {
68
+ groups.push({
69
+ group: 'Reference',
70
+ pages: orphanedSlugs.map((slug) => path.posix.join(directory, slug)),
71
+ });
72
+ }
73
+ return { ...tab, groups: [...(tab.groups ?? []), ...groups] };
74
+ }
75
+ return Object.fromEntries(await Promise.all(Object.entries(node).map(async ([key, entry]) => [key, await processNode(entry)])));
76
+ }
77
+ const navigation = (await processNode(docsConfig.navigation));
78
+ const generatedPages = [...pagesByRouteKey.values()].sort((left, right) => left.slug.localeCompare(right.slug));
79
+ const pagesAcc = Object.fromEntries(generatedPages.map((page) => [
80
+ page.slug,
81
+ {
82
+ href: `/${page.slug}`,
83
+ title: page.title,
84
+ ...(page.description ? { description: page.description } : {}),
85
+ ...(page.tag ? { tag: page.tag } : {}),
86
+ },
87
+ ]));
88
+ return {
89
+ newDocsConfig: { ...docsConfig, navigation },
90
+ pagesAcc,
91
+ generatedPages,
92
+ generatedRoutes: generatedPages.map(({ slug }) => slug),
93
+ };
94
+ }
95
+ export const isRemoteSdkSource = (source) => {
96
+ try {
97
+ return new URL(source).protocol === 'https:';
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ };
103
+ function assertSafeArchiveEntry(name, source) {
104
+ const segments = name.split('/');
105
+ if (name.includes('\\') ||
106
+ path.posix.isAbsolute(name) ||
107
+ path.win32.isAbsolute(name) ||
108
+ segments.some((segment) => segment === '.' || segment === '..')) {
109
+ throw new Error(`SDK artifact ${source} contains an unsafe archive entry "${name}"`);
110
+ }
111
+ }
112
+ function unzipWithSizeCap(bytes, source) {
113
+ const entries = new Map();
114
+ let extractedBytes = 0;
115
+ let failure;
116
+ const overflow = () => new Error(`SDK artifact ${source} exceeds the ${MAX_EXTRACTED_SDK_ARTIFACT_BYTES} byte extracted size limit`);
117
+ const unzip = new Unzip((file) => {
118
+ try {
119
+ assertSafeArchiveEntry(file.name, source);
120
+ }
121
+ catch (error) {
122
+ failure ??= error instanceof Error ? error : new Error(String(error));
123
+ return;
124
+ }
125
+ if (file.name.endsWith('/'))
126
+ return;
127
+ const chunks = [];
128
+ file.ondata = (error, data, final) => {
129
+ if (failure)
130
+ return;
131
+ if (error) {
132
+ failure = error;
133
+ return;
134
+ }
135
+ extractedBytes += data.byteLength;
136
+ if (extractedBytes > MAX_EXTRACTED_SDK_ARTIFACT_BYTES) {
137
+ failure = overflow();
138
+ file.terminate();
139
+ return;
140
+ }
141
+ chunks.push(data);
142
+ if (final) {
143
+ const merged = new Uint8Array(chunks.reduce((size, chunk) => size + chunk.byteLength, 0));
144
+ let offset = 0;
145
+ for (const chunk of chunks) {
146
+ merged.set(chunk, offset);
147
+ offset += chunk.byteLength;
148
+ }
149
+ entries.set(file.name, merged);
150
+ }
151
+ };
152
+ file.start();
153
+ });
154
+ unzip.register(UnzipInflate);
155
+ unzip.push(bytes, true);
156
+ if (failure)
157
+ throw failure;
158
+ return entries;
159
+ }
160
+ async function extractRemoteArtifact(buffer, source, tempDir) {
161
+ const bytes = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
162
+ if (bytes[0] !== 0x50 || bytes[1] !== 0x4b) {
163
+ const basename = path.posix.basename(new URL(source).pathname) || 'artifact';
164
+ const filePath = path.join(tempDir, basename.replace(/[^\w.-]+/g, '-'));
165
+ await fse.writeFile(filePath, buffer);
166
+ return filePath;
167
+ }
168
+ const entries = unzipWithSizeCap(bytes, source);
169
+ for (const [name, data] of entries) {
170
+ await fse.outputFile(path.join(tempDir, name), Buffer.from(data));
171
+ }
172
+ let root = tempDir;
173
+ for (;;) {
174
+ const rootEntries = await fse.readdir(root, { withFileTypes: true });
175
+ if (rootEntries.length !== 1 || !rootEntries[0])
176
+ return root;
177
+ const only = path.join(root, rootEntries[0].name);
178
+ if (rootEntries[0].isFile())
179
+ return only;
180
+ root = only;
181
+ }
182
+ }
183
+ async function loadRemoteSdkReference(config, remoteOptions) {
184
+ const buffer = await loadRemoteFile(config.source, {
185
+ label: REMOTE_ARTIFACT_LABEL,
186
+ maxBytes: MAX_REMOTE_SDK_ARTIFACT_BYTES,
187
+ timeoutMs: REMOTE_SDK_ARTIFACT_TIMEOUT_MS,
188
+ ...remoteOptions,
189
+ });
190
+ const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'mintlify-sdk-'));
191
+ try {
192
+ const artifactPath = await extractRemoteArtifact(buffer, config.source, tempDir);
193
+ return await generateSdkReference(config.format, artifactPath);
194
+ }
195
+ finally {
196
+ await fse.remove(tempDir);
197
+ }
198
+ }
199
+ export async function loadSdkReferenceSource(config, contentDirectoryPath, remoteOptions) {
200
+ const { format, source } = config;
201
+ if (isRemoteSdkSource(source)) {
202
+ return loadRemoteSdkReference(config, remoteOptions);
203
+ }
204
+ if (path.isAbsolute(source) || path.win32.isAbsolute(source)) {
205
+ throw new Error(`Unable to load SDK reference ${source}: path must be relative`);
206
+ }
207
+ const contentRoot = await fse.realpath(contentDirectoryPath);
208
+ const candidatePath = path.resolve(contentRoot, source);
209
+ const assertContained = (target, label) => {
210
+ const relativePath = path.relative(contentRoot, target);
211
+ if (relativePath === '..' ||
212
+ relativePath.startsWith(`..${path.sep}`) ||
213
+ path.isAbsolute(relativePath)) {
214
+ throw new Error(`Unable to load SDK reference ${source}: ${label}`);
215
+ }
216
+ };
217
+ assertContained(candidatePath, 'path escapes the content directory');
218
+ const realSourcePath = await fse.realpath(candidatePath);
219
+ assertContained(realSourcePath, 'path resolves outside the content directory');
220
+ return generateSdkReference(format, realSourcePath);
221
+ }
222
+ function prefixInternalLinks(content, directory, slugs) {
223
+ return content.replace(/\]\(\/([^)#\s]+)((?:#[^)\s]*)?)\)/g, (match, target, fragment) => slugs.has(target) ? `](/${path.posix.join(directory, target)}${fragment})` : match);
224
+ }
225
+ function assertSafeSlug(slug, config) {
226
+ const segments = slug.split('/');
227
+ if (!slug ||
228
+ slug.includes('\\') ||
229
+ path.posix.isAbsolute(slug) ||
230
+ segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
231
+ throw new Error(`SDK reference ${config.format}:${config.source} produced an unsafe page slug "${slug}"`);
232
+ }
233
+ }
234
+ function normalizeProse(text) {
235
+ return text
236
+ .replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
237
+ .replace(/[`*_\\]/g, '')
238
+ .replace(/\s+/g, ' ')
239
+ .trim();
240
+ }
241
+ function stripLeadingDescription(content, description) {
242
+ if (!description)
243
+ return content;
244
+ const [firstChunk = '', ...rest] = content.split('\n\n');
245
+ if (/^[#<>`-]/.test(firstChunk.trim()))
246
+ return content;
247
+ const matches = normalizeProse(firstChunk) === normalizeProse(description);
248
+ return matches ? rest.join('\n\n').trim() : content;
249
+ }
250
+ function frontmatter(page) {
251
+ const fields = [
252
+ `title: ${JSON.stringify(page.title)}`,
253
+ ...(page.description ? [`description: ${JSON.stringify(page.description)}`] : []),
254
+ ...(page.tag ? [`tag: ${JSON.stringify(page.tag)}`] : []),
255
+ ];
256
+ return `---\n${fields.join('\n')}\n---\n\n`;
257
+ }
258
+ function getContainedSdkPagePath(targetDir, page) {
259
+ const propsDirectory = path.resolve(targetDir ?? '', 'src', '_props');
260
+ const pagePath = path.resolve(propsDirectory, `${page}.mdx`);
261
+ const relativePath = path.relative(propsDirectory, pagePath);
262
+ if (relativePath.startsWith(`..${path.sep}`) ||
263
+ relativePath === '..' ||
264
+ path.isAbsolute(relativePath)) {
265
+ return undefined;
266
+ }
267
+ return pagePath;
268
+ }
269
+ export async function writeSdkArtifacts(result, targetDir) {
270
+ const propsDirectory = path.resolve(targetDir ?? '', 'src', '_props');
271
+ const manifestPath = path.join(propsDirectory, 'sdk-data.json');
272
+ let previousPages = [];
273
+ try {
274
+ const previousManifest = JSON.parse(await fse.readFile(manifestPath, 'utf8'));
275
+ if (Array.isArray(previousManifest.pages)) {
276
+ previousPages = previousManifest.pages.filter((page) => typeof page === 'string');
277
+ }
278
+ }
279
+ catch { }
280
+ await Promise.all(previousPages.map(async (page) => {
281
+ const pagePath = getContainedSdkPagePath(targetDir, page);
282
+ if (!pagePath)
283
+ return;
284
+ try {
285
+ await fse.remove(pagePath);
286
+ }
287
+ catch (error) {
288
+ console.warn(`Failed to remove stale SDK reference page ${page}: ${error instanceof Error ? error.message : String(error)}`);
289
+ }
290
+ }));
291
+ await fse.remove(manifestPath);
292
+ if (result.generatedPages.length === 0)
293
+ return;
294
+ await fse.outputFile(manifestPath, JSON.stringify({ pages: result.generatedRoutes }), {
295
+ flag: 'w',
296
+ });
297
+ await Promise.all(result.generatedPages.map((page) => {
298
+ const pagePath = getContainedSdkPagePath(targetDir, page.slug);
299
+ if (!pagePath) {
300
+ throw new Error(`SDK reference page "${page.slug}" resolves outside the output directory`);
301
+ }
302
+ return fse.outputFile(pagePath, `${frontmatter(page)}${page.content}\n`, { flag: 'w' });
303
+ }));
304
+ }
@@ -24,6 +24,8 @@ export declare function updateDocsConfigFile({ contentDirectoryPath, openApiFile
24
24
  }>;
25
25
  export { generateOpenApiDivisions } from './generateOpenApiDivisions.js';
26
26
  export { generateOpenApiFromDocsConfig } from './generateOpenApiFromDocsConfig.js';
27
+ export { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, writeSdkArtifacts, } from './generateSdkDivisions.js';
28
+ export type { GeneratedSdkPage, GenerateSdkDivisionsResult, SdkReferenceLoader, } from './generateSdkDivisions.js';
27
29
  export { getOpenApiFilesFromConfig } from '../read/getOpenApiFilesFromConfig.js';
28
30
  export { generateAsyncApiDivisions } from './generateAsyncApiDivisions.js';
29
31
  export { generateAsyncApiFromDocsConfig } from './generateAsyncApiFromDocsConfig.js';
@@ -1,8 +1,10 @@
1
1
  import { getConfigPath } from '../../../utils.js';
2
2
  import { DocsConfigUpdater } from '../ConfigUpdater.js';
3
3
  import { generateAsyncApiDivisions } from './generateAsyncApiDivisions.js';
4
+ import { assertNoGeneratedRouteCollisions } from './generatedRouteCollisions.js';
4
5
  import { generateGraphqlDivisionsFromDocsConfig, loadGraphqlSdlSource, writeGraphqlArtifacts, } from './generateGraphqlDivisions.js';
5
6
  import { generateOpenApiDivisions } from './generateOpenApiDivisions.js';
7
+ import { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, writeSdkArtifacts, } from './generateSdkDivisions.js';
6
8
  import { getCustomLanguages } from './getCustomLanguages.js';
7
9
  const NOT_CORRECT_PATH_ERROR = 'must be run in a directory where a docs.json file exists.';
8
10
  export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles, asyncApiFiles, docsConfig, localSchema, disableOpenApi, strict, invalidSpecFiles, allowSourceRefs, }) {
@@ -30,16 +32,24 @@ export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles,
30
32
  flushWrites: async () => { },
31
33
  };
32
34
  const { newDocsConfig: docsConfigWithAsyncApiPages, pagesAcc: pagesAccWithAsyncApiPages, asyncApiFiles: newAsyncApiFiles, flushWrites: flushAsyncApiWrites, } = await generateAsyncApiDivisions(docsConfigWithOpenApiPages, asyncApiFiles, undefined, localSchema, pagesAccWithGraphqlPages, true, true);
35
+ const sdkResult = await generateSdkDivisionsFromDocsConfig(docsConfigWithAsyncApiPages, (config) => loadSdkReferenceSource(config, contentDirectoryPath));
36
+ const { newDocsConfig: docsConfigWithSdkPages, pagesAcc: pagesAccWithSdkPages } = sdkResult;
37
+ const sdkRoutes = { label: 'SDK', pages: pagesAccWithSdkPages };
38
+ assertNoGeneratedRouteCollisions({ label: 'GraphQL', pages: pagesAccWithGraphqlPages }, sdkRoutes);
39
+ assertNoGeneratedRouteCollisions({ label: 'OpenAPI', pages: pagesAccWithOpenApiPages }, sdkRoutes);
40
+ assertNoGeneratedRouteCollisions({ label: 'AsyncAPI', pages: pagesAccWithAsyncApiPages }, sdkRoutes);
33
41
  const pagesAcc = {
34
42
  ...pagesAccWithOpenApiPages,
35
43
  ...pagesAccWithAsyncApiPages,
36
44
  ...pagesAccWithGraphqlPages,
45
+ ...pagesAccWithSdkPages,
37
46
  };
38
47
  await Promise.all([flushOpenApiWrites(), flushAsyncApiWrites()]);
39
48
  await writeGraphqlArtifacts(graphqlResult);
40
- await DocsConfigUpdater.writeConfigFile(docsConfigWithAsyncApiPages);
49
+ await writeSdkArtifacts(sdkResult);
50
+ await DocsConfigUpdater.writeConfigFile(docsConfigWithSdkPages);
41
51
  return {
42
- docsConfig: docsConfigWithAsyncApiPages,
52
+ docsConfig: docsConfigWithSdkPages,
43
53
  pagesAcc,
44
54
  newOpenApiFiles,
45
55
  newAsyncApiFiles,
@@ -48,6 +58,7 @@ export async function updateDocsConfigFile({ contentDirectoryPath, openApiFiles,
48
58
  }
49
59
  export { generateOpenApiDivisions } from './generateOpenApiDivisions.js';
50
60
  export { generateOpenApiFromDocsConfig } from './generateOpenApiFromDocsConfig.js';
61
+ export { generateSdkDivisionsFromDocsConfig, loadSdkReferenceSource, writeSdkArtifacts, } from './generateSdkDivisions.js';
51
62
  export { getOpenApiFilesFromConfig } from '../read/getOpenApiFilesFromConfig.js';
52
63
  export { generateAsyncApiDivisions } from './generateAsyncApiDivisions.js';
53
64
  export { generateAsyncApiFromDocsConfig } from './generateAsyncApiFromDocsConfig.js';
@@ -9,10 +9,13 @@ type ResolveHostname = (hostname: string) => Promise<ResolvedAddress[]>;
9
9
  type HttpsRequest = typeof request;
10
10
  export declare const isGlobalIpAddress: (address: string) => boolean;
11
11
  export declare function createPinnedLookup(resolved: ResolvedAddress): LookupFunction;
12
- export declare function loadRemoteGraphqlSdl(source: string, options?: {
12
+ export type LoadRemoteFileOptions = {
13
13
  timeoutMs?: number;
14
14
  maxBytes?: number;
15
15
  resolveHostname?: ResolveHostname;
16
16
  requestImpl?: HttpsRequest;
17
- }): Promise<string>;
17
+ label?: string;
18
+ };
19
+ export declare function loadRemoteGraphqlSdl(source: string, options?: LoadRemoteFileOptions): Promise<string>;
20
+ export declare function loadRemoteFile(source: string, options?: LoadRemoteFileOptions): Promise<Buffer>;
18
21
  export {};
@@ -4,6 +4,7 @@ import { BlockList, isIP } from 'node:net';
4
4
  const MAX_REMOTE_REDIRECTS = 5;
5
5
  export const MAX_GRAPHQL_SCHEMA_BYTES = 5 * 1024 * 1024;
6
6
  const REMOTE_SCHEMA_TIMEOUT_MS = 10_000;
7
+ const DEFAULT_LABEL = 'GraphQL schema';
7
8
  const blockedAddresses = new BlockList();
8
9
  for (const [network, prefix] of [
9
10
  ['0.0.0.0', 8],
@@ -66,17 +67,25 @@ async function defaultResolveHostname(hostname) {
66
67
  return (await lookup(hostname, { all: true }));
67
68
  }
68
69
  export function createPinnedLookup(resolved) {
69
- return ((_hostname, _options, callback) => {
70
- callback(null, resolved.address, resolved.family);
70
+ return ((_hostname, options, callback) => {
71
+ const withAll = typeof options === 'object' && options?.all;
72
+ if (withAll) {
73
+ callback(null, [
74
+ resolved,
75
+ ]);
76
+ }
77
+ else {
78
+ callback(null, resolved.address, resolved.family);
79
+ }
71
80
  });
72
81
  }
73
- async function resolveAndValidate(url, resolveHostname) {
82
+ async function resolveAndValidate(url, resolveHostname, label) {
74
83
  if (url.protocol !== 'https:') {
75
- throw new Error(`GraphQL schema redirects must use HTTPS: ${url.href}`);
84
+ throw new Error(`${label} redirects must use HTTPS: ${url.href}`);
76
85
  }
77
86
  const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
78
87
  if (hostname === 'localhost' || hostname.endsWith('.localhost')) {
79
- throw new Error(`GraphQL schema host must be public: ${hostname}`);
88
+ throw new Error(`${label} host must be public: ${hostname}`);
80
89
  }
81
90
  let addresses;
82
91
  try {
@@ -84,15 +93,15 @@ async function resolveAndValidate(url, resolveHostname) {
84
93
  }
85
94
  catch (error) {
86
95
  const detail = error instanceof Error ? `: ${error.message}` : '';
87
- throw new Error(`Unable to resolve GraphQL schema host ${hostname}${detail}`);
96
+ throw new Error(`Unable to resolve ${label} host ${hostname}${detail}`);
88
97
  }
89
98
  // Reject ambiguous split-horizon answers so every address associated with the host is safe.
90
99
  if (addresses.length === 0 || addresses.some(({ address }) => !isGlobalIpAddress(address))) {
91
- throw new Error(`GraphQL schema host must resolve only to public addresses: ${hostname}`);
100
+ throw new Error(`${label} host must resolve only to public addresses: ${hostname}`);
92
101
  }
93
102
  return addresses[0];
94
103
  }
95
- function requestPinned(url, resolved, requestImpl, timeoutMs, maxBytes) {
104
+ function requestPinned(url, resolved, requestImpl, timeoutMs, maxBytes, label) {
96
105
  return new Promise((resolve, reject) => {
97
106
  let responseRef;
98
107
  let requestRef;
@@ -125,7 +134,7 @@ function requestPinned(url, resolved, requestImpl, timeoutMs, maxBytes) {
125
134
  const contentLength = Number(response.headers['content-length']);
126
135
  if (Number.isFinite(contentLength) && contentLength > maxBytes) {
127
136
  response.destroy();
128
- finish(reject, new Error(`GraphQL schema ${url.href} exceeds the ${maxBytes} byte limit`));
137
+ finish(reject, new Error(`${label} ${url.href} exceeds the ${maxBytes} byte limit`));
129
138
  return;
130
139
  }
131
140
  response.on('data', (chunk) => {
@@ -133,7 +142,7 @@ function requestPinned(url, resolved, requestImpl, timeoutMs, maxBytes) {
133
142
  totalBytes += bytes.byteLength;
134
143
  if (totalBytes > maxBytes) {
135
144
  response.destroy();
136
- finish(reject, new Error(`GraphQL schema ${url.href} exceeds the ${maxBytes} byte limit`));
145
+ finish(reject, new Error(`${label} ${url.href} exceeds the ${maxBytes} byte limit`));
137
146
  return;
138
147
  }
139
148
  chunks.push(new Uint8Array(bytes));
@@ -143,7 +152,7 @@ function requestPinned(url, resolved, requestImpl, timeoutMs, maxBytes) {
143
152
  statusCode: response.statusCode ?? 0,
144
153
  statusMessage: response.statusMessage ?? '',
145
154
  headers: response.headers,
146
- body: Buffer.concat(chunks).toString('utf8'),
155
+ body: Buffer.concat(chunks),
147
156
  });
148
157
  });
149
158
  response.on('error', (error) => finish(reject, error));
@@ -158,10 +167,14 @@ function requestPinned(url, resolved, requestImpl, timeoutMs, maxBytes) {
158
167
  });
159
168
  }
160
169
  export async function loadRemoteGraphqlSdl(source, options = {}) {
170
+ return (await loadRemoteFile(source, options)).toString('utf8');
171
+ }
172
+ export async function loadRemoteFile(source, options = {}) {
161
173
  const timeoutMs = options.timeoutMs ?? REMOTE_SCHEMA_TIMEOUT_MS;
162
174
  const maxBytes = options.maxBytes ?? MAX_GRAPHQL_SCHEMA_BYTES;
163
175
  const resolveHostname = options.resolveHostname ?? defaultResolveHostname;
164
176
  const requestImpl = options.requestImpl ?? request;
177
+ const label = options.label ?? DEFAULT_LABEL;
165
178
  const deadline = Date.now() + timeoutMs;
166
179
  let currentUrl = new URL(source);
167
180
  const withTimeout = async (pending, remainingMs) => {
@@ -182,36 +195,36 @@ export async function loadRemoteGraphqlSdl(source, options = {}) {
182
195
  for (let redirects = 0; redirects <= MAX_REMOTE_REDIRECTS; redirects++) {
183
196
  let remainingMs = deadline - Date.now();
184
197
  if (remainingMs <= 0)
185
- throw new Error(`Timed out loading GraphQL schema ${source}`);
198
+ throw new Error(`Timed out loading ${label} ${source}`);
186
199
  let response;
187
200
  try {
188
- const resolved = await withTimeout(resolveAndValidate(currentUrl, resolveHostname), remainingMs);
201
+ const resolved = await withTimeout(resolveAndValidate(currentUrl, resolveHostname, label), remainingMs);
189
202
  remainingMs = deadline - Date.now();
190
203
  if (remainingMs <= 0)
191
204
  throw new Error('timeout');
192
- response = await requestPinned(currentUrl, resolved, requestImpl, remainingMs, maxBytes);
205
+ response = await requestPinned(currentUrl, resolved, requestImpl, remainingMs, maxBytes, label);
193
206
  }
194
207
  catch (error) {
195
208
  if (Date.now() >= deadline || (error instanceof Error && error.message === 'timeout')) {
196
- throw new Error(`Timed out loading GraphQL schema ${source}`);
209
+ throw new Error(`Timed out loading ${label} ${source}`);
197
210
  }
198
211
  throw error;
199
212
  }
200
213
  if (response.statusCode >= 300 && response.statusCode < 400) {
201
214
  const location = response.headers.location;
202
215
  if (typeof location !== 'string') {
203
- throw new Error(`GraphQL schema redirect from ${currentUrl.href} has no location`);
216
+ throw new Error(`${label} redirect from ${currentUrl.href} has no location`);
204
217
  }
205
218
  if (redirects === MAX_REMOTE_REDIRECTS) {
206
- throw new Error(`Too many redirects loading GraphQL schema ${source}`);
219
+ throw new Error(`Too many redirects loading ${label} ${source}`);
207
220
  }
208
221
  currentUrl = new URL(location, currentUrl);
209
222
  continue;
210
223
  }
211
224
  if (response.statusCode < 200 || response.statusCode >= 300) {
212
- throw new Error(`Unable to load GraphQL schema ${currentUrl.href}: ${response.statusCode} ${response.statusMessage}`);
225
+ throw new Error(`Unable to load ${label} ${currentUrl.href}: ${response.statusCode} ${response.statusMessage}`);
213
226
  }
214
227
  return response.body;
215
228
  }
216
- throw new Error(`Too many redirects loading GraphQL schema ${source}`);
229
+ throw new Error(`Too many redirects loading ${label} ${source}`);
217
230
  }