Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | 1x 17x 17x 17x 17x 17x 18x 18x 17x 17x 17x 17x 43x 20x 5x 5x 5x 1x 1x 4x 15x 12x 12x 11x 11x 12x 12x 3x 3x 9x 3x 23x 4x 21x 18x 18x 22x 18x 3x 11x 4x 4x 10x 10x 9x 7x 7x 11x 1x | import { existsSync, readFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import type { OpRootNode, PluginValue } from '@contractkit/core';
import type { DiagnosticCollector } from '@contractkit/core';
import type { HttpCache } from './cache.js';
const FILE_URL_PREFIX = 'file://';
export interface ResolvePluginExtensionsOptions {
/**
* Persistent HTTP response cache. Successful responses are written through
* `set` and reused via `get` on subsequent runs. Omit to disable disk
* caching entirely (e.g. for `--force`).
*/
httpCache?: HttpCache;
}
/**
* Resolves URL strings inside operation `plugins` JSON values.
*
* For each operation that declares a `plugins` block, walks the JSON tree of
* every entry and replaces:
* - `file://<path>` strings with the contents of the file (path is resolved
* relative to the operation's `.ck` source file).
* - `http://<url>` / `https://<url>` strings with the response body of a GET
* request to that URL.
*
* The transformed tree is stored as `op.pluginExtensions[name]`. Strings without
* a recognized URL prefix and non-string leaves pass through unchanged. Missing
* files and failed/non-2xx HTTP requests emit warnings and leave the original
* string in place.
*
* Each unique HTTP URL is fetched at most once per CLI invocation; when
* `options.httpCacheDir` is set, successful responses are persisted there and
* reused on subsequent runs.
*/
export async function resolvePluginExtensions(
roots: OpRootNode[],
rootDir: string,
diag: DiagnosticCollector,
options: ResolvePluginExtensionsOptions = {},
): Promise<void> {
const inFlight = new Map<string, Promise<string | null>>();
const httpCache = options.httpCache;
for (const root of roots) {
const contractDir = dirname(resolve(rootDir, root.file));
for (const route of root.routes) {
for (const op of route.operations) {
if (!op.plugins) continue;
const resolved: Record<string, PluginValue> = {};
for (const [name, value] of Object.entries(op.plugins)) {
resolved[name] = await resolveUrls(value, contractDir, root.file, op.loc.line, name, diag, inFlight, httpCache);
}
op.pluginExtensions = resolved;
}
}
}
}
async function resolveUrls(
value: PluginValue,
contractDir: string,
file: string,
line: number,
pluginName: string,
diag: DiagnosticCollector,
inFlight: Map<string, Promise<string | null>>,
httpCache: HttpCache | undefined,
): Promise<PluginValue> {
if (typeof value === 'string') {
if (value.startsWith(FILE_URL_PREFIX)) {
const relPath = value.slice(FILE_URL_PREFIX.length);
const absPath = resolve(contractDir, relPath);
if (!existsSync(absPath)) {
diag.warn(file, line, `plugins.${pluginName}: file not found: ${relPath}`);
return value;
}
return readFileSync(absPath, 'utf-8');
}
if (value.startsWith('http://') || value.startsWith('https://')) {
let pending = inFlight.get(value);
if (!pending) {
pending = fetchUrl(value, httpCache);
inFlight.set(value, pending);
}
const fetched = await pending;
if (fetched === null) {
diag.warn(file, line, `plugins.${pluginName}: failed to fetch ${value}`);
return value;
}
return fetched;
}
return value;
}
if (Array.isArray(value)) {
return Promise.all(value.map(item => resolveUrls(item, contractDir, file, line, pluginName, diag, inFlight, httpCache)));
}
if (value !== null && typeof value === 'object') {
const out: Record<string, PluginValue> = {};
for (const [k, v] of Object.entries(value)) {
out[k] = await resolveUrls(v, contractDir, file, line, pluginName, diag, inFlight, httpCache);
}
return out;
}
return value;
}
async function fetchUrl(url: string, httpCache: HttpCache | undefined): Promise<string | null> {
if (httpCache) {
const cached = httpCache.get(url);
if (cached !== null) return cached;
}
try {
const res = await fetch(url);
if (!res.ok) return null;
const body = await res.text();
httpCache?.set(url, body);
return body;
} catch {
return null;
}
}
|