openapi-typesafe 1.0.0-rc.5 → 1.0.0-rc.6
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 +73 -0
- package/dist/adapters/redocly-loader/query-document-names.cjs +50 -0
- package/dist/adapters/redocly-loader/query-document-names.cjs.map +1 -0
- package/dist/adapters/redocly-loader/query-document-names.mjs +48 -0
- package/dist/adapters/redocly-loader/query-document-names.mjs.map +1 -0
- package/dist/adapters/redocly-loader/query-named-components.cjs +29 -37
- package/dist/adapters/redocly-loader/query-named-components.cjs.map +1 -1
- package/dist/adapters/redocly-loader/query-named-components.mjs +29 -37
- package/dist/adapters/redocly-loader/query-named-components.mjs.map +1 -1
- package/dist/adapters/redocly-loader/redocly-config-source.cjs +3 -0
- package/dist/adapters/redocly-loader/redocly-config-source.cjs.map +1 -1
- package/dist/adapters/redocly-loader/redocly-config-source.mjs +4 -1
- package/dist/adapters/redocly-loader/redocly-config-source.mjs.map +1 -1
- package/dist/adapters/redocly-loader/redocly-schema-loader.cjs +16 -7
- package/dist/adapters/redocly-loader/redocly-schema-loader.cjs.map +1 -1
- package/dist/adapters/redocly-loader/redocly-schema-loader.mjs +17 -8
- package/dist/adapters/redocly-loader/redocly-schema-loader.mjs.map +1 -1
- package/dist/adapters/redocly-loader/ref-access.cjs +9 -1
- package/dist/adapters/redocly-loader/ref-access.cjs.map +1 -1
- package/dist/adapters/redocly-loader/ref-access.mjs +10 -2
- package/dist/adapters/redocly-loader/ref-access.mjs.map +1 -1
- package/dist/adapters/redocly-loader/remote-fetch.cjs +16 -1
- package/dist/adapters/redocly-loader/remote-fetch.cjs.map +1 -1
- package/dist/adapters/redocly-loader/remote-fetch.mjs +16 -2
- package/dist/adapters/redocly-loader/remote-fetch.mjs.map +1 -1
- package/dist/api/cwd-option.cjs +58 -0
- package/dist/api/cwd-option.cjs.map +1 -0
- package/dist/api/cwd-option.mjs +52 -0
- package/dist/api/cwd-option.mjs.map +1 -0
- package/dist/api/default-ports.cjs +15 -7
- package/dist/api/default-ports.cjs.map +1 -1
- package/dist/api/default-ports.mjs +14 -8
- package/dist/api/default-ports.mjs.map +1 -1
- package/dist/api/document-run.cjs +2 -16
- package/dist/api/document-run.cjs.map +1 -1
- package/dist/api/document-run.mjs +2 -12
- package/dist/api/document-run.mjs.map +1 -1
- package/dist/api/legacy-utils.cjs +1 -1
- package/dist/api/legacy-utils.cjs.map +1 -1
- package/dist/api/legacy-utils.mjs +1 -1
- package/dist/api/legacy-utils.mjs.map +1 -1
- package/dist/api/openapi-ts.cjs.map +1 -1
- package/dist/api/openapi-ts.mjs.map +1 -1
- package/dist/api/option-errors.cjs +24 -0
- package/dist/api/option-errors.cjs.map +1 -0
- package/dist/api/option-errors.mjs +21 -0
- package/dist/api/option-errors.mjs.map +1 -0
- package/dist/api/ref-limit-options.cjs +19 -24
- package/dist/api/ref-limit-options.cjs.map +1 -1
- package/dist/api/ref-limit-options.mjs +15 -20
- package/dist/api/ref-limit-options.mjs.map +1 -1
- package/dist/application/transform/document/path-template-refs.cjs +12 -2
- package/dist/application/transform/document/path-template-refs.cjs.map +1 -1
- package/dist/application/transform/document/path-template-refs.mjs +12 -2
- package/dist/application/transform/document/path-template-refs.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/generated-shape.cjs +28 -10
- package/dist/application/transform/ref-pointer/generated-shape.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/generated-shape.mjs +29 -11
- package/dist/application/transform/ref-pointer/generated-shape.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/original-index.cjs +1 -1
- package/dist/application/transform/ref-pointer/original-index.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/original-index.mjs +1 -1
- package/dist/application/transform/ref-pointer/original-index.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/property-shape.cjs +5 -0
- package/dist/application/transform/ref-pointer/property-shape.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/property-shape.mjs +5 -1
- package/dist/application/transform/ref-pointer/property-shape.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/typed-walk.cjs +11 -1
- package/dist/application/transform/ref-pointer/typed-walk.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/typed-walk.mjs +12 -2
- package/dist/application/transform/ref-pointer/typed-walk.mjs.map +1 -1
- package/dist/cli/api-settings.cjs +3 -2
- package/dist/cli/api-settings.cjs.map +1 -1
- package/dist/cli/api-settings.mjs +2 -1
- package/dist/cli/api-settings.mjs.map +1 -1
- package/dist/cli/api-writes.cjs +7 -1
- package/dist/cli/api-writes.cjs.map +1 -1
- package/dist/cli/api-writes.mjs +7 -1
- package/dist/cli/api-writes.mjs.map +1 -1
- package/dist/cli/cli-arguments.cjs +25 -107
- package/dist/cli/cli-arguments.cjs.map +1 -1
- package/dist/cli/cli-arguments.mjs +18 -100
- package/dist/cli/cli-arguments.mjs.map +1 -1
- package/dist/cli/cli-errors.cjs +1 -0
- package/dist/cli/cli-errors.cjs.map +1 -1
- package/dist/cli/cli-errors.mjs +1 -0
- package/dist/cli/cli-errors.mjs.map +1 -1
- package/dist/cli/cli-flag-names.cjs +77 -0
- package/dist/cli/cli-flag-names.cjs.map +1 -0
- package/dist/cli/cli-flag-names.mjs +64 -0
- package/dist/cli/cli-flag-names.mjs.map +1 -0
- package/dist/cli/cli-output.cjs +7 -2
- package/dist/cli/cli-output.cjs.map +1 -1
- package/dist/cli/cli-output.mjs +7 -2
- package/dist/cli/cli-output.mjs.map +1 -1
- package/dist/cli/main.cjs +1 -1
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.mjs +1 -1
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/redocly-apis.cjs +14 -4
- package/dist/cli/redocly-apis.cjs.map +1 -1
- package/dist/cli/redocly-apis.mjs +14 -4
- package/dist/cli/redocly-apis.mjs.map +1 -1
- package/dist/cli/written-flags.cjs +83 -0
- package/dist/cli/written-flags.cjs.map +1 -0
- package/dist/cli/written-flags.mjs +77 -0
- package/dist/cli/written-flags.mjs.map +1 -0
- package/dist/domain/errors.cjs +1 -0
- package/dist/domain/errors.cjs.map +1 -1
- package/dist/domain/errors.mjs +1 -0
- package/dist/domain/errors.mjs.map +1 -1
- package/dist/domain/location-text.cjs +7 -1
- package/dist/domain/location-text.cjs.map +1 -1
- package/dist/domain/location-text.mjs +7 -1
- package/dist/domain/location-text.mjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/package.json +1 -1
- package/src/adapters/redocly-loader/query-document-names.ts +79 -0
- package/src/adapters/redocly-loader/query-named-components.ts +48 -49
- package/src/adapters/redocly-loader/redocly-config-source.ts +11 -1
- package/src/adapters/redocly-loader/redocly-schema-loader.ts +20 -6
- package/src/adapters/redocly-loader/ref-access.ts +16 -2
- package/src/adapters/redocly-loader/remote-fetch.ts +26 -0
- package/src/api/cwd-option.ts +76 -0
- package/src/api/default-ports.ts +33 -10
- package/src/api/document-run.ts +2 -17
- package/src/api/legacy-utils.ts +1 -1
- package/src/api/openapi-ts.ts +14 -1
- package/src/api/option-errors.ts +22 -0
- package/src/api/ref-limit-options.ts +17 -24
- package/src/application/ports/redocly-config-source.ts +5 -0
- package/src/application/transform/document/path-template-refs.ts +28 -7
- package/src/application/transform/ref-pointer/generated-shape.ts +57 -14
- package/src/application/transform/ref-pointer/original-index.ts +6 -3
- package/src/application/transform/ref-pointer/property-shape.ts +9 -0
- package/src/application/transform/ref-pointer/typed-walk.ts +26 -4
- package/src/cli/api-writes.ts +11 -3
- package/src/cli/cli-arguments.ts +42 -153
- package/src/cli/cli-errors.ts +1 -0
- package/src/cli/cli-flag-names.ts +99 -0
- package/src/cli/cli-output.ts +20 -6
- package/src/cli/main.ts +2 -1
- package/src/cli/redocly-apis.ts +21 -5
- package/src/cli/written-flags.ts +131 -0
- package/src/domain/errors.ts +1 -0
- package/src/domain/location-text.ts +17 -2
|
@@ -55,6 +55,18 @@ function fetchWithTimeout(timeoutMs, bodyLimit) {
|
|
|
55
55
|
function fetchWithBodyLimit(fetchImpl, bodyLimit) {
|
|
56
56
|
return async (input, init) => bufferedResponse(await fetchImpl(input, init), input, bodyLimit);
|
|
57
57
|
}
|
|
58
|
+
class RedirectedUrls {
|
|
59
|
+
#finalUrls = /* @__PURE__ */ new Map();
|
|
60
|
+
record(requested, final) {
|
|
61
|
+
if (requested !== final) {
|
|
62
|
+
this.#finalUrls.set(requested, final);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** The URL the document of `requested` came from; `requested` when it was not redirected. */
|
|
66
|
+
finalUrlOf(requested) {
|
|
67
|
+
return this.#finalUrls.get(requested) ?? requested;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
58
70
|
function fetchWithCheckedRedirects(fetchImpl, policy) {
|
|
59
71
|
return async (input, init = {}) => {
|
|
60
72
|
const hosts = input === policy.rootUrl ? void 0 : policy.hosts;
|
|
@@ -70,6 +82,7 @@ function fetchWithCheckedRedirects(fetchImpl, policy) {
|
|
|
70
82
|
const response = await fetchImpl(url.href, { ...init, headers, redirect: "manual" });
|
|
71
83
|
const location = response.headers.get("location");
|
|
72
84
|
if (!REDIRECT_STATUSES.has(response.status) || location === null) {
|
|
85
|
+
policy.redirects.record(input, url.href);
|
|
73
86
|
return response;
|
|
74
87
|
}
|
|
75
88
|
url = new URL(location, url);
|
|
@@ -105,13 +118,15 @@ function resolveConfigWithAccess(resolve, options) {
|
|
|
105
118
|
const checked = fetchWithCheckedRedirects(base, {
|
|
106
119
|
headers: resolve.http.headers,
|
|
107
120
|
hosts: options.access.remoteHosts,
|
|
108
|
-
rootUrl: options.rootUrl
|
|
121
|
+
rootUrl: options.rootUrl,
|
|
122
|
+
redirects: options.redirects
|
|
109
123
|
});
|
|
110
124
|
const customFetch = fetchWithConcurrencyLimit(checked, options.maxParallelFetches);
|
|
111
125
|
return { ...resolve, http: { ...resolve.http, headers: [], customFetch } };
|
|
112
126
|
}
|
|
113
127
|
|
|
114
128
|
exports.MAX_REDIRECTS = MAX_REDIRECTS;
|
|
129
|
+
exports.RedirectedUrls = RedirectedUrls;
|
|
115
130
|
exports.fetchWithCheckedRedirects = fetchWithCheckedRedirects;
|
|
116
131
|
exports.fetchWithConcurrencyLimit = fetchWithConcurrencyLimit;
|
|
117
132
|
exports.resolveConfigWithAccess = resolveConfigWithAccess;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-fetch.cjs","sources":["../../../src/adapters/redocly-loader/remote-fetch.ts"],"sourcesContent":["import type { Config as RedoclyConfig } from \"@redocly/openapi-core\";\nimport type { RefAccess } from \"../../application/ports/schema-loader.js\";\nimport { isHostAllowed } from \"./allowed-hosts.js\";\nimport { type ConfiguredHeader, headersFor } from \"./configured-headers.js\";\nimport { RemoteBodyLimit, type RemoteBodyLimits } from \"./remote-body-limit.js\";\nimport { refForbiddenError } from \"./schema-load-errors.js\";\n\n/**\n * How the loader fetches remote documents: a time limit, size limits, the host limit and the\n * configured headers checked on every redirect.\n */\n\ntype ResolveConfig = RedoclyConfig[\"resolve\"];\n\n/** The fetch Redocly calls for a remote document (`resolve.http.customFetch`). */\nexport type RemoteFetch = (input: string, init?: RequestInit) => Promise<Response>;\n\n/**\n * Redirects followed for one remote document. The loader follows them itself, so that each hop is\n * checked; 20 is the limit of the Fetch standard, which fetch applied before.\n */\nexport const MAX_REDIRECTS = 20;\n\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/** Statuses whose responses have no body; `new Response()` refuses a body for them. */\nconst NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);\n\n/**\n * A response whose body is already read within the size limits, so that reading it again cannot\n * wait, fail or take more memory.\n */\nasync function bufferedResponse(\n response: Response,\n url: string,\n bodyLimit: RemoteBodyLimit,\n): Promise<Response> {\n const body = await bodyLimit.read(response, url);\n const { status, statusText, headers } = response;\n return new Response(NULL_BODY_STATUSES.has(status) ? null : body, {\n status,\n statusText,\n headers,\n });\n}\n\n/**\n * Codes of a connection that closed before any response came. The server can close a kept-alive\n * connection while the process is busy (parsing a large document, generating another API) and\n * has not read the close yet; the next request on that connection then fails with one of these.\n */\nconst CLOSED_CONNECTION_CODES: ReadonlySet<string> = new Set([\n \"ECONNRESET\",\n \"EPIPE\",\n \"UND_ERR_SOCKET\",\n]);\n\n/** Methods that may be sent again without a second effect (RFC 9110 §9.2.2). */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\"]);\nconst DEFAULT_METHOD = \"GET\";\n\n/** Whether fetch failed because its connection closed before any response came. */\nfunction isClosedConnection(error: unknown): boolean {\n const cause = error instanceof TypeError ? error.cause : undefined;\n const code = cause instanceof Error && \"code\" in cause ? cause.code : undefined;\n return typeof code === \"string\" && CLOSED_CONNECTION_CODES.has(code);\n}\n\n/**\n * Fetch, and send the request once more when its connection closed before any response came, as\n * RFC 9112 §9.3.1 allows for an idempotent request. Fetch drops a connection that failed, so the\n * second attempt goes out on another one; a second failure is the server's answer and is thrown.\n */\nasync function fetchOnOpenConnection(input: string, init: RequestInit): Promise<Response> {\n try {\n return await fetch(input, init);\n } catch (error) {\n const method = (init.method ?? DEFAULT_METHOD).toUpperCase();\n if (!(IDEMPOTENT_METHODS.has(method) && isClosedConnection(error))) {\n throw error;\n }\n return fetch(input, init);\n }\n}\n\n/**\n * Fetch with a time limit for the whole response, body included: Node's own fetch waits five\n * minutes for a server that never answers, and a server that sends its body one byte at a time\n * never finishes. The body is read here, inside the limit; when Redocly read it, the abort\n * surfaced as a `DOMException` whose read-only `message` Redocly tried to change, which gave\n * \"Cannot set property message of which has only a getter\" instead of the time limit.\n */\nfunction fetchWithTimeout(timeoutMs: number, bodyLimit: RemoteBodyLimit): RemoteFetch {\n return async (input, init = {}) => {\n try {\n // One signal for both attempts, so that a request sent again stays within the same limit.\n const signal = AbortSignal.timeout(timeoutMs);\n const response = await fetchOnOpenConnection(input, { ...init, signal });\n return await bufferedResponse(response, input, bodyLimit);\n } catch (cause) {\n const isTimeout = cause instanceof Error && cause.name === \"TimeoutError\";\n throw isTimeout ? new Error(`Timed out after ${timeoutMs} ms loading ${input}`) : cause;\n }\n };\n}\n\n/** A configured `customFetch` with its bodies read within the size limits. */\nfunction fetchWithBodyLimit(fetchImpl: RemoteFetch, bodyLimit: RemoteBodyLimit): RemoteFetch {\n return async (input, init) => bufferedResponse(await fetchImpl(input, init), input, bodyLimit);\n}\n\n/** What every hop of a remote fetch is checked against. */\nexport interface RedirectPolicy {\n /** `resolve.http.headers` of the Redocly config; chosen again for the URL of every hop. */\n readonly headers: readonly ConfiguredHeader[];\n /** `--remote-ref-hosts`; undefined allows every host. */\n readonly hosts: readonly string[] | undefined;\n /**\n * The URL of the root document, if it is remote. The developer chose it, so neither it nor its\n * redirects are limited to the hosts (ADR-017): a spec URL that redirects to a CDN loads.\n */\n readonly rootUrl: string | undefined;\n}\n\n/**\n * Follow redirects one by one. Fetch drops only `Authorization`, `Cookie` and\n * `Proxy-Authorization` on a redirect to another origin and keeps every other header (an\n * `X-Api-Key`), so the configured headers are chosen for each hop's own URL instead, and a hop to\n * a host that is not listed is refused before it is fetched. The first URL was checked by the\n * resolver (or is the root document). Redocly passes only the configured headers in `init`, and\n * it gets an empty list of them (`resolveConfigWithAccess`), so `init.headers` is replaced.\n */\nexport function fetchWithCheckedRedirects(\n fetchImpl: RemoteFetch,\n policy: RedirectPolicy,\n): RemoteFetch {\n return async (input, init = {}) => {\n const hosts = input === policy.rootUrl ? undefined : policy.hosts;\n let url = new URL(input);\n for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {\n if (hop > 0 && hosts !== undefined && !isHostAllowed(url, hosts)) {\n throw refForbiddenError(url.href, {\n reason: \"host-not-allowed\",\n allowedHosts: [...hosts],\n });\n }\n const headers = await headersFor(url, policy.headers);\n const response = await fetchImpl(url.href, { ...init, headers, redirect: \"manual\" });\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n return response;\n }\n url = new URL(location, url);\n }\n throw new Error(`More than ${MAX_REDIRECTS} redirects loading ${input}`);\n };\n}\n\n/** Bounds on the remote documents of one load (threat model T2, ADR-033). */\nexport interface RemoteDocumentLimits extends RemoteBodyLimits {\n /** Remote documents one load may fetch, the root included; one more is refused. */\n readonly maxDocuments: number;\n /** Remote documents fetched at the same time; the others wait for a free slot. */\n readonly maxParallelFetches: number;\n /** Nodes (keys and values) the remote documents of one load may parse into together. */\n readonly maxNodes: number;\n}\n\n/**\n * At most `limit` calls of the fetch run at once; the others wait in order. A finished call hands\n * its slot to the next waiting one.\n */\nexport function fetchWithConcurrencyLimit(fetchImpl: RemoteFetch, limit: number): RemoteFetch {\n let running = 0;\n const waiting: (() => void)[] = [];\n return async (input, init) => {\n if (running < limit) {\n running += 1;\n } else {\n await new Promise<void>((resolve) => waiting.push(resolve));\n }\n try {\n return await fetchImpl(input, init);\n } finally {\n const next = waiting.shift();\n if (next === undefined) {\n running -= 1;\n } else {\n next();\n }\n }\n };\n}\n\nexport interface RemoteFetchOptions {\n /** Time limit of one fetch and its body, unless the Redocly config has its own `customFetch`. */\n readonly timeoutMs: number;\n readonly access: RefAccess;\n /** The root document's URL, whose redirects are not limited to the hosts. */\n readonly rootUrl: string | undefined;\n readonly maxParallelFetches: number;\n readonly bodyLimits: RemoteBodyLimits;\n}\n\n/**\n * The resolve config with the fetch the loader uses. A `customFetch` of the Redocly config is\n * kept and wrapped, so that each redirect is checked, each body is read within the size limits\n * and only a few documents load at once. The\n * configured headers move into that wrapper: Redocly would match them against the raw URL and\n * send them on through redirects.\n */\nexport function resolveConfigWithAccess(\n resolve: ResolveConfig,\n options: RemoteFetchOptions,\n): ResolveConfig {\n // Redocly types `customFetch` as `Function`; it is called as fetch is.\n const configured = resolve.http.customFetch as RemoteFetch | undefined;\n // One limit per load, so that the total covers every document this resolver fetches.\n const bodyLimit = new RemoteBodyLimit(options.bodyLimits);\n const base =\n configured === undefined\n ? fetchWithTimeout(options.timeoutMs, bodyLimit)\n : fetchWithBodyLimit(configured, bodyLimit);\n const checked = fetchWithCheckedRedirects(base, {\n headers: resolve.http.headers,\n hosts: options.access.remoteHosts,\n rootUrl: options.rootUrl,\n });\n // The limit is outside the redirects, so all hops of one document share one slot.\n const customFetch = fetchWithConcurrencyLimit(checked, options.maxParallelFetches);\n return { ...resolve, http: { ...resolve.http, headers: [], customFetch } };\n}\n"],"names":["isHostAllowed","refForbiddenError","headersFor","RemoteBodyLimit"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"remote-fetch.cjs","sources":["../../../src/adapters/redocly-loader/remote-fetch.ts"],"sourcesContent":["import type { Config as RedoclyConfig } from \"@redocly/openapi-core\";\nimport type { RefAccess } from \"../../application/ports/schema-loader.js\";\nimport { isHostAllowed } from \"./allowed-hosts.js\";\nimport { type ConfiguredHeader, headersFor } from \"./configured-headers.js\";\nimport { RemoteBodyLimit, type RemoteBodyLimits } from \"./remote-body-limit.js\";\nimport { refForbiddenError } from \"./schema-load-errors.js\";\n\n/**\n * How the loader fetches remote documents: a time limit, size limits, the host limit and the\n * configured headers checked on every redirect.\n */\n\ntype ResolveConfig = RedoclyConfig[\"resolve\"];\n\n/** The fetch Redocly calls for a remote document (`resolve.http.customFetch`). */\nexport type RemoteFetch = (input: string, init?: RequestInit) => Promise<Response>;\n\n/**\n * Redirects followed for one remote document. The loader follows them itself, so that each hop is\n * checked; 20 is the limit of the Fetch standard, which fetch applied before.\n */\nexport const MAX_REDIRECTS = 20;\n\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/** Statuses whose responses have no body; `new Response()` refuses a body for them. */\nconst NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);\n\n/**\n * A response whose body is already read within the size limits, so that reading it again cannot\n * wait, fail or take more memory.\n */\nasync function bufferedResponse(\n response: Response,\n url: string,\n bodyLimit: RemoteBodyLimit,\n): Promise<Response> {\n const body = await bodyLimit.read(response, url);\n const { status, statusText, headers } = response;\n return new Response(NULL_BODY_STATUSES.has(status) ? null : body, {\n status,\n statusText,\n headers,\n });\n}\n\n/**\n * Codes of a connection that closed before any response came. The server can close a kept-alive\n * connection while the process is busy (parsing a large document, generating another API) and\n * has not read the close yet; the next request on that connection then fails with one of these.\n */\nconst CLOSED_CONNECTION_CODES: ReadonlySet<string> = new Set([\n \"ECONNRESET\",\n \"EPIPE\",\n \"UND_ERR_SOCKET\",\n]);\n\n/** Methods that may be sent again without a second effect (RFC 9110 §9.2.2). */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\"]);\nconst DEFAULT_METHOD = \"GET\";\n\n/** Whether fetch failed because its connection closed before any response came. */\nfunction isClosedConnection(error: unknown): boolean {\n const cause = error instanceof TypeError ? error.cause : undefined;\n const code = cause instanceof Error && \"code\" in cause ? cause.code : undefined;\n return typeof code === \"string\" && CLOSED_CONNECTION_CODES.has(code);\n}\n\n/**\n * Fetch, and send the request once more when its connection closed before any response came, as\n * RFC 9112 §9.3.1 allows for an idempotent request. Fetch drops a connection that failed, so the\n * second attempt goes out on another one; a second failure is the server's answer and is thrown.\n */\nasync function fetchOnOpenConnection(input: string, init: RequestInit): Promise<Response> {\n try {\n return await fetch(input, init);\n } catch (error) {\n const method = (init.method ?? DEFAULT_METHOD).toUpperCase();\n if (!(IDEMPOTENT_METHODS.has(method) && isClosedConnection(error))) {\n throw error;\n }\n return fetch(input, init);\n }\n}\n\n/**\n * Fetch with a time limit for the whole response, body included: Node's own fetch waits five\n * minutes for a server that never answers, and a server that sends its body one byte at a time\n * never finishes. The body is read here, inside the limit; when Redocly read it, the abort\n * surfaced as a `DOMException` whose read-only `message` Redocly tried to change, which gave\n * \"Cannot set property message of which has only a getter\" instead of the time limit.\n */\nfunction fetchWithTimeout(timeoutMs: number, bodyLimit: RemoteBodyLimit): RemoteFetch {\n return async (input, init = {}) => {\n try {\n // One signal for both attempts, so that a request sent again stays within the same limit.\n const signal = AbortSignal.timeout(timeoutMs);\n const response = await fetchOnOpenConnection(input, { ...init, signal });\n return await bufferedResponse(response, input, bodyLimit);\n } catch (cause) {\n const isTimeout = cause instanceof Error && cause.name === \"TimeoutError\";\n throw isTimeout ? new Error(`Timed out after ${timeoutMs} ms loading ${input}`) : cause;\n }\n };\n}\n\n/** A configured `customFetch` with its bodies read within the size limits. */\nfunction fetchWithBodyLimit(fetchImpl: RemoteFetch, bodyLimit: RemoteBodyLimit): RemoteFetch {\n return async (input, init) => bufferedResponse(await fetchImpl(input, init), input, bodyLimit);\n}\n\n/**\n * The URL each remote document was read from in the end, when redirects moved it. A relative\n * `$ref` resolves against the URL the document was retrieved from (RFC 3986, section 5.1.3), so\n * `/latest` that redirects to `/static/v1/root.yaml` loads `./pet.yaml` from `/static/v1/`.\n * Redocly reads only the body of the response, so the fetch records the URL here.\n */\nexport class RedirectedUrls {\n readonly #finalUrls = new Map<string, string>();\n\n record(requested: string, final: string): void {\n if (requested !== final) {\n this.#finalUrls.set(requested, final);\n }\n }\n\n /** The URL the document of `requested` came from; `requested` when it was not redirected. */\n finalUrlOf(requested: string): string {\n return this.#finalUrls.get(requested) ?? requested;\n }\n}\n\n/** What every hop of a remote fetch is checked against. */\nexport interface RedirectPolicy {\n /** `resolve.http.headers` of the Redocly config; chosen again for the URL of every hop. */\n readonly headers: readonly ConfiguredHeader[];\n /** `--remote-ref-hosts`; undefined allows every host. */\n readonly hosts: readonly string[] | undefined;\n /**\n * The URL of the root document, if it is remote. The developer chose it, so neither it nor its\n * redirects are limited to the hosts (ADR-017): a spec URL that redirects to a CDN loads.\n */\n readonly rootUrl: string | undefined;\n /** Where the final URL of each redirected document is recorded. */\n readonly redirects: RedirectedUrls;\n}\n\n/**\n * Follow redirects one by one. Fetch drops only `Authorization`, `Cookie` and\n * `Proxy-Authorization` on a redirect to another origin and keeps every other header (an\n * `X-Api-Key`), so the configured headers are chosen for each hop's own URL instead, and a hop to\n * a host that is not listed is refused before it is fetched. The first URL was checked by the\n * resolver (or is the root document). Redocly passes only the configured headers in `init`, and\n * it gets an empty list of them (`resolveConfigWithAccess`), so `init.headers` is replaced.\n */\nexport function fetchWithCheckedRedirects(\n fetchImpl: RemoteFetch,\n policy: RedirectPolicy,\n): RemoteFetch {\n return async (input, init = {}) => {\n const hosts = input === policy.rootUrl ? undefined : policy.hosts;\n let url = new URL(input);\n for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {\n if (hop > 0 && hosts !== undefined && !isHostAllowed(url, hosts)) {\n throw refForbiddenError(url.href, {\n reason: \"host-not-allowed\",\n allowedHosts: [...hosts],\n });\n }\n const headers = await headersFor(url, policy.headers);\n const response = await fetchImpl(url.href, { ...init, headers, redirect: \"manual\" });\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n policy.redirects.record(input, url.href);\n return response;\n }\n url = new URL(location, url);\n }\n throw new Error(`More than ${MAX_REDIRECTS} redirects loading ${input}`);\n };\n}\n\n/** Bounds on the remote documents of one load (threat model T2, ADR-033). */\nexport interface RemoteDocumentLimits extends RemoteBodyLimits {\n /** Remote documents one load may fetch, the root included; one more is refused. */\n readonly maxDocuments: number;\n /** Remote documents fetched at the same time; the others wait for a free slot. */\n readonly maxParallelFetches: number;\n /** Nodes (keys and values) the remote documents of one load may parse into together. */\n readonly maxNodes: number;\n}\n\n/**\n * At most `limit` calls of the fetch run at once; the others wait in order. A finished call hands\n * its slot to the next waiting one.\n */\nexport function fetchWithConcurrencyLimit(fetchImpl: RemoteFetch, limit: number): RemoteFetch {\n let running = 0;\n const waiting: (() => void)[] = [];\n return async (input, init) => {\n if (running < limit) {\n running += 1;\n } else {\n await new Promise<void>((resolve) => waiting.push(resolve));\n }\n try {\n return await fetchImpl(input, init);\n } finally {\n const next = waiting.shift();\n if (next === undefined) {\n running -= 1;\n } else {\n next();\n }\n }\n };\n}\n\nexport interface RemoteFetchOptions {\n /** Time limit of one fetch and its body, unless the Redocly config has its own `customFetch`. */\n readonly timeoutMs: number;\n readonly access: RefAccess;\n /** The root document's URL, whose redirects are not limited to the hosts. */\n readonly rootUrl: string | undefined;\n readonly maxParallelFetches: number;\n readonly bodyLimits: RemoteBodyLimits;\n readonly redirects: RedirectedUrls;\n}\n\n/**\n * The resolve config with the fetch the loader uses. A `customFetch` of the Redocly config is\n * kept and wrapped, so that each redirect is checked, each body is read within the size limits\n * and only a few documents load at once. The\n * configured headers move into that wrapper: Redocly would match them against the raw URL and\n * send them on through redirects.\n */\nexport function resolveConfigWithAccess(\n resolve: ResolveConfig,\n options: RemoteFetchOptions,\n): ResolveConfig {\n // Redocly types `customFetch` as `Function`; it is called as fetch is.\n const configured = resolve.http.customFetch as RemoteFetch | undefined;\n // One limit per load, so that the total covers every document this resolver fetches.\n const bodyLimit = new RemoteBodyLimit(options.bodyLimits);\n const base =\n configured === undefined\n ? fetchWithTimeout(options.timeoutMs, bodyLimit)\n : fetchWithBodyLimit(configured, bodyLimit);\n const checked = fetchWithCheckedRedirects(base, {\n headers: resolve.http.headers,\n hosts: options.access.remoteHosts,\n rootUrl: options.rootUrl,\n redirects: options.redirects,\n });\n // The limit is outside the redirects, so all hops of one document share one slot.\n const customFetch = fetchWithConcurrencyLimit(checked, options.maxParallelFetches);\n return { ...resolve, http: { ...resolve.http, headers: [], customFetch } };\n}\n"],"names":["isHostAllowed","refForbiddenError","headersFor","RemoteBodyLimit"],"mappings":"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"}
|
|
@@ -53,6 +53,18 @@ function fetchWithTimeout(timeoutMs, bodyLimit) {
|
|
|
53
53
|
function fetchWithBodyLimit(fetchImpl, bodyLimit) {
|
|
54
54
|
return async (input, init) => bufferedResponse(await fetchImpl(input, init), input, bodyLimit);
|
|
55
55
|
}
|
|
56
|
+
class RedirectedUrls {
|
|
57
|
+
#finalUrls = /* @__PURE__ */ new Map();
|
|
58
|
+
record(requested, final) {
|
|
59
|
+
if (requested !== final) {
|
|
60
|
+
this.#finalUrls.set(requested, final);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** The URL the document of `requested` came from; `requested` when it was not redirected. */
|
|
64
|
+
finalUrlOf(requested) {
|
|
65
|
+
return this.#finalUrls.get(requested) ?? requested;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
56
68
|
function fetchWithCheckedRedirects(fetchImpl, policy) {
|
|
57
69
|
return async (input, init = {}) => {
|
|
58
70
|
const hosts = input === policy.rootUrl ? void 0 : policy.hosts;
|
|
@@ -68,6 +80,7 @@ function fetchWithCheckedRedirects(fetchImpl, policy) {
|
|
|
68
80
|
const response = await fetchImpl(url.href, { ...init, headers, redirect: "manual" });
|
|
69
81
|
const location = response.headers.get("location");
|
|
70
82
|
if (!REDIRECT_STATUSES.has(response.status) || location === null) {
|
|
83
|
+
policy.redirects.record(input, url.href);
|
|
71
84
|
return response;
|
|
72
85
|
}
|
|
73
86
|
url = new URL(location, url);
|
|
@@ -103,11 +116,12 @@ function resolveConfigWithAccess(resolve, options) {
|
|
|
103
116
|
const checked = fetchWithCheckedRedirects(base, {
|
|
104
117
|
headers: resolve.http.headers,
|
|
105
118
|
hosts: options.access.remoteHosts,
|
|
106
|
-
rootUrl: options.rootUrl
|
|
119
|
+
rootUrl: options.rootUrl,
|
|
120
|
+
redirects: options.redirects
|
|
107
121
|
});
|
|
108
122
|
const customFetch = fetchWithConcurrencyLimit(checked, options.maxParallelFetches);
|
|
109
123
|
return { ...resolve, http: { ...resolve.http, headers: [], customFetch } };
|
|
110
124
|
}
|
|
111
125
|
|
|
112
|
-
export { MAX_REDIRECTS, fetchWithCheckedRedirects, fetchWithConcurrencyLimit, resolveConfigWithAccess };
|
|
126
|
+
export { MAX_REDIRECTS, RedirectedUrls, fetchWithCheckedRedirects, fetchWithConcurrencyLimit, resolveConfigWithAccess };
|
|
113
127
|
//# sourceMappingURL=remote-fetch.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-fetch.mjs","sources":["../../../src/adapters/redocly-loader/remote-fetch.ts"],"sourcesContent":["import type { Config as RedoclyConfig } from \"@redocly/openapi-core\";\nimport type { RefAccess } from \"../../application/ports/schema-loader.js\";\nimport { isHostAllowed } from \"./allowed-hosts.js\";\nimport { type ConfiguredHeader, headersFor } from \"./configured-headers.js\";\nimport { RemoteBodyLimit, type RemoteBodyLimits } from \"./remote-body-limit.js\";\nimport { refForbiddenError } from \"./schema-load-errors.js\";\n\n/**\n * How the loader fetches remote documents: a time limit, size limits, the host limit and the\n * configured headers checked on every redirect.\n */\n\ntype ResolveConfig = RedoclyConfig[\"resolve\"];\n\n/** The fetch Redocly calls for a remote document (`resolve.http.customFetch`). */\nexport type RemoteFetch = (input: string, init?: RequestInit) => Promise<Response>;\n\n/**\n * Redirects followed for one remote document. The loader follows them itself, so that each hop is\n * checked; 20 is the limit of the Fetch standard, which fetch applied before.\n */\nexport const MAX_REDIRECTS = 20;\n\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/** Statuses whose responses have no body; `new Response()` refuses a body for them. */\nconst NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);\n\n/**\n * A response whose body is already read within the size limits, so that reading it again cannot\n * wait, fail or take more memory.\n */\nasync function bufferedResponse(\n response: Response,\n url: string,\n bodyLimit: RemoteBodyLimit,\n): Promise<Response> {\n const body = await bodyLimit.read(response, url);\n const { status, statusText, headers } = response;\n return new Response(NULL_BODY_STATUSES.has(status) ? null : body, {\n status,\n statusText,\n headers,\n });\n}\n\n/**\n * Codes of a connection that closed before any response came. The server can close a kept-alive\n * connection while the process is busy (parsing a large document, generating another API) and\n * has not read the close yet; the next request on that connection then fails with one of these.\n */\nconst CLOSED_CONNECTION_CODES: ReadonlySet<string> = new Set([\n \"ECONNRESET\",\n \"EPIPE\",\n \"UND_ERR_SOCKET\",\n]);\n\n/** Methods that may be sent again without a second effect (RFC 9110 §9.2.2). */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\"]);\nconst DEFAULT_METHOD = \"GET\";\n\n/** Whether fetch failed because its connection closed before any response came. */\nfunction isClosedConnection(error: unknown): boolean {\n const cause = error instanceof TypeError ? error.cause : undefined;\n const code = cause instanceof Error && \"code\" in cause ? cause.code : undefined;\n return typeof code === \"string\" && CLOSED_CONNECTION_CODES.has(code);\n}\n\n/**\n * Fetch, and send the request once more when its connection closed before any response came, as\n * RFC 9112 §9.3.1 allows for an idempotent request. Fetch drops a connection that failed, so the\n * second attempt goes out on another one; a second failure is the server's answer and is thrown.\n */\nasync function fetchOnOpenConnection(input: string, init: RequestInit): Promise<Response> {\n try {\n return await fetch(input, init);\n } catch (error) {\n const method = (init.method ?? DEFAULT_METHOD).toUpperCase();\n if (!(IDEMPOTENT_METHODS.has(method) && isClosedConnection(error))) {\n throw error;\n }\n return fetch(input, init);\n }\n}\n\n/**\n * Fetch with a time limit for the whole response, body included: Node's own fetch waits five\n * minutes for a server that never answers, and a server that sends its body one byte at a time\n * never finishes. The body is read here, inside the limit; when Redocly read it, the abort\n * surfaced as a `DOMException` whose read-only `message` Redocly tried to change, which gave\n * \"Cannot set property message of which has only a getter\" instead of the time limit.\n */\nfunction fetchWithTimeout(timeoutMs: number, bodyLimit: RemoteBodyLimit): RemoteFetch {\n return async (input, init = {}) => {\n try {\n // One signal for both attempts, so that a request sent again stays within the same limit.\n const signal = AbortSignal.timeout(timeoutMs);\n const response = await fetchOnOpenConnection(input, { ...init, signal });\n return await bufferedResponse(response, input, bodyLimit);\n } catch (cause) {\n const isTimeout = cause instanceof Error && cause.name === \"TimeoutError\";\n throw isTimeout ? new Error(`Timed out after ${timeoutMs} ms loading ${input}`) : cause;\n }\n };\n}\n\n/** A configured `customFetch` with its bodies read within the size limits. */\nfunction fetchWithBodyLimit(fetchImpl: RemoteFetch, bodyLimit: RemoteBodyLimit): RemoteFetch {\n return async (input, init) => bufferedResponse(await fetchImpl(input, init), input, bodyLimit);\n}\n\n/** What every hop of a remote fetch is checked against. */\nexport interface RedirectPolicy {\n /** `resolve.http.headers` of the Redocly config; chosen again for the URL of every hop. */\n readonly headers: readonly ConfiguredHeader[];\n /** `--remote-ref-hosts`; undefined allows every host. */\n readonly hosts: readonly string[] | undefined;\n /**\n * The URL of the root document, if it is remote. The developer chose it, so neither it nor its\n * redirects are limited to the hosts (ADR-017): a spec URL that redirects to a CDN loads.\n */\n readonly rootUrl: string | undefined;\n}\n\n/**\n * Follow redirects one by one. Fetch drops only `Authorization`, `Cookie` and\n * `Proxy-Authorization` on a redirect to another origin and keeps every other header (an\n * `X-Api-Key`), so the configured headers are chosen for each hop's own URL instead, and a hop to\n * a host that is not listed is refused before it is fetched. The first URL was checked by the\n * resolver (or is the root document). Redocly passes only the configured headers in `init`, and\n * it gets an empty list of them (`resolveConfigWithAccess`), so `init.headers` is replaced.\n */\nexport function fetchWithCheckedRedirects(\n fetchImpl: RemoteFetch,\n policy: RedirectPolicy,\n): RemoteFetch {\n return async (input, init = {}) => {\n const hosts = input === policy.rootUrl ? undefined : policy.hosts;\n let url = new URL(input);\n for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {\n if (hop > 0 && hosts !== undefined && !isHostAllowed(url, hosts)) {\n throw refForbiddenError(url.href, {\n reason: \"host-not-allowed\",\n allowedHosts: [...hosts],\n });\n }\n const headers = await headersFor(url, policy.headers);\n const response = await fetchImpl(url.href, { ...init, headers, redirect: \"manual\" });\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n return response;\n }\n url = new URL(location, url);\n }\n throw new Error(`More than ${MAX_REDIRECTS} redirects loading ${input}`);\n };\n}\n\n/** Bounds on the remote documents of one load (threat model T2, ADR-033). */\nexport interface RemoteDocumentLimits extends RemoteBodyLimits {\n /** Remote documents one load may fetch, the root included; one more is refused. */\n readonly maxDocuments: number;\n /** Remote documents fetched at the same time; the others wait for a free slot. */\n readonly maxParallelFetches: number;\n /** Nodes (keys and values) the remote documents of one load may parse into together. */\n readonly maxNodes: number;\n}\n\n/**\n * At most `limit` calls of the fetch run at once; the others wait in order. A finished call hands\n * its slot to the next waiting one.\n */\nexport function fetchWithConcurrencyLimit(fetchImpl: RemoteFetch, limit: number): RemoteFetch {\n let running = 0;\n const waiting: (() => void)[] = [];\n return async (input, init) => {\n if (running < limit) {\n running += 1;\n } else {\n await new Promise<void>((resolve) => waiting.push(resolve));\n }\n try {\n return await fetchImpl(input, init);\n } finally {\n const next = waiting.shift();\n if (next === undefined) {\n running -= 1;\n } else {\n next();\n }\n }\n };\n}\n\nexport interface RemoteFetchOptions {\n /** Time limit of one fetch and its body, unless the Redocly config has its own `customFetch`. */\n readonly timeoutMs: number;\n readonly access: RefAccess;\n /** The root document's URL, whose redirects are not limited to the hosts. */\n readonly rootUrl: string | undefined;\n readonly maxParallelFetches: number;\n readonly bodyLimits: RemoteBodyLimits;\n}\n\n/**\n * The resolve config with the fetch the loader uses. A `customFetch` of the Redocly config is\n * kept and wrapped, so that each redirect is checked, each body is read within the size limits\n * and only a few documents load at once. The\n * configured headers move into that wrapper: Redocly would match them against the raw URL and\n * send them on through redirects.\n */\nexport function resolveConfigWithAccess(\n resolve: ResolveConfig,\n options: RemoteFetchOptions,\n): ResolveConfig {\n // Redocly types `customFetch` as `Function`; it is called as fetch is.\n const configured = resolve.http.customFetch as RemoteFetch | undefined;\n // One limit per load, so that the total covers every document this resolver fetches.\n const bodyLimit = new RemoteBodyLimit(options.bodyLimits);\n const base =\n configured === undefined\n ? fetchWithTimeout(options.timeoutMs, bodyLimit)\n : fetchWithBodyLimit(configured, bodyLimit);\n const checked = fetchWithCheckedRedirects(base, {\n headers: resolve.http.headers,\n hosts: options.access.remoteHosts,\n rootUrl: options.rootUrl,\n });\n // The limit is outside the redirects, so all hops of one document share one slot.\n const customFetch = fetchWithConcurrencyLimit(checked, options.maxParallelFetches);\n return { ...resolve, http: { ...resolve.http, headers: [], customFetch } };\n}\n"],"names":[],"mappings":";;;;;AAqBO,MAAM,aAAA,GAAgB;AAE7B,MAAM,iBAAA,uBAAwB,GAAA,CAAI,CAAC,KAAK,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAC,CAAA;AAG3D,MAAM,kBAAA,uBAAyB,GAAA,CAAI,CAAC,KAAK,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAC,CAAA;AAM5D,eAAe,gBAAA,CACb,QAAA,EACA,GAAA,EACA,SAAA,EACmB;AACnB,EAAA,MAAM,IAAA,GAAO,MAAM,SAAA,CAAU,IAAA,CAAK,UAAU,GAAG,CAAA;AAC/C,EAAA,MAAM,EAAE,MAAA,EAAQ,UAAA,EAAY,OAAA,EAAQ,GAAI,QAAA;AACxC,EAAA,OAAO,IAAI,QAAA,CAAS,kBAAA,CAAmB,IAAI,MAAM,CAAA,GAAI,OAAO,IAAA,EAAM;AAAA,IAChE,MAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACD,CAAA;AACH;AAOA,MAAM,uBAAA,uBAAmD,GAAA,CAAI;AAAA,EAC3D,YAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAC,CAAA;AAGD,MAAM,qCAA0C,IAAI,GAAA,CAAI,CAAC,KAAA,EAAO,MAAM,CAAC,CAAA;AACvE,MAAM,cAAA,GAAiB,KAAA;AAGvB,SAAS,mBAAmB,KAAA,EAAyB;AACnD,EAAA,MAAM,KAAA,GAAQ,KAAA,YAAiB,SAAA,GAAY,KAAA,CAAM,KAAA,GAAQ,MAAA;AACzD,EAAA,MAAM,OAAO,KAAA,YAAiB,KAAA,IAAS,MAAA,IAAU,KAAA,GAAQ,MAAM,IAAA,GAAO,MAAA;AACtE,EAAA,OAAO,OAAO,IAAA,KAAS,QAAA,IAAY,uBAAA,CAAwB,IAAI,IAAI,CAAA;AACrE;AAOA,eAAe,qBAAA,CAAsB,OAAe,IAAA,EAAsC;AACxF,EAAA,IAAI;AACF,IAAA,OAAO,MAAM,KAAA,CAAM,KAAA,EAAO,IAAI,CAAA;AAAA,EAChC,SAAS,KAAA,EAAO;AACd,IAAA,MAAM,MAAA,GAAA,CAAU,IAAA,CAAK,MAAA,IAAU,cAAA,EAAgB,WAAA,EAAY;AAC3D,IAAA,IAAI,EAAE,kBAAA,CAAmB,GAAA,CAAI,MAAM,CAAA,IAAK,kBAAA,CAAmB,KAAK,CAAA,CAAA,EAAI;AAClE,MAAA,MAAM,KAAA;AAAA,IACR;AACA,IAAA,OAAO,KAAA,CAAM,OAAO,IAAI,CAAA;AAAA,EAC1B;AACF;AASA,SAAS,gBAAA,CAAiB,WAAmB,SAAA,EAAyC;AACpF,EAAA,OAAO,OAAO,KAAA,EAAO,IAAA,GAAO,EAAC,KAAM;AACjC,IAAA,IAAI;AAEF,MAAA,MAAM,MAAA,GAAS,WAAA,CAAY,OAAA,CAAQ,SAAS,CAAA;AAC5C,MAAA,MAAM,QAAA,GAAW,MAAM,qBAAA,CAAsB,KAAA,EAAO,EAAE,GAAG,IAAA,EAAM,QAAQ,CAAA;AACvE,MAAA,OAAO,MAAM,gBAAA,CAAiB,QAAA,EAAU,KAAA,EAAO,SAAS,CAAA;AAAA,IAC1D,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,SAAA,GAAY,KAAA,YAAiB,KAAA,IAAS,KAAA,CAAM,IAAA,KAAS,cAAA;AAC3D,MAAA,MAAM,SAAA,GAAY,IAAI,KAAA,CAAM,CAAA,gBAAA,EAAmB,SAAS,CAAA,YAAA,EAAe,KAAK,EAAE,CAAA,GAAI,KAAA;AAAA,IACpF;AAAA,EACF,CAAA;AACF;AAGA,SAAS,kBAAA,CAAmB,WAAwB,SAAA,EAAyC;AAC3F,EAAA,OAAO,OAAO,KAAA,EAAO,IAAA,KAAS,gBAAA,CAAiB,MAAM,UAAU,KAAA,EAAO,IAAI,CAAA,EAAG,KAAA,EAAO,SAAS,CAAA;AAC/F;AAuBO,SAAS,yBAAA,CACd,WACA,MAAA,EACa;AACb,EAAA,OAAO,OAAO,KAAA,EAAO,IAAA,GAAO,EAAC,KAAM;AACjC,IAAA,MAAM,KAAA,GAAQ,KAAA,KAAU,MAAA,CAAO,OAAA,GAAU,SAAY,MAAA,CAAO,KAAA;AAC5D,IAAA,IAAI,GAAA,GAAM,IAAI,GAAA,CAAI,KAAK,CAAA;AACvB,IAAA,KAAA,IAAS,GAAA,GAAM,CAAA,EAAG,GAAA,IAAO,aAAA,EAAe,OAAO,CAAA,EAAG;AAChD,MAAA,IAAI,GAAA,GAAM,KAAK,KAAA,KAAU,MAAA,IAAa,CAAC,aAAA,CAAc,GAAA,EAAK,KAAK,CAAA,EAAG;AAChE,QAAA,MAAM,iBAAA,CAAkB,IAAI,IAAA,EAAM;AAAA,UAChC,MAAA,EAAQ,kBAAA;AAAA,UACR,YAAA,EAAc,CAAC,GAAG,KAAK;AAAA,SACxB,CAAA;AAAA,MACH;AACA,MAAA,MAAM,OAAA,GAAU,MAAM,UAAA,CAAW,GAAA,EAAK,OAAO,OAAO,CAAA;AACpD,MAAA,MAAM,QAAA,GAAW,MAAM,SAAA,CAAU,GAAA,CAAI,IAAA,EAAM,EAAE,GAAG,IAAA,EAAM,OAAA,EAAS,QAAA,EAAU,QAAA,EAAU,CAAA;AACnF,MAAA,MAAM,QAAA,GAAW,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA;AAChD,MAAA,IAAI,CAAC,iBAAA,CAAkB,GAAA,CAAI,SAAS,MAAM,CAAA,IAAK,aAAa,IAAA,EAAM;AAChE,QAAA,OAAO,QAAA;AAAA,MACT;AACA,MAAA,GAAA,GAAM,IAAI,GAAA,CAAI,QAAA,EAAU,GAAG,CAAA;AAAA,IAC7B;AACA,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,UAAA,EAAa,aAAa,CAAA,mBAAA,EAAsB,KAAK,CAAA,CAAE,CAAA;AAAA,EACzE,CAAA;AACF;AAgBO,SAAS,yBAAA,CAA0B,WAAwB,KAAA,EAA4B;AAC5F,EAAA,IAAI,OAAA,GAAU,CAAA;AACd,EAAA,MAAM,UAA0B,EAAC;AACjC,EAAA,OAAO,OAAO,OAAO,IAAA,KAAS;AAC5B,IAAA,IAAI,UAAU,KAAA,EAAO;AACnB,MAAA,OAAA,IAAW,CAAA;AAAA,IACb,CAAA,MAAO;AACL,MAAA,MAAM,IAAI,OAAA,CAAc,CAAC,YAAY,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAC,CAAA;AAAA,IAC5D;AACA,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,SAAA,CAAU,KAAA,EAAO,IAAI,CAAA;AAAA,IACpC,CAAA,SAAE;AACA,MAAA,MAAM,IAAA,GAAO,QAAQ,KAAA,EAAM;AAC3B,MAAA,IAAI,SAAS,MAAA,EAAW;AACtB,QAAA,OAAA,IAAW,CAAA;AAAA,MACb,CAAA,MAAO;AACL,QAAA,IAAA,EAAK;AAAA,MACP;AAAA,IACF;AAAA,EACF,CAAA;AACF;AAmBO,SAAS,uBAAA,CACd,SACA,OAAA,EACe;AAEf,EAAA,MAAM,UAAA,GAAa,QAAQ,IAAA,CAAK,WAAA;AAEhC,EAAA,MAAM,SAAA,GAAY,IAAI,eAAA,CAAgB,OAAA,CAAQ,UAAU,CAAA;AACxD,EAAA,MAAM,IAAA,GACJ,UAAA,KAAe,MAAA,GACX,gBAAA,CAAiB,OAAA,CAAQ,WAAW,SAAS,CAAA,GAC7C,kBAAA,CAAmB,UAAA,EAAY,SAAS,CAAA;AAC9C,EAAA,MAAM,OAAA,GAAU,0BAA0B,IAAA,EAAM;AAAA,IAC9C,OAAA,EAAS,QAAQ,IAAA,CAAK,OAAA;AAAA,IACtB,KAAA,EAAO,QAAQ,MAAA,CAAO,WAAA;AAAA,IACtB,SAAS,OAAA,CAAQ;AAAA,GAClB,CAAA;AAED,EAAA,MAAM,WAAA,GAAc,yBAAA,CAA0B,OAAA,EAAS,OAAA,CAAQ,kBAAkB,CAAA;AACjF,EAAA,OAAO,EAAE,GAAG,OAAA,EAAS,IAAA,EAAM,EAAE,GAAG,OAAA,CAAQ,IAAA,EAAM,OAAA,EAAS,EAAC,EAAG,WAAA,EAAY,EAAE;AAC3E;;"}
|
|
1
|
+
{"version":3,"file":"remote-fetch.mjs","sources":["../../../src/adapters/redocly-loader/remote-fetch.ts"],"sourcesContent":["import type { Config as RedoclyConfig } from \"@redocly/openapi-core\";\nimport type { RefAccess } from \"../../application/ports/schema-loader.js\";\nimport { isHostAllowed } from \"./allowed-hosts.js\";\nimport { type ConfiguredHeader, headersFor } from \"./configured-headers.js\";\nimport { RemoteBodyLimit, type RemoteBodyLimits } from \"./remote-body-limit.js\";\nimport { refForbiddenError } from \"./schema-load-errors.js\";\n\n/**\n * How the loader fetches remote documents: a time limit, size limits, the host limit and the\n * configured headers checked on every redirect.\n */\n\ntype ResolveConfig = RedoclyConfig[\"resolve\"];\n\n/** The fetch Redocly calls for a remote document (`resolve.http.customFetch`). */\nexport type RemoteFetch = (input: string, init?: RequestInit) => Promise<Response>;\n\n/**\n * Redirects followed for one remote document. The loader follows them itself, so that each hop is\n * checked; 20 is the limit of the Fetch standard, which fetch applied before.\n */\nexport const MAX_REDIRECTS = 20;\n\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/** Statuses whose responses have no body; `new Response()` refuses a body for them. */\nconst NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);\n\n/**\n * A response whose body is already read within the size limits, so that reading it again cannot\n * wait, fail or take more memory.\n */\nasync function bufferedResponse(\n response: Response,\n url: string,\n bodyLimit: RemoteBodyLimit,\n): Promise<Response> {\n const body = await bodyLimit.read(response, url);\n const { status, statusText, headers } = response;\n return new Response(NULL_BODY_STATUSES.has(status) ? null : body, {\n status,\n statusText,\n headers,\n });\n}\n\n/**\n * Codes of a connection that closed before any response came. The server can close a kept-alive\n * connection while the process is busy (parsing a large document, generating another API) and\n * has not read the close yet; the next request on that connection then fails with one of these.\n */\nconst CLOSED_CONNECTION_CODES: ReadonlySet<string> = new Set([\n \"ECONNRESET\",\n \"EPIPE\",\n \"UND_ERR_SOCKET\",\n]);\n\n/** Methods that may be sent again without a second effect (RFC 9110 §9.2.2). */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\"]);\nconst DEFAULT_METHOD = \"GET\";\n\n/** Whether fetch failed because its connection closed before any response came. */\nfunction isClosedConnection(error: unknown): boolean {\n const cause = error instanceof TypeError ? error.cause : undefined;\n const code = cause instanceof Error && \"code\" in cause ? cause.code : undefined;\n return typeof code === \"string\" && CLOSED_CONNECTION_CODES.has(code);\n}\n\n/**\n * Fetch, and send the request once more when its connection closed before any response came, as\n * RFC 9112 §9.3.1 allows for an idempotent request. Fetch drops a connection that failed, so the\n * second attempt goes out on another one; a second failure is the server's answer and is thrown.\n */\nasync function fetchOnOpenConnection(input: string, init: RequestInit): Promise<Response> {\n try {\n return await fetch(input, init);\n } catch (error) {\n const method = (init.method ?? DEFAULT_METHOD).toUpperCase();\n if (!(IDEMPOTENT_METHODS.has(method) && isClosedConnection(error))) {\n throw error;\n }\n return fetch(input, init);\n }\n}\n\n/**\n * Fetch with a time limit for the whole response, body included: Node's own fetch waits five\n * minutes for a server that never answers, and a server that sends its body one byte at a time\n * never finishes. The body is read here, inside the limit; when Redocly read it, the abort\n * surfaced as a `DOMException` whose read-only `message` Redocly tried to change, which gave\n * \"Cannot set property message of which has only a getter\" instead of the time limit.\n */\nfunction fetchWithTimeout(timeoutMs: number, bodyLimit: RemoteBodyLimit): RemoteFetch {\n return async (input, init = {}) => {\n try {\n // One signal for both attempts, so that a request sent again stays within the same limit.\n const signal = AbortSignal.timeout(timeoutMs);\n const response = await fetchOnOpenConnection(input, { ...init, signal });\n return await bufferedResponse(response, input, bodyLimit);\n } catch (cause) {\n const isTimeout = cause instanceof Error && cause.name === \"TimeoutError\";\n throw isTimeout ? new Error(`Timed out after ${timeoutMs} ms loading ${input}`) : cause;\n }\n };\n}\n\n/** A configured `customFetch` with its bodies read within the size limits. */\nfunction fetchWithBodyLimit(fetchImpl: RemoteFetch, bodyLimit: RemoteBodyLimit): RemoteFetch {\n return async (input, init) => bufferedResponse(await fetchImpl(input, init), input, bodyLimit);\n}\n\n/**\n * The URL each remote document was read from in the end, when redirects moved it. A relative\n * `$ref` resolves against the URL the document was retrieved from (RFC 3986, section 5.1.3), so\n * `/latest` that redirects to `/static/v1/root.yaml` loads `./pet.yaml` from `/static/v1/`.\n * Redocly reads only the body of the response, so the fetch records the URL here.\n */\nexport class RedirectedUrls {\n readonly #finalUrls = new Map<string, string>();\n\n record(requested: string, final: string): void {\n if (requested !== final) {\n this.#finalUrls.set(requested, final);\n }\n }\n\n /** The URL the document of `requested` came from; `requested` when it was not redirected. */\n finalUrlOf(requested: string): string {\n return this.#finalUrls.get(requested) ?? requested;\n }\n}\n\n/** What every hop of a remote fetch is checked against. */\nexport interface RedirectPolicy {\n /** `resolve.http.headers` of the Redocly config; chosen again for the URL of every hop. */\n readonly headers: readonly ConfiguredHeader[];\n /** `--remote-ref-hosts`; undefined allows every host. */\n readonly hosts: readonly string[] | undefined;\n /**\n * The URL of the root document, if it is remote. The developer chose it, so neither it nor its\n * redirects are limited to the hosts (ADR-017): a spec URL that redirects to a CDN loads.\n */\n readonly rootUrl: string | undefined;\n /** Where the final URL of each redirected document is recorded. */\n readonly redirects: RedirectedUrls;\n}\n\n/**\n * Follow redirects one by one. Fetch drops only `Authorization`, `Cookie` and\n * `Proxy-Authorization` on a redirect to another origin and keeps every other header (an\n * `X-Api-Key`), so the configured headers are chosen for each hop's own URL instead, and a hop to\n * a host that is not listed is refused before it is fetched. The first URL was checked by the\n * resolver (or is the root document). Redocly passes only the configured headers in `init`, and\n * it gets an empty list of them (`resolveConfigWithAccess`), so `init.headers` is replaced.\n */\nexport function fetchWithCheckedRedirects(\n fetchImpl: RemoteFetch,\n policy: RedirectPolicy,\n): RemoteFetch {\n return async (input, init = {}) => {\n const hosts = input === policy.rootUrl ? undefined : policy.hosts;\n let url = new URL(input);\n for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {\n if (hop > 0 && hosts !== undefined && !isHostAllowed(url, hosts)) {\n throw refForbiddenError(url.href, {\n reason: \"host-not-allowed\",\n allowedHosts: [...hosts],\n });\n }\n const headers = await headersFor(url, policy.headers);\n const response = await fetchImpl(url.href, { ...init, headers, redirect: \"manual\" });\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n policy.redirects.record(input, url.href);\n return response;\n }\n url = new URL(location, url);\n }\n throw new Error(`More than ${MAX_REDIRECTS} redirects loading ${input}`);\n };\n}\n\n/** Bounds on the remote documents of one load (threat model T2, ADR-033). */\nexport interface RemoteDocumentLimits extends RemoteBodyLimits {\n /** Remote documents one load may fetch, the root included; one more is refused. */\n readonly maxDocuments: number;\n /** Remote documents fetched at the same time; the others wait for a free slot. */\n readonly maxParallelFetches: number;\n /** Nodes (keys and values) the remote documents of one load may parse into together. */\n readonly maxNodes: number;\n}\n\n/**\n * At most `limit` calls of the fetch run at once; the others wait in order. A finished call hands\n * its slot to the next waiting one.\n */\nexport function fetchWithConcurrencyLimit(fetchImpl: RemoteFetch, limit: number): RemoteFetch {\n let running = 0;\n const waiting: (() => void)[] = [];\n return async (input, init) => {\n if (running < limit) {\n running += 1;\n } else {\n await new Promise<void>((resolve) => waiting.push(resolve));\n }\n try {\n return await fetchImpl(input, init);\n } finally {\n const next = waiting.shift();\n if (next === undefined) {\n running -= 1;\n } else {\n next();\n }\n }\n };\n}\n\nexport interface RemoteFetchOptions {\n /** Time limit of one fetch and its body, unless the Redocly config has its own `customFetch`. */\n readonly timeoutMs: number;\n readonly access: RefAccess;\n /** The root document's URL, whose redirects are not limited to the hosts. */\n readonly rootUrl: string | undefined;\n readonly maxParallelFetches: number;\n readonly bodyLimits: RemoteBodyLimits;\n readonly redirects: RedirectedUrls;\n}\n\n/**\n * The resolve config with the fetch the loader uses. A `customFetch` of the Redocly config is\n * kept and wrapped, so that each redirect is checked, each body is read within the size limits\n * and only a few documents load at once. The\n * configured headers move into that wrapper: Redocly would match them against the raw URL and\n * send them on through redirects.\n */\nexport function resolveConfigWithAccess(\n resolve: ResolveConfig,\n options: RemoteFetchOptions,\n): ResolveConfig {\n // Redocly types `customFetch` as `Function`; it is called as fetch is.\n const configured = resolve.http.customFetch as RemoteFetch | undefined;\n // One limit per load, so that the total covers every document this resolver fetches.\n const bodyLimit = new RemoteBodyLimit(options.bodyLimits);\n const base =\n configured === undefined\n ? fetchWithTimeout(options.timeoutMs, bodyLimit)\n : fetchWithBodyLimit(configured, bodyLimit);\n const checked = fetchWithCheckedRedirects(base, {\n headers: resolve.http.headers,\n hosts: options.access.remoteHosts,\n rootUrl: options.rootUrl,\n redirects: options.redirects,\n });\n // The limit is outside the redirects, so all hops of one document share one slot.\n const customFetch = fetchWithConcurrencyLimit(checked, options.maxParallelFetches);\n return { ...resolve, http: { ...resolve.http, headers: [], customFetch } };\n}\n"],"names":[],"mappings":";;;;;AAqBO,MAAM,aAAA,GAAgB;AAE7B,MAAM,iBAAA,uBAAwB,GAAA,CAAI,CAAC,KAAK,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAC,CAAA;AAG3D,MAAM,kBAAA,uBAAyB,GAAA,CAAI,CAAC,KAAK,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAC,CAAA;AAM5D,eAAe,gBAAA,CACb,QAAA,EACA,GAAA,EACA,SAAA,EACmB;AACnB,EAAA,MAAM,IAAA,GAAO,MAAM,SAAA,CAAU,IAAA,CAAK,UAAU,GAAG,CAAA;AAC/C,EAAA,MAAM,EAAE,MAAA,EAAQ,UAAA,EAAY,OAAA,EAAQ,GAAI,QAAA;AACxC,EAAA,OAAO,IAAI,QAAA,CAAS,kBAAA,CAAmB,IAAI,MAAM,CAAA,GAAI,OAAO,IAAA,EAAM;AAAA,IAChE,MAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACD,CAAA;AACH;AAOA,MAAM,uBAAA,uBAAmD,GAAA,CAAI;AAAA,EAC3D,YAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAC,CAAA;AAGD,MAAM,qCAA0C,IAAI,GAAA,CAAI,CAAC,KAAA,EAAO,MAAM,CAAC,CAAA;AACvE,MAAM,cAAA,GAAiB,KAAA;AAGvB,SAAS,mBAAmB,KAAA,EAAyB;AACnD,EAAA,MAAM,KAAA,GAAQ,KAAA,YAAiB,SAAA,GAAY,KAAA,CAAM,KAAA,GAAQ,MAAA;AACzD,EAAA,MAAM,OAAO,KAAA,YAAiB,KAAA,IAAS,MAAA,IAAU,KAAA,GAAQ,MAAM,IAAA,GAAO,MAAA;AACtE,EAAA,OAAO,OAAO,IAAA,KAAS,QAAA,IAAY,uBAAA,CAAwB,IAAI,IAAI,CAAA;AACrE;AAOA,eAAe,qBAAA,CAAsB,OAAe,IAAA,EAAsC;AACxF,EAAA,IAAI;AACF,IAAA,OAAO,MAAM,KAAA,CAAM,KAAA,EAAO,IAAI,CAAA;AAAA,EAChC,SAAS,KAAA,EAAO;AACd,IAAA,MAAM,MAAA,GAAA,CAAU,IAAA,CAAK,MAAA,IAAU,cAAA,EAAgB,WAAA,EAAY;AAC3D,IAAA,IAAI,EAAE,kBAAA,CAAmB,GAAA,CAAI,MAAM,CAAA,IAAK,kBAAA,CAAmB,KAAK,CAAA,CAAA,EAAI;AAClE,MAAA,MAAM,KAAA;AAAA,IACR;AACA,IAAA,OAAO,KAAA,CAAM,OAAO,IAAI,CAAA;AAAA,EAC1B;AACF;AASA,SAAS,gBAAA,CAAiB,WAAmB,SAAA,EAAyC;AACpF,EAAA,OAAO,OAAO,KAAA,EAAO,IAAA,GAAO,EAAC,KAAM;AACjC,IAAA,IAAI;AAEF,MAAA,MAAM,MAAA,GAAS,WAAA,CAAY,OAAA,CAAQ,SAAS,CAAA;AAC5C,MAAA,MAAM,QAAA,GAAW,MAAM,qBAAA,CAAsB,KAAA,EAAO,EAAE,GAAG,IAAA,EAAM,QAAQ,CAAA;AACvE,MAAA,OAAO,MAAM,gBAAA,CAAiB,QAAA,EAAU,KAAA,EAAO,SAAS,CAAA;AAAA,IAC1D,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,SAAA,GAAY,KAAA,YAAiB,KAAA,IAAS,KAAA,CAAM,IAAA,KAAS,cAAA;AAC3D,MAAA,MAAM,SAAA,GAAY,IAAI,KAAA,CAAM,CAAA,gBAAA,EAAmB,SAAS,CAAA,YAAA,EAAe,KAAK,EAAE,CAAA,GAAI,KAAA;AAAA,IACpF;AAAA,EACF,CAAA;AACF;AAGA,SAAS,kBAAA,CAAmB,WAAwB,SAAA,EAAyC;AAC3F,EAAA,OAAO,OAAO,KAAA,EAAO,IAAA,KAAS,gBAAA,CAAiB,MAAM,UAAU,KAAA,EAAO,IAAI,CAAA,EAAG,KAAA,EAAO,SAAS,CAAA;AAC/F;AAQO,MAAM,cAAA,CAAe;AAAA,EACjB,UAAA,uBAAiB,GAAA,EAAoB;AAAA,EAE9C,MAAA,CAAO,WAAmB,KAAA,EAAqB;AAC7C,IAAA,IAAI,cAAc,KAAA,EAAO;AACvB,MAAA,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,SAAA,EAAW,KAAK,CAAA;AAAA,IACtC;AAAA,EACF;AAAA;AAAA,EAGA,WAAW,SAAA,EAA2B;AACpC,IAAA,OAAO,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,SAAS,CAAA,IAAK,SAAA;AAAA,EAC3C;AACF;AAyBO,SAAS,yBAAA,CACd,WACA,MAAA,EACa;AACb,EAAA,OAAO,OAAO,KAAA,EAAO,IAAA,GAAO,EAAC,KAAM;AACjC,IAAA,MAAM,KAAA,GAAQ,KAAA,KAAU,MAAA,CAAO,OAAA,GAAU,SAAY,MAAA,CAAO,KAAA;AAC5D,IAAA,IAAI,GAAA,GAAM,IAAI,GAAA,CAAI,KAAK,CAAA;AACvB,IAAA,KAAA,IAAS,GAAA,GAAM,CAAA,EAAG,GAAA,IAAO,aAAA,EAAe,OAAO,CAAA,EAAG;AAChD,MAAA,IAAI,GAAA,GAAM,KAAK,KAAA,KAAU,MAAA,IAAa,CAAC,aAAA,CAAc,GAAA,EAAK,KAAK,CAAA,EAAG;AAChE,QAAA,MAAM,iBAAA,CAAkB,IAAI,IAAA,EAAM;AAAA,UAChC,MAAA,EAAQ,kBAAA;AAAA,UACR,YAAA,EAAc,CAAC,GAAG,KAAK;AAAA,SACxB,CAAA;AAAA,MACH;AACA,MAAA,MAAM,OAAA,GAAU,MAAM,UAAA,CAAW,GAAA,EAAK,OAAO,OAAO,CAAA;AACpD,MAAA,MAAM,QAAA,GAAW,MAAM,SAAA,CAAU,GAAA,CAAI,IAAA,EAAM,EAAE,GAAG,IAAA,EAAM,OAAA,EAAS,QAAA,EAAU,QAAA,EAAU,CAAA;AACnF,MAAA,MAAM,QAAA,GAAW,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA;AAChD,MAAA,IAAI,CAAC,iBAAA,CAAkB,GAAA,CAAI,SAAS,MAAM,CAAA,IAAK,aAAa,IAAA,EAAM;AAChE,QAAA,MAAA,CAAO,SAAA,CAAU,MAAA,CAAO,KAAA,EAAO,GAAA,CAAI,IAAI,CAAA;AACvC,QAAA,OAAO,QAAA;AAAA,MACT;AACA,MAAA,GAAA,GAAM,IAAI,GAAA,CAAI,QAAA,EAAU,GAAG,CAAA;AAAA,IAC7B;AACA,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,UAAA,EAAa,aAAa,CAAA,mBAAA,EAAsB,KAAK,CAAA,CAAE,CAAA;AAAA,EACzE,CAAA;AACF;AAgBO,SAAS,yBAAA,CAA0B,WAAwB,KAAA,EAA4B;AAC5F,EAAA,IAAI,OAAA,GAAU,CAAA;AACd,EAAA,MAAM,UAA0B,EAAC;AACjC,EAAA,OAAO,OAAO,OAAO,IAAA,KAAS;AAC5B,IAAA,IAAI,UAAU,KAAA,EAAO;AACnB,MAAA,OAAA,IAAW,CAAA;AAAA,IACb,CAAA,MAAO;AACL,MAAA,MAAM,IAAI,OAAA,CAAc,CAAC,YAAY,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAC,CAAA;AAAA,IAC5D;AACA,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,SAAA,CAAU,KAAA,EAAO,IAAI,CAAA;AAAA,IACpC,CAAA,SAAE;AACA,MAAA,MAAM,IAAA,GAAO,QAAQ,KAAA,EAAM;AAC3B,MAAA,IAAI,SAAS,MAAA,EAAW;AACtB,QAAA,OAAA,IAAW,CAAA;AAAA,MACb,CAAA,MAAO;AACL,QAAA,IAAA,EAAK;AAAA,MACP;AAAA,IACF;AAAA,EACF,CAAA;AACF;AAoBO,SAAS,uBAAA,CACd,SACA,OAAA,EACe;AAEf,EAAA,MAAM,UAAA,GAAa,QAAQ,IAAA,CAAK,WAAA;AAEhC,EAAA,MAAM,SAAA,GAAY,IAAI,eAAA,CAAgB,OAAA,CAAQ,UAAU,CAAA;AACxD,EAAA,MAAM,IAAA,GACJ,UAAA,KAAe,MAAA,GACX,gBAAA,CAAiB,OAAA,CAAQ,WAAW,SAAS,CAAA,GAC7C,kBAAA,CAAmB,UAAA,EAAY,SAAS,CAAA;AAC9C,EAAA,MAAM,OAAA,GAAU,0BAA0B,IAAA,EAAM;AAAA,IAC9C,OAAA,EAAS,QAAQ,IAAA,CAAK,OAAA;AAAA,IACtB,KAAA,EAAO,QAAQ,MAAA,CAAO,WAAA;AAAA,IACtB,SAAS,OAAA,CAAQ,OAAA;AAAA,IACjB,WAAW,OAAA,CAAQ;AAAA,GACpB,CAAA;AAED,EAAA,MAAM,WAAA,GAAc,yBAAA,CAA0B,OAAA,EAAS,OAAA,CAAQ,kBAAkB,CAAA;AACjF,EAAA,OAAO,EAAE,GAAG,OAAA,EAAS,IAAA,EAAM,EAAE,GAAG,OAAA,CAAQ,IAAA,EAAM,OAAA,EAAS,EAAC,EAAG,WAAA,EAAY,EAAE;AAC3E;;"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const node_url = require('node:url');
|
|
5
|
+
const locationText = require('../domain/location-text.cjs');
|
|
6
|
+
const defaultPorts = require('./default-ports.cjs');
|
|
7
|
+
const optionErrors = require('./option-errors.cjs');
|
|
8
|
+
|
|
9
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
10
|
+
|
|
11
|
+
const path__default = /*#__PURE__*/_interopDefaultCompat(path);
|
|
12
|
+
|
|
13
|
+
const FILE_PROTOCOL = "file:";
|
|
14
|
+
const CWD_PROTOCOLS = /* @__PURE__ */ new Set([FILE_PROTOCOL, "http:", "https:"]);
|
|
15
|
+
const CWD_EXPECTED = "a directory path, or a file:, http: or https: URL";
|
|
16
|
+
function pathDirectoryUrl(directory) {
|
|
17
|
+
return node_url.pathToFileURL(path__default.join(path__default.resolve(directory), path__default.sep));
|
|
18
|
+
}
|
|
19
|
+
function cwdUrlOf(value) {
|
|
20
|
+
if (value === void 0) {
|
|
21
|
+
return pathDirectoryUrl(defaultPorts.processCwd());
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
if (!locationText.hasUrlScheme(value)) {
|
|
25
|
+
return pathDirectoryUrl(value);
|
|
26
|
+
}
|
|
27
|
+
return URL.parse(value, pathDirectoryUrl(defaultPorts.processCwd()).href) ?? void 0;
|
|
28
|
+
}
|
|
29
|
+
if (Buffer.isBuffer(value)) {
|
|
30
|
+
return pathDirectoryUrl(value.toString());
|
|
31
|
+
}
|
|
32
|
+
return value instanceof URL ? value : void 0;
|
|
33
|
+
}
|
|
34
|
+
function isLocalPath(url) {
|
|
35
|
+
try {
|
|
36
|
+
node_url.fileURLToPath(url);
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function cwdKindOf(value, url) {
|
|
43
|
+
if (url?.protocol === FILE_PROTOCOL) {
|
|
44
|
+
return "a file: URL that names no local path";
|
|
45
|
+
}
|
|
46
|
+
return url === void 0 && typeof value === "string" ? "a string that is no valid URL" : optionErrors.kindOf(url ?? value);
|
|
47
|
+
}
|
|
48
|
+
function sourceCwdOf(options) {
|
|
49
|
+
const url = cwdUrlOf(options.cwd);
|
|
50
|
+
const isUsable = url !== void 0 && CWD_PROTOCOLS.has(url.protocol) && (url.protocol !== FILE_PROTOCOL || isLocalPath(url));
|
|
51
|
+
if (!isUsable) {
|
|
52
|
+
throw optionErrors.optionInvalid("cwd", CWD_EXPECTED, cwdKindOf(options.cwd, url));
|
|
53
|
+
}
|
|
54
|
+
return url.pathname.endsWith("/") ? url : new URL(`${url.pathname}/`, url);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
exports.sourceCwdOf = sourceCwdOf;
|
|
58
|
+
//# sourceMappingURL=cwd-option.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cwd-option.cjs","sources":["../../src/api/cwd-option.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { hasUrlScheme } from \"../domain/location-text.js\";\nimport type { GeneratorOptions } from \"../types.js\";\nimport { processCwd } from \"./default-ports.js\";\nimport { kindOf, optionInvalid } from \"./option-errors.js\";\n\n/**\n * The `cwd` option: the directory that relative `$ref`s of an inline source resolve against\n * (ADR-023). A string is read as `localRefRoot` is: a URL when it has a scheme, else a path.\n */\n\nconst FILE_PROTOCOL = \"file:\";\n\n/** A `file:` cwd is a local directory; an `http:` or `https:` one holds remote documents. */\nconst CWD_PROTOCOLS: ReadonlySet<string> = new Set([FILE_PROTOCOL, \"http:\", \"https:\"]);\n\nconst CWD_EXPECTED = \"a directory path, or a file:, http: or https: URL\";\n\n/** A path, resolved against the working directory, as a directory URL; `#` and `%` are encoded. */\nfunction pathDirectoryUrl(directory: string): URL {\n return pathToFileURL(path.join(path.resolve(directory), path.sep));\n}\n\n/** The URL the value names, before its scheme is checked; undefined for another type. */\nfunction cwdUrlOf(value: unknown): URL | undefined {\n if (value === undefined) {\n return pathDirectoryUrl(processCwd());\n }\n if (typeof value === \"string\") {\n if (!hasUrlScheme(value)) {\n return pathDirectoryUrl(value);\n }\n return URL.parse(value, pathDirectoryUrl(processCwd()).href) ?? undefined;\n }\n if (Buffer.isBuffer(value)) {\n return pathDirectoryUrl(value.toString());\n }\n return value instanceof URL ? value : undefined;\n}\n\n/** A `file:` URL with a host, or with an encoded slash, names no path on this platform. */\nfunction isLocalPath(url: URL): boolean {\n try {\n fileURLToPath(url);\n return true;\n } catch {\n return false;\n }\n}\n\n/** What the cwd is instead of a directory, for the error. */\nfunction cwdKindOf(value: unknown, url: URL | undefined): string {\n if (url?.protocol === FILE_PROTOCOL) {\n return \"a file: URL that names no local path\";\n }\n return url === undefined && typeof value === \"string\"\n ? \"a string that is no valid URL\"\n : kindOf(url ?? value);\n}\n\n/**\n * The cwd as a URL that ends in a slash. Fails with `OPTION_INVALID` for another type, a URL of\n * another scheme or a `file:` URL that names no local path.\n */\nexport function sourceCwdOf(options: GeneratorOptions): URL {\n const url = cwdUrlOf(options.cwd);\n const isUsable =\n url !== undefined &&\n CWD_PROTOCOLS.has(url.protocol) &&\n (url.protocol !== FILE_PROTOCOL || isLocalPath(url));\n if (!isUsable) {\n throw optionInvalid(\"cwd\", CWD_EXPECTED, cwdKindOf(options.cwd, url));\n }\n return url.pathname.endsWith(\"/\") ? url : new URL(`${url.pathname}/`, url);\n}\n"],"names":["pathToFileURL","path","processCwd","hasUrlScheme","fileURLToPath","kindOf","optionInvalid"],"mappings":";;;;;;;;;;;;AAYA,MAAM,aAAA,GAAgB,OAAA;AAGtB,MAAM,gCAAqC,IAAI,GAAA,CAAI,CAAC,aAAA,EAAe,OAAA,EAAS,QAAQ,CAAC,CAAA;AAErF,MAAM,YAAA,GAAe,mDAAA;AAGrB,SAAS,iBAAiB,SAAA,EAAwB;AAChD,EAAA,OAAOA,sBAAA,CAAcC,cAAK,IAAA,CAAKA,aAAA,CAAK,QAAQ,SAAS,CAAA,EAAGA,aAAA,CAAK,GAAG,CAAC,CAAA;AACnE;AAGA,SAAS,SAAS,KAAA,EAAiC;AACjD,EAAA,IAAI,UAAU,MAAA,EAAW;AACvB,IAAA,OAAO,gBAAA,CAAiBC,yBAAY,CAAA;AAAA,EACtC;AACA,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,IAAI,CAACC,yBAAA,CAAa,KAAK,CAAA,EAAG;AACxB,MAAA,OAAO,iBAAiB,KAAK,CAAA;AAAA,IAC/B;AACA,IAAA,OAAO,GAAA,CAAI,MAAM,KAAA,EAAO,gBAAA,CAAiBD,yBAAY,CAAA,CAAE,IAAI,CAAA,IAAK,MAAA;AAAA,EAClE;AACA,EAAA,IAAI,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,EAAG;AAC1B,IAAA,OAAO,gBAAA,CAAiB,KAAA,CAAM,QAAA,EAAU,CAAA;AAAA,EAC1C;AACA,EAAA,OAAO,KAAA,YAAiB,MAAM,KAAA,GAAQ,MAAA;AACxC;AAGA,SAAS,YAAY,GAAA,EAAmB;AACtC,EAAA,IAAI;AACF,IAAAE,sBAAA,CAAc,GAAG,CAAA;AACjB,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAGA,SAAS,SAAA,CAAU,OAAgB,GAAA,EAA8B;AAC/D,EAAA,IAAI,GAAA,EAAK,aAAa,aAAA,EAAe;AACnC,IAAA,OAAO,sCAAA;AAAA,EACT;AACA,EAAA,OAAO,GAAA,KAAQ,UAAa,OAAO,KAAA,KAAU,WACzC,+BAAA,GACAC,mBAAA,CAAO,OAAO,KAAK,CAAA;AACzB;AAMO,SAAS,YAAY,OAAA,EAAgC;AAC1D,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,OAAA,CAAQ,GAAG,CAAA;AAChC,EAAA,MAAM,QAAA,GACJ,GAAA,KAAQ,MAAA,IACR,aAAA,CAAc,GAAA,CAAI,GAAA,CAAI,QAAQ,CAAA,KAC7B,GAAA,CAAI,QAAA,KAAa,aAAA,IAAiB,WAAA,CAAY,GAAG,CAAA,CAAA;AACpD,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,MAAMC,2BAAc,KAAA,EAAO,YAAA,EAAc,UAAU,OAAA,CAAQ,GAAA,EAAK,GAAG,CAAC,CAAA;AAAA,EACtE;AACA,EAAA,OAAO,GAAA,CAAI,QAAA,CAAS,QAAA,CAAS,GAAG,CAAA,GAAI,GAAA,GAAM,IAAI,GAAA,CAAI,CAAA,EAAG,GAAA,CAAI,QAAQ,CAAA,CAAA,CAAA,EAAK,GAAG,CAAA;AAC3E;;"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
|
+
import { hasUrlScheme } from '../domain/location-text.mjs';
|
|
4
|
+
import { processCwd } from './default-ports.mjs';
|
|
5
|
+
import { optionInvalid, kindOf } from './option-errors.mjs';
|
|
6
|
+
|
|
7
|
+
const FILE_PROTOCOL = "file:";
|
|
8
|
+
const CWD_PROTOCOLS = /* @__PURE__ */ new Set([FILE_PROTOCOL, "http:", "https:"]);
|
|
9
|
+
const CWD_EXPECTED = "a directory path, or a file:, http: or https: URL";
|
|
10
|
+
function pathDirectoryUrl(directory) {
|
|
11
|
+
return pathToFileURL(path.join(path.resolve(directory), path.sep));
|
|
12
|
+
}
|
|
13
|
+
function cwdUrlOf(value) {
|
|
14
|
+
if (value === void 0) {
|
|
15
|
+
return pathDirectoryUrl(processCwd());
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === "string") {
|
|
18
|
+
if (!hasUrlScheme(value)) {
|
|
19
|
+
return pathDirectoryUrl(value);
|
|
20
|
+
}
|
|
21
|
+
return URL.parse(value, pathDirectoryUrl(processCwd()).href) ?? void 0;
|
|
22
|
+
}
|
|
23
|
+
if (Buffer.isBuffer(value)) {
|
|
24
|
+
return pathDirectoryUrl(value.toString());
|
|
25
|
+
}
|
|
26
|
+
return value instanceof URL ? value : void 0;
|
|
27
|
+
}
|
|
28
|
+
function isLocalPath(url) {
|
|
29
|
+
try {
|
|
30
|
+
fileURLToPath(url);
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function cwdKindOf(value, url) {
|
|
37
|
+
if (url?.protocol === FILE_PROTOCOL) {
|
|
38
|
+
return "a file: URL that names no local path";
|
|
39
|
+
}
|
|
40
|
+
return url === void 0 && typeof value === "string" ? "a string that is no valid URL" : kindOf(url ?? value);
|
|
41
|
+
}
|
|
42
|
+
function sourceCwdOf(options) {
|
|
43
|
+
const url = cwdUrlOf(options.cwd);
|
|
44
|
+
const isUsable = url !== void 0 && CWD_PROTOCOLS.has(url.protocol) && (url.protocol !== FILE_PROTOCOL || isLocalPath(url));
|
|
45
|
+
if (!isUsable) {
|
|
46
|
+
throw optionInvalid("cwd", CWD_EXPECTED, cwdKindOf(options.cwd, url));
|
|
47
|
+
}
|
|
48
|
+
return url.pathname.endsWith("/") ? url : new URL(`${url.pathname}/`, url);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { sourceCwdOf };
|
|
52
|
+
//# sourceMappingURL=cwd-option.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cwd-option.mjs","sources":["../../src/api/cwd-option.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { hasUrlScheme } from \"../domain/location-text.js\";\nimport type { GeneratorOptions } from \"../types.js\";\nimport { processCwd } from \"./default-ports.js\";\nimport { kindOf, optionInvalid } from \"./option-errors.js\";\n\n/**\n * The `cwd` option: the directory that relative `$ref`s of an inline source resolve against\n * (ADR-023). A string is read as `localRefRoot` is: a URL when it has a scheme, else a path.\n */\n\nconst FILE_PROTOCOL = \"file:\";\n\n/** A `file:` cwd is a local directory; an `http:` or `https:` one holds remote documents. */\nconst CWD_PROTOCOLS: ReadonlySet<string> = new Set([FILE_PROTOCOL, \"http:\", \"https:\"]);\n\nconst CWD_EXPECTED = \"a directory path, or a file:, http: or https: URL\";\n\n/** A path, resolved against the working directory, as a directory URL; `#` and `%` are encoded. */\nfunction pathDirectoryUrl(directory: string): URL {\n return pathToFileURL(path.join(path.resolve(directory), path.sep));\n}\n\n/** The URL the value names, before its scheme is checked; undefined for another type. */\nfunction cwdUrlOf(value: unknown): URL | undefined {\n if (value === undefined) {\n return pathDirectoryUrl(processCwd());\n }\n if (typeof value === \"string\") {\n if (!hasUrlScheme(value)) {\n return pathDirectoryUrl(value);\n }\n return URL.parse(value, pathDirectoryUrl(processCwd()).href) ?? undefined;\n }\n if (Buffer.isBuffer(value)) {\n return pathDirectoryUrl(value.toString());\n }\n return value instanceof URL ? value : undefined;\n}\n\n/** A `file:` URL with a host, or with an encoded slash, names no path on this platform. */\nfunction isLocalPath(url: URL): boolean {\n try {\n fileURLToPath(url);\n return true;\n } catch {\n return false;\n }\n}\n\n/** What the cwd is instead of a directory, for the error. */\nfunction cwdKindOf(value: unknown, url: URL | undefined): string {\n if (url?.protocol === FILE_PROTOCOL) {\n return \"a file: URL that names no local path\";\n }\n return url === undefined && typeof value === \"string\"\n ? \"a string that is no valid URL\"\n : kindOf(url ?? value);\n}\n\n/**\n * The cwd as a URL that ends in a slash. Fails with `OPTION_INVALID` for another type, a URL of\n * another scheme or a `file:` URL that names no local path.\n */\nexport function sourceCwdOf(options: GeneratorOptions): URL {\n const url = cwdUrlOf(options.cwd);\n const isUsable =\n url !== undefined &&\n CWD_PROTOCOLS.has(url.protocol) &&\n (url.protocol !== FILE_PROTOCOL || isLocalPath(url));\n if (!isUsable) {\n throw optionInvalid(\"cwd\", CWD_EXPECTED, cwdKindOf(options.cwd, url));\n }\n return url.pathname.endsWith(\"/\") ? url : new URL(`${url.pathname}/`, url);\n}\n"],"names":[],"mappings":";;;;;;AAYA,MAAM,aAAA,GAAgB,OAAA;AAGtB,MAAM,gCAAqC,IAAI,GAAA,CAAI,CAAC,aAAA,EAAe,OAAA,EAAS,QAAQ,CAAC,CAAA;AAErF,MAAM,YAAA,GAAe,mDAAA;AAGrB,SAAS,iBAAiB,SAAA,EAAwB;AAChD,EAAA,OAAO,aAAA,CAAc,KAAK,IAAA,CAAK,IAAA,CAAK,QAAQ,SAAS,CAAA,EAAG,IAAA,CAAK,GAAG,CAAC,CAAA;AACnE;AAGA,SAAS,SAAS,KAAA,EAAiC;AACjD,EAAA,IAAI,UAAU,MAAA,EAAW;AACvB,IAAA,OAAO,gBAAA,CAAiB,YAAY,CAAA;AAAA,EACtC;AACA,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,IAAI,CAAC,YAAA,CAAa,KAAK,CAAA,EAAG;AACxB,MAAA,OAAO,iBAAiB,KAAK,CAAA;AAAA,IAC/B;AACA,IAAA,OAAO,GAAA,CAAI,MAAM,KAAA,EAAO,gBAAA,CAAiB,YAAY,CAAA,CAAE,IAAI,CAAA,IAAK,MAAA;AAAA,EAClE;AACA,EAAA,IAAI,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,EAAG;AAC1B,IAAA,OAAO,gBAAA,CAAiB,KAAA,CAAM,QAAA,EAAU,CAAA;AAAA,EAC1C;AACA,EAAA,OAAO,KAAA,YAAiB,MAAM,KAAA,GAAQ,MAAA;AACxC;AAGA,SAAS,YAAY,GAAA,EAAmB;AACtC,EAAA,IAAI;AACF,IAAA,aAAA,CAAc,GAAG,CAAA;AACjB,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAGA,SAAS,SAAA,CAAU,OAAgB,GAAA,EAA8B;AAC/D,EAAA,IAAI,GAAA,EAAK,aAAa,aAAA,EAAe;AACnC,IAAA,OAAO,sCAAA;AAAA,EACT;AACA,EAAA,OAAO,GAAA,KAAQ,UAAa,OAAO,KAAA,KAAU,WACzC,+BAAA,GACA,MAAA,CAAO,OAAO,KAAK,CAAA;AACzB;AAMO,SAAS,YAAY,OAAA,EAAgC;AAC1D,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,OAAA,CAAQ,GAAG,CAAA;AAChC,EAAA,MAAM,QAAA,GACJ,GAAA,KAAQ,MAAA,IACR,aAAA,CAAc,GAAA,CAAI,GAAA,CAAI,QAAQ,CAAA,KAC7B,GAAA,CAAI,QAAA,KAAa,aAAA,IAAiB,WAAA,CAAY,GAAG,CAAA,CAAA;AACpD,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,MAAM,cAAc,KAAA,EAAO,YAAA,EAAc,UAAU,OAAA,CAAQ,GAAA,EAAK,GAAG,CAAC,CAAA;AAAA,EACtE;AACA,EAAA,OAAO,GAAA,CAAI,QAAA,CAAS,QAAA,CAAS,GAAG,CAAA,GAAI,GAAA,GAAM,IAAI,GAAA,CAAI,CAAA,EAAG,GAAA,CAAI,QAAQ,CAAA,CAAA,CAAA,EAAK,GAAG,CAAA;AAC3E;;"}
|
|
@@ -10,9 +10,12 @@ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'defau
|
|
|
10
10
|
|
|
11
11
|
const supportsColor__default = /*#__PURE__*/_interopDefaultCompat(supportsColor);
|
|
12
12
|
|
|
13
|
-
function
|
|
14
|
-
const
|
|
15
|
-
return
|
|
13
|
+
function streamSupportsColor(stream, support) {
|
|
14
|
+
const info = support[stream];
|
|
15
|
+
return info !== false && info.hasBasic;
|
|
16
|
+
}
|
|
17
|
+
function terminalSupportsColor(stream) {
|
|
18
|
+
return streamSupportsColor(stream, supportsColor__default);
|
|
16
19
|
}
|
|
17
20
|
const STDERR_SINK = {
|
|
18
21
|
// biome-ignore lint/suspicious/noConsole: the terminal logger's own channel
|
|
@@ -22,13 +25,16 @@ const STDERR_SINK = {
|
|
|
22
25
|
// biome-ignore lint/suspicious/noConsole: the terminal logger's own channel
|
|
23
26
|
error: (line) => console.error(line)
|
|
24
27
|
};
|
|
25
|
-
function
|
|
26
|
-
return
|
|
28
|
+
function defaultLoggerOptions(silent, support) {
|
|
29
|
+
return {
|
|
27
30
|
silent,
|
|
28
31
|
debugFilter: process.env.DEBUG,
|
|
29
|
-
useColors:
|
|
32
|
+
useColors: streamSupportsColor("stderr", support),
|
|
30
33
|
sink: STDERR_SINK
|
|
31
|
-
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function defaultLogger(silent) {
|
|
37
|
+
return terminalLogger.createTerminalLogger(defaultLoggerOptions(silent, supportsColor__default));
|
|
32
38
|
}
|
|
33
39
|
function defaultDeprecationReporter() {
|
|
34
40
|
return nodeDeprecations.createNodeDeprecationReporter();
|
|
@@ -57,8 +63,10 @@ exports.DEFAULT_REMOTE_DOCUMENT_LIMITS = DEFAULT_REMOTE_DOCUMENT_LIMITS;
|
|
|
57
63
|
exports.REMOTE_REF_TIMEOUT_MS = REMOTE_REF_TIMEOUT_MS;
|
|
58
64
|
exports.defaultDeprecationReporter = defaultDeprecationReporter;
|
|
59
65
|
exports.defaultLogger = defaultLogger;
|
|
66
|
+
exports.defaultLoggerOptions = defaultLoggerOptions;
|
|
60
67
|
exports.defaultRedoclyConfig = defaultRedoclyConfig;
|
|
61
68
|
exports.monotonicNow = monotonicNow;
|
|
62
69
|
exports.processCwd = processCwd;
|
|
70
|
+
exports.streamSupportsColor = streamSupportsColor;
|
|
63
71
|
exports.terminalSupportsColor = terminalSupportsColor;
|
|
64
72
|
//# sourceMappingURL=default-ports.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default-ports.cjs","sources":["../../src/api/default-ports.ts"],"sourcesContent":["import { performance } from \"node:perf_hooks\";\nimport { createConfig, type Config as RedoclyConfig } from \"@redocly/openapi-core\";\nimport supportsColor from \"supports-color\";\nimport { createNodeDeprecationReporter } from \"../adapters/node-deprecations/node-deprecations.js\";\nimport type { RemoteDocumentLimits } from \"../adapters/redocly-loader/remote-fetch.js\";\nimport {\n createTerminalLogger,\n type TerminalSink,\n} from \"../adapters/terminal-logger/terminal-logger.js\";\nimport type { DeprecationReporter } from \"../application/ports/deprecation-reporter.js\";\nimport type { Logger } from \"../application/ports/logger.js\";\n\n/**\n * The default adapters of the Node API (ENGINEERING_PLAN 4.5): this module and the CLI's\n * composition root are where the process environment is read.\n */\n\n/**
|
|
1
|
+
{"version":3,"file":"default-ports.cjs","sources":["../../src/api/default-ports.ts"],"sourcesContent":["import { performance } from \"node:perf_hooks\";\nimport { createConfig, type Config as RedoclyConfig } from \"@redocly/openapi-core\";\nimport supportsColor, { type ColorInfo } from \"supports-color\";\nimport { createNodeDeprecationReporter } from \"../adapters/node-deprecations/node-deprecations.js\";\nimport type { RemoteDocumentLimits } from \"../adapters/redocly-loader/remote-fetch.js\";\nimport {\n createTerminalLogger,\n type TerminalLoggerOptions,\n type TerminalSink,\n} from \"../adapters/terminal-logger/terminal-logger.js\";\nimport type { DeprecationReporter } from \"../application/ports/deprecation-reporter.js\";\nimport type { Logger } from \"../application/ports/logger.js\";\n\n/**\n * The default adapters of the Node API (ENGINEERING_PLAN 4.5): this module and the CLI's\n * composition root are where the process environment is read.\n */\n\n/** The output streams of the process, which can each be a terminal or redirected. */\nexport type TerminalStream = \"stdout\" | \"stderr\";\n\n/** The color support of each stream, as `supports-color` detects it. */\nexport type StreamColorSupport = Readonly<Record<TerminalStream, ColorInfo>>;\n\n/**\n * Whether `stream` shows colors. Each stream is asked for itself: with stdout on a terminal and\n * stderr redirected to a log, the log gets no escape sequences.\n */\nexport function streamSupportsColor(stream: TerminalStream, support: StreamColorSupport): boolean {\n const info = support[stream];\n return info !== false && info.hasBasic;\n}\n\n/** Colors when the terminal of `stream` supports them, as the original decided. */\nexport function terminalSupportsColor(stream: TerminalStream): boolean {\n return streamSupportsColor(stream, supportsColor);\n}\n\n/**\n * Every line of the logger goes to stderr: the CLI writes the types to stdout, and `console.debug`\n * writes there too, so debug lines would end up inside the generated file.\n */\nconst STDERR_SINK: TerminalSink = {\n // biome-ignore lint/suspicious/noConsole: the terminal logger's own channel\n debug: (line) => console.error(line),\n // biome-ignore lint/suspicious/noConsole: the terminal logger's own channel\n warn: (line) => console.warn(line),\n // biome-ignore lint/suspicious/noConsole: the terminal logger's own channel\n error: (line) => console.error(line),\n};\n\n/** The options of the default logger: it writes to stderr, so the colors follow stderr. */\nexport function defaultLoggerOptions(\n silent: boolean,\n support: StreamColorSupport,\n): TerminalLoggerOptions {\n return {\n silent,\n debugFilter: process.env.DEBUG,\n useColors: streamSupportsColor(\"stderr\", support),\n sink: STDERR_SINK,\n };\n}\n\n/** The terminal logger with `DEBUG` from the environment, writing to stderr. */\nexport function defaultLogger(silent: boolean): Logger {\n return createTerminalLogger(defaultLoggerOptions(silent, supportsColor));\n}\n\n/** Deprecations as Node.js `DeprecationWarning`s, each code once per process. */\nexport function defaultDeprecationReporter(): DeprecationReporter {\n return createNodeDeprecationReporter();\n}\n\n/**\n * How long one remote `$ref` (or a remote source) may take to load. Large documents (the 27 MB\n * Cloudflare spec) download in seconds; a minute covers slow hosts, while Node's own fetch would\n * wait five minutes for a server that never answers (ENGINEERING_PLAN 8.5).\n */\nexport const REMOTE_REF_TIMEOUT_MS = 60_000;\n\nconst MEBIBYTE = 1024 * 1024;\n\n/**\n * Bounds on the remote documents of one schema (ADR-033). Large split documents have a few thousand\n * files, so 10,000 leaves room and still ends an endless fan-out. 16 parallel fetches keep a\n * multi-file document fast without exhausting local ports.\n *\n * The size limits count decoded bytes and parsed nodes (threat model T2, ADR-033 decision 6).\n * The largest document of the 1,820-entry compatibility corpus is Microsoft Graph beta: 71 MiB,\n * 2.65 million nodes (keys and values), 1.6 GB peak memory when loaded from a URL. A remote body\n * took about 7 bytes of memory per byte of text and 110 per node in our measurements, so 128 MiB\n * per document (1.8 times Graph beta), 256 MiB per schema (a split document may be twice as\n * large) and 8 million nodes per schema (3 times Graph beta) let crafted documents finish within\n * a heap of 2 GB; with the default heap of 4 GB they peak at about 1.2 to 2.3 GB.\n */\nexport const DEFAULT_REMOTE_DOCUMENT_LIMITS: RemoteDocumentLimits = {\n maxDocuments: 10_000,\n maxParallelFetches: 16,\n maxDocumentBytes: 128 * MEBIBYTE,\n maxTotalBytes: 256 * MEBIBYTE,\n maxNodes: 8_000_000,\n};\n\n/** The working directory of the process: the default `cwd` of an inline source. */\nexport function processCwd(): string {\n return process.cwd();\n}\n\n/** A monotonic clock in milliseconds, for debug durations. */\nexport function monotonicNow(): number {\n return performance.now();\n}\n\n/** The default Redocly config: minimal rules, and duplicate operation IDs are an error. */\nexport function defaultRedoclyConfig(): Promise<RedoclyConfig> {\n const rules = { \"operation-operationId-unique\": { severity: \"error\" as const } };\n return createConfig({ rules }, { extends: [\"minimal\"] });\n}\n"],"names":["supportsColor","createTerminalLogger","createNodeDeprecationReporter","performance","createConfig"],"mappings":";;;;;;;;;;;;AA4BO,SAAS,mBAAA,CAAoB,QAAwB,OAAA,EAAsC;AAChG,EAAA,MAAM,IAAA,GAAO,QAAQ,MAAM,CAAA;AAC3B,EAAA,OAAO,IAAA,KAAS,SAAS,IAAA,CAAK,QAAA;AAChC;AAGO,SAAS,sBAAsB,MAAA,EAAiC;AACrE,EAAA,OAAO,mBAAA,CAAoB,QAAQA,sBAAa,CAAA;AAClD;AAMA,MAAM,WAAA,GAA4B;AAAA;AAAA,EAEhC,KAAA,EAAO,CAAC,IAAA,KAAS,OAAA,CAAQ,MAAM,IAAI,CAAA;AAAA;AAAA,EAEnC,IAAA,EAAM,CAAC,IAAA,KAAS,OAAA,CAAQ,KAAK,IAAI,CAAA;AAAA;AAAA,EAEjC,KAAA,EAAO,CAAC,IAAA,KAAS,OAAA,CAAQ,MAAM,IAAI;AACrC,CAAA;AAGO,SAAS,oBAAA,CACd,QACA,OAAA,EACuB;AACvB,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,WAAA,EAAa,QAAQ,GAAA,CAAI,KAAA;AAAA,IACzB,SAAA,EAAW,mBAAA,CAAoB,QAAA,EAAU,OAAO,CAAA;AAAA,IAChD,IAAA,EAAM;AAAA,GACR;AACF;AAGO,SAAS,cAAc,MAAA,EAAyB;AACrD,EAAA,OAAOC,mCAAA,CAAqB,oBAAA,CAAqB,MAAA,EAAQD,sBAAa,CAAC,CAAA;AACzE;AAGO,SAAS,0BAAA,GAAkD;AAChE,EAAA,OAAOE,8CAAA,EAA8B;AACvC;AAOO,MAAM,qBAAA,GAAwB;AAErC,MAAM,WAAW,IAAA,GAAO,IAAA;AAejB,MAAM,8BAAA,GAAuD;AAAA,EAClE,YAAA,EAAc,GAAA;AAAA,EACd,kBAAA,EAAoB,EAAA;AAAA,EACpB,kBAAkB,GAAA,GAAM,QAAA;AAAA,EACxB,eAAe,GAAA,GAAM,QAAA;AAAA,EACrB,QAAA,EAAU;AACZ;AAGO,SAAS,UAAA,GAAqB;AACnC,EAAA,OAAO,QAAQ,GAAA,EAAI;AACrB;AAGO,SAAS,YAAA,GAAuB;AACrC,EAAA,OAAOC,4BAAY,GAAA,EAAI;AACzB;AAGO,SAAS,oBAAA,GAA+C;AAC7D,EAAA,MAAM,QAAQ,EAAE,8BAAA,EAAgC,EAAE,QAAA,EAAU,SAAiB,EAAE;AAC/E,EAAA,OAAOC,wBAAA,CAAa,EAAE,KAAA,EAAM,EAAG,EAAE,OAAA,EAAS,CAAC,SAAS,CAAA,EAAG,CAAA;AACzD;;;;;;;;;;;"}
|
|
@@ -4,9 +4,12 @@ import supportsColor from 'supports-color';
|
|
|
4
4
|
import { createNodeDeprecationReporter } from '../adapters/node-deprecations/node-deprecations.mjs';
|
|
5
5
|
import { createTerminalLogger } from '../adapters/terminal-logger/terminal-logger.mjs';
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
const
|
|
9
|
-
return
|
|
7
|
+
function streamSupportsColor(stream, support) {
|
|
8
|
+
const info = support[stream];
|
|
9
|
+
return info !== false && info.hasBasic;
|
|
10
|
+
}
|
|
11
|
+
function terminalSupportsColor(stream) {
|
|
12
|
+
return streamSupportsColor(stream, supportsColor);
|
|
10
13
|
}
|
|
11
14
|
const STDERR_SINK = {
|
|
12
15
|
// biome-ignore lint/suspicious/noConsole: the terminal logger's own channel
|
|
@@ -16,13 +19,16 @@ const STDERR_SINK = {
|
|
|
16
19
|
// biome-ignore lint/suspicious/noConsole: the terminal logger's own channel
|
|
17
20
|
error: (line) => console.error(line)
|
|
18
21
|
};
|
|
19
|
-
function
|
|
20
|
-
return
|
|
22
|
+
function defaultLoggerOptions(silent, support) {
|
|
23
|
+
return {
|
|
21
24
|
silent,
|
|
22
25
|
debugFilter: process.env.DEBUG,
|
|
23
|
-
useColors:
|
|
26
|
+
useColors: streamSupportsColor("stderr", support),
|
|
24
27
|
sink: STDERR_SINK
|
|
25
|
-
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function defaultLogger(silent) {
|
|
31
|
+
return createTerminalLogger(defaultLoggerOptions(silent, supportsColor));
|
|
26
32
|
}
|
|
27
33
|
function defaultDeprecationReporter() {
|
|
28
34
|
return createNodeDeprecationReporter();
|
|
@@ -47,5 +53,5 @@ function defaultRedoclyConfig() {
|
|
|
47
53
|
return createConfig({ rules }, { extends: ["minimal"] });
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
export { DEFAULT_REMOTE_DOCUMENT_LIMITS, REMOTE_REF_TIMEOUT_MS, defaultDeprecationReporter, defaultLogger, defaultRedoclyConfig, monotonicNow, processCwd, terminalSupportsColor };
|
|
56
|
+
export { DEFAULT_REMOTE_DOCUMENT_LIMITS, REMOTE_REF_TIMEOUT_MS, defaultDeprecationReporter, defaultLogger, defaultLoggerOptions, defaultRedoclyConfig, monotonicNow, processCwd, streamSupportsColor, terminalSupportsColor };
|
|
51
57
|
//# sourceMappingURL=default-ports.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default-ports.mjs","sources":["../../src/api/default-ports.ts"],"sourcesContent":["import { performance } from \"node:perf_hooks\";\nimport { createConfig, type Config as RedoclyConfig } from \"@redocly/openapi-core\";\nimport supportsColor from \"supports-color\";\nimport { createNodeDeprecationReporter } from \"../adapters/node-deprecations/node-deprecations.js\";\nimport type { RemoteDocumentLimits } from \"../adapters/redocly-loader/remote-fetch.js\";\nimport {\n createTerminalLogger,\n type TerminalSink,\n} from \"../adapters/terminal-logger/terminal-logger.js\";\nimport type { DeprecationReporter } from \"../application/ports/deprecation-reporter.js\";\nimport type { Logger } from \"../application/ports/logger.js\";\n\n/**\n * The default adapters of the Node API (ENGINEERING_PLAN 4.5): this module and the CLI's\n * composition root are where the process environment is read.\n */\n\n/**
|
|
1
|
+
{"version":3,"file":"default-ports.mjs","sources":["../../src/api/default-ports.ts"],"sourcesContent":["import { performance } from \"node:perf_hooks\";\nimport { createConfig, type Config as RedoclyConfig } from \"@redocly/openapi-core\";\nimport supportsColor, { type ColorInfo } from \"supports-color\";\nimport { createNodeDeprecationReporter } from \"../adapters/node-deprecations/node-deprecations.js\";\nimport type { RemoteDocumentLimits } from \"../adapters/redocly-loader/remote-fetch.js\";\nimport {\n createTerminalLogger,\n type TerminalLoggerOptions,\n type TerminalSink,\n} from \"../adapters/terminal-logger/terminal-logger.js\";\nimport type { DeprecationReporter } from \"../application/ports/deprecation-reporter.js\";\nimport type { Logger } from \"../application/ports/logger.js\";\n\n/**\n * The default adapters of the Node API (ENGINEERING_PLAN 4.5): this module and the CLI's\n * composition root are where the process environment is read.\n */\n\n/** The output streams of the process, which can each be a terminal or redirected. */\nexport type TerminalStream = \"stdout\" | \"stderr\";\n\n/** The color support of each stream, as `supports-color` detects it. */\nexport type StreamColorSupport = Readonly<Record<TerminalStream, ColorInfo>>;\n\n/**\n * Whether `stream` shows colors. Each stream is asked for itself: with stdout on a terminal and\n * stderr redirected to a log, the log gets no escape sequences.\n */\nexport function streamSupportsColor(stream: TerminalStream, support: StreamColorSupport): boolean {\n const info = support[stream];\n return info !== false && info.hasBasic;\n}\n\n/** Colors when the terminal of `stream` supports them, as the original decided. */\nexport function terminalSupportsColor(stream: TerminalStream): boolean {\n return streamSupportsColor(stream, supportsColor);\n}\n\n/**\n * Every line of the logger goes to stderr: the CLI writes the types to stdout, and `console.debug`\n * writes there too, so debug lines would end up inside the generated file.\n */\nconst STDERR_SINK: TerminalSink = {\n // biome-ignore lint/suspicious/noConsole: the terminal logger's own channel\n debug: (line) => console.error(line),\n // biome-ignore lint/suspicious/noConsole: the terminal logger's own channel\n warn: (line) => console.warn(line),\n // biome-ignore lint/suspicious/noConsole: the terminal logger's own channel\n error: (line) => console.error(line),\n};\n\n/** The options of the default logger: it writes to stderr, so the colors follow stderr. */\nexport function defaultLoggerOptions(\n silent: boolean,\n support: StreamColorSupport,\n): TerminalLoggerOptions {\n return {\n silent,\n debugFilter: process.env.DEBUG,\n useColors: streamSupportsColor(\"stderr\", support),\n sink: STDERR_SINK,\n };\n}\n\n/** The terminal logger with `DEBUG` from the environment, writing to stderr. */\nexport function defaultLogger(silent: boolean): Logger {\n return createTerminalLogger(defaultLoggerOptions(silent, supportsColor));\n}\n\n/** Deprecations as Node.js `DeprecationWarning`s, each code once per process. */\nexport function defaultDeprecationReporter(): DeprecationReporter {\n return createNodeDeprecationReporter();\n}\n\n/**\n * How long one remote `$ref` (or a remote source) may take to load. Large documents (the 27 MB\n * Cloudflare spec) download in seconds; a minute covers slow hosts, while Node's own fetch would\n * wait five minutes for a server that never answers (ENGINEERING_PLAN 8.5).\n */\nexport const REMOTE_REF_TIMEOUT_MS = 60_000;\n\nconst MEBIBYTE = 1024 * 1024;\n\n/**\n * Bounds on the remote documents of one schema (ADR-033). Large split documents have a few thousand\n * files, so 10,000 leaves room and still ends an endless fan-out. 16 parallel fetches keep a\n * multi-file document fast without exhausting local ports.\n *\n * The size limits count decoded bytes and parsed nodes (threat model T2, ADR-033 decision 6).\n * The largest document of the 1,820-entry compatibility corpus is Microsoft Graph beta: 71 MiB,\n * 2.65 million nodes (keys and values), 1.6 GB peak memory when loaded from a URL. A remote body\n * took about 7 bytes of memory per byte of text and 110 per node in our measurements, so 128 MiB\n * per document (1.8 times Graph beta), 256 MiB per schema (a split document may be twice as\n * large) and 8 million nodes per schema (3 times Graph beta) let crafted documents finish within\n * a heap of 2 GB; with the default heap of 4 GB they peak at about 1.2 to 2.3 GB.\n */\nexport const DEFAULT_REMOTE_DOCUMENT_LIMITS: RemoteDocumentLimits = {\n maxDocuments: 10_000,\n maxParallelFetches: 16,\n maxDocumentBytes: 128 * MEBIBYTE,\n maxTotalBytes: 256 * MEBIBYTE,\n maxNodes: 8_000_000,\n};\n\n/** The working directory of the process: the default `cwd` of an inline source. */\nexport function processCwd(): string {\n return process.cwd();\n}\n\n/** A monotonic clock in milliseconds, for debug durations. */\nexport function monotonicNow(): number {\n return performance.now();\n}\n\n/** The default Redocly config: minimal rules, and duplicate operation IDs are an error. */\nexport function defaultRedoclyConfig(): Promise<RedoclyConfig> {\n const rules = { \"operation-operationId-unique\": { severity: \"error\" as const } };\n return createConfig({ rules }, { extends: [\"minimal\"] });\n}\n"],"names":[],"mappings":";;;;;;AA4BO,SAAS,mBAAA,CAAoB,QAAwB,OAAA,EAAsC;AAChG,EAAA,MAAM,IAAA,GAAO,QAAQ,MAAM,CAAA;AAC3B,EAAA,OAAO,IAAA,KAAS,SAAS,IAAA,CAAK,QAAA;AAChC;AAGO,SAAS,sBAAsB,MAAA,EAAiC;AACrE,EAAA,OAAO,mBAAA,CAAoB,QAAQ,aAAa,CAAA;AAClD;AAMA,MAAM,WAAA,GAA4B;AAAA;AAAA,EAEhC,KAAA,EAAO,CAAC,IAAA,KAAS,OAAA,CAAQ,MAAM,IAAI,CAAA;AAAA;AAAA,EAEnC,IAAA,EAAM,CAAC,IAAA,KAAS,OAAA,CAAQ,KAAK,IAAI,CAAA;AAAA;AAAA,EAEjC,KAAA,EAAO,CAAC,IAAA,KAAS,OAAA,CAAQ,MAAM,IAAI;AACrC,CAAA;AAGO,SAAS,oBAAA,CACd,QACA,OAAA,EACuB;AACvB,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,WAAA,EAAa,QAAQ,GAAA,CAAI,KAAA;AAAA,IACzB,SAAA,EAAW,mBAAA,CAAoB,QAAA,EAAU,OAAO,CAAA;AAAA,IAChD,IAAA,EAAM;AAAA,GACR;AACF;AAGO,SAAS,cAAc,MAAA,EAAyB;AACrD,EAAA,OAAO,oBAAA,CAAqB,oBAAA,CAAqB,MAAA,EAAQ,aAAa,CAAC,CAAA;AACzE;AAGO,SAAS,0BAAA,GAAkD;AAChE,EAAA,OAAO,6BAAA,EAA8B;AACvC;AAOO,MAAM,qBAAA,GAAwB;AAErC,MAAM,WAAW,IAAA,GAAO,IAAA;AAejB,MAAM,8BAAA,GAAuD;AAAA,EAClE,YAAA,EAAc,GAAA;AAAA,EACd,kBAAA,EAAoB,EAAA;AAAA,EACpB,kBAAkB,GAAA,GAAM,QAAA;AAAA,EACxB,eAAe,GAAA,GAAM,QAAA;AAAA,EACrB,QAAA,EAAU;AACZ;AAGO,SAAS,UAAA,GAAqB;AACnC,EAAA,OAAO,QAAQ,GAAA,EAAI;AACrB;AAGO,SAAS,YAAA,GAAuB;AACrC,EAAA,OAAO,YAAY,GAAA,EAAI;AACzB;AAGO,SAAS,oBAAA,GAA+C;AAC7D,EAAA,MAAM,QAAQ,EAAE,8BAAA,EAAgC,EAAE,QAAA,EAAU,SAAiB,EAAE;AAC/E,EAAA,OAAO,YAAA,CAAa,EAAE,KAAA,EAAM,EAAG,EAAE,OAAA,EAAS,CAAC,SAAS,CAAA,EAAG,CAAA;AACzD;;"}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const path = require('node:path');
|
|
4
|
-
const node_url = require('node:url');
|
|
5
3
|
const schemaLoadErrors = require('../adapters/redocly-loader/schema-load-errors.cjs');
|
|
6
4
|
const stackOverflow = require('../application/stack-overflow.cjs');
|
|
7
5
|
const outputFixes = require('../domain/output-fixes.cjs');
|
|
8
6
|
const warnings = require('../domain/warnings.cjs');
|
|
9
|
-
const
|
|
7
|
+
const cwdOption = require('./cwd-option.cjs');
|
|
10
8
|
const discriminatorConflictWarnings = require('./discriminator-conflict-warnings.cjs');
|
|
11
9
|
const discriminatorScan = require('./discriminator-scan.cjs');
|
|
12
10
|
const documentRefs = require('./document-refs.cjs');
|
|
@@ -14,18 +12,6 @@ const pathTemplateWarnings = require('./path-template-warnings.cjs');
|
|
|
14
12
|
const refLimitOptions = require('./ref-limit-options.cjs');
|
|
15
13
|
const unsupportedFeatureWarnings = require('./unsupported-feature-warnings.cjs');
|
|
16
14
|
|
|
17
|
-
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
18
|
-
|
|
19
|
-
const path__default = /*#__PURE__*/_interopDefaultCompat(path);
|
|
20
|
-
|
|
21
|
-
function sourceCwd(options) {
|
|
22
|
-
const { cwd } = options;
|
|
23
|
-
if (cwd instanceof URL) {
|
|
24
|
-
return cwd.pathname.endsWith("/") ? cwd : new URL(`${cwd.pathname}/`, cwd);
|
|
25
|
-
}
|
|
26
|
-
const directory = path__default.resolve(cwd === void 0 ? defaultPorts.processCwd() : cwd.toString());
|
|
27
|
-
return node_url.pathToFileURL(path__default.join(directory, path__default.sep));
|
|
28
|
-
}
|
|
29
15
|
function callbackOptions(options) {
|
|
30
16
|
return {
|
|
31
17
|
irCallbacks: options.irCallbacks === true,
|
|
@@ -85,7 +71,7 @@ async function createDocumentRun(source, options, ports) {
|
|
|
85
71
|
const { logger } = ports;
|
|
86
72
|
const fix = outputFixesOf(options, logger);
|
|
87
73
|
const silent = options.silent ?? false;
|
|
88
|
-
const cwd =
|
|
74
|
+
const cwd = cwdOption.sourceCwdOf(options);
|
|
89
75
|
const { document: schema, localFiles } = await ports.loader.load({
|
|
90
76
|
source,
|
|
91
77
|
cwd,
|