Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
@@ -0,0 +1,11 @@
import path from "path";
export default function convertPathToPosix(filePath: string) {
const isExtendedLengthPath = filePath.startsWith("\\\\?\\");
if (isExtendedLengthPath) {
return filePath;
}
return filePath.split(path?.win32?.sep).join(path?.posix?.sep ?? "/");
}
+157
View File
@@ -0,0 +1,157 @@
import { Ono } from "@jsdevtools/ono";
import { stripHash, toFileSystemPath } from "./url.js";
import type $RefParser from "../index.js";
import type { ParserOptions } from "../index.js";
import type { JSONSchema } from "../index.js";
import type $Ref from "../ref";
export type JSONParserErrorType =
| "EUNKNOWN"
| "EPARSER"
| "EUNMATCHEDPARSER"
| "ETIMEOUT"
| "ERESOLVER"
| "EUNMATCHEDRESOLVER"
| "EMISSINGPOINTER"
| "EINVALIDPOINTER";
export class JSONParserError extends Error {
public readonly name: string;
public readonly message: string;
public source: string | undefined;
public path: Array<string | number> | null;
public readonly code: JSONParserErrorType;
public constructor(message: string, source?: string) {
super();
this.code = "EUNKNOWN";
this.name = "JSONParserError";
this.message = message;
this.source = source;
this.path = null;
Ono.extend(this);
}
get footprint() {
return `${this.path}+${this.source}+${this.code}+${this.message}`;
}
}
export class JSONParserErrorGroup<
S extends object = JSONSchema,
O extends ParserOptions<S> = ParserOptions<S>,
> extends Error {
files: $RefParser<S, O>;
constructor(parser: $RefParser<S, O>) {
super();
this.files = parser;
this.name = "JSONParserErrorGroup";
this.message = `${this.errors.length} error${
this.errors.length > 1 ? "s" : ""
} occurred while reading '${toFileSystemPath(parser.$refs._root$Ref!.path)}'`;
Ono.extend(this);
}
static getParserErrors<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
parser: $RefParser<S, O>,
) {
const errors = [];
for (const $ref of Object.values(parser.$refs._$refs) as $Ref<S, O>[]) {
if ($ref.errors) {
errors.push(...$ref.errors);
}
}
return errors;
}
get errors(): Array<
| JSONParserError
| InvalidPointerError
| ResolverError
| ParserError
| MissingPointerError
| UnmatchedParserError
| UnmatchedResolverError
> {
return JSONParserErrorGroup.getParserErrors<S, O>(this.files);
}
}
export class ParserError extends JSONParserError {
code = "EPARSER" as JSONParserErrorType;
name = "ParserError";
constructor(message: any, source: any) {
super(`Error parsing ${source}: ${message}`, source);
}
}
export class UnmatchedParserError extends JSONParserError {
code = "EUNMATCHEDPARSER" as JSONParserErrorType;
name = "UnmatchedParserError";
constructor(source: string) {
super(`Could not find parser for "${source}"`, source);
}
}
export class ResolverError extends JSONParserError {
code = "ERESOLVER" as JSONParserErrorType;
name = "ResolverError";
ioErrorCode?: string;
constructor(ex: Error | any, source?: string) {
super(ex.message || `Error reading file "${source}"`, source);
if ("code" in ex) {
this.ioErrorCode = String(ex.code);
}
}
}
export class UnmatchedResolverError extends JSONParserError {
code = "EUNMATCHEDRESOLVER" as JSONParserErrorType;
name = "UnmatchedResolverError";
constructor(source: any) {
super(`Could not find resolver for "${source}"`, source);
}
}
export class MissingPointerError extends JSONParserError {
code = "EUNMATCHEDRESOLVER" as JSONParserErrorType;
name = "MissingPointerError";
constructor(token: any, path: any) {
super(`Token "${token}" does not exist.`, stripHash(path));
}
}
export class TimeoutError extends JSONParserError {
code = "ETIMEOUT" as JSONParserErrorType;
name = "TimeoutError";
constructor(timeout: number) {
super(`Dereferencing timeout reached: ${timeout}ms`);
}
}
export class InvalidPointerError extends JSONParserError {
code = "EUNMATCHEDRESOLVER" as JSONParserErrorType;
name = "InvalidPointerError";
constructor(pointer: any, path: any) {
super(`Invalid $ref pointer "${pointer}". Pointers must begin with "#/"`, stripHash(path));
}
}
export function isHandledError(err: any): err is JSONParserError {
return err instanceof JSONParserError || err instanceof JSONParserErrorGroup;
}
export function normalizeError(err: any) {
if (err.path === null) {
err.path = [];
}
return err;
}
@@ -0,0 +1,2 @@
const isWindowsConst = /^win/.test(globalThis.process ? globalThis.process.platform : "");
export const isWindows = () => isWindowsConst;
+22
View File
@@ -0,0 +1,22 @@
import next from "./next.js";
type MaybeParams<T> = (err: Error | any | null, result?: T) => void;
export default function maybe<T>(cb: MaybeParams<T> | undefined, promise: Promise<T>): Promise<T> | void {
if (cb) {
promise.then(
function (result) {
next(function () {
cb(null, result);
});
},
function (err) {
next(function () {
cb(err);
});
},
);
return undefined;
} else {
return promise;
}
}
+13
View File
@@ -0,0 +1,13 @@
function makeNext() {
if (typeof process === "object" && typeof process.nextTick === "function") {
return process.nextTick;
} else if (typeof setImmediate === "function") {
return setImmediate;
} else {
return function next(f: () => void) {
setTimeout(f, 0);
};
}
}
export default makeNext();
+160
View File
@@ -0,0 +1,160 @@
import type { FileInfo, JSONSchema } from "../types/index.js";
import type { ParserOptions } from "../options.js";
import type { ResolverOptions } from "../types/index.js";
import type $Refs from "../refs.js";
import type { Plugin } from "../types/index.js";
/**
* Returns the given plugins as an array, rather than an object map.
* All other methods in this module expect an array of plugins rather than an object map.
*
* @returns
*/
export function all<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
plugins: O["resolve"],
): Plugin[] {
return (Object.keys(plugins || {}) as (keyof ResolverOptions<S>)[])
.filter((key) => {
return typeof plugins![key] === "object";
})
.map((key) => {
(plugins![key] as ResolverOptions<S>)!.name = key;
return plugins![key] as Plugin;
});
}
/**
* Filters the given plugins, returning only the ones return `true` for the given method.
*/
export function filter(plugins: Plugin[], method: any, file: any) {
return plugins.filter((plugin: Plugin) => {
return !!getResult(plugin, method, file);
});
}
/**
* Sorts the given plugins, in place, by their `order` property.
*/
export function sort(plugins: Plugin[]) {
for (const plugin of plugins) {
plugin.order = plugin.order || Number.MAX_SAFE_INTEGER;
}
return plugins.sort((a: any, b: any) => {
return a.order - b.order;
});
}
// @ts-ignore
export interface PluginResult<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>> {
plugin: Plugin;
result?: string | Buffer | S;
error?: any;
}
/**
* Runs the specified method of the given plugins, in order, until one of them returns a successful result.
* Each method can return a synchronous value, a Promise, or call an error-first callback.
* If the promise resolves successfully, or the callback is called without an error, then the result
* is immediately returned and no further plugins are called.
* If the promise rejects, or the callback is called with an error, then the next plugin is called.
* If ALL plugins fail, then the last error is thrown.
*/
export async function run<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
plugins: Plugin[],
method: keyof Plugin | keyof ResolverOptions<S>,
file: FileInfo,
$refs: $Refs<S, O>,
) {
let plugin: Plugin;
let lastError: PluginResult<S, O>;
let index = 0;
return new Promise<PluginResult<S, O>>((resolve, reject) => {
runNextPlugin();
function runNextPlugin() {
plugin = plugins[index++];
if (!plugin) {
// There are no more functions, so re-throw the last error
return reject(lastError);
}
try {
// console.log(' %s', plugin.name);
const result = getResult(plugin, method, file, callback, $refs);
if (result && typeof result.then === "function") {
// A promise was returned
result.then(onSuccess, onError);
} else if (result !== undefined) {
// A synchronous result was returned
onSuccess(result);
} else if (index === plugins.length) {
throw new Error("No promise has been returned or callback has been called.");
}
} catch (e) {
onError(e);
}
}
function callback(err: PluginResult<S, O>["error"], result: PluginResult<S, O>["result"]) {
if (err) {
onError(err);
} else {
onSuccess(result);
}
}
function onSuccess(result: PluginResult<S, O>["result"]) {
// console.log(' success');
resolve({
plugin,
result,
});
}
function onError(error: PluginResult<S, O>["error"]) {
// console.log(' %s', err.message || err);
lastError = {
plugin,
error,
};
runNextPlugin();
}
});
}
/**
* Returns the value of the given property.
* If the property is a function, then the result of the function is returned.
* If the value is a RegExp, then it will be tested against the file URL.
* If the value is an array, then it will be compared against the file extension.
*/
function getResult<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
obj: Plugin,
prop: keyof Plugin | keyof ResolverOptions<S>,
file: FileInfo,
callback?: (err?: Error, result?: any) => void,
$refs?: $Refs<S, O>,
) {
const value = obj[prop as keyof typeof obj] as unknown;
if (typeof value === "function") {
return value.apply(obj, [file, callback, $refs]);
}
if (!callback) {
// The synchronous plugin functions (canParse and canRead)
// allow a "shorthand" syntax, where the user can match
// files by RegExp or by file extension.
if (value instanceof RegExp) {
return value.test(file.url);
} else if (typeof value === "string") {
return value === file.extension;
} else if (Array.isArray(value)) {
return value.indexOf(file.extension) !== -1;
}
}
return value;
}
+308
View File
@@ -0,0 +1,308 @@
import convertPathToPosix from "./convert-path-to-posix";
import path, { win32 } from "path";
const forwardSlashPattern = /\//g;
const protocolPattern = /^(\w{2,}):\/\//i;
const jsonPointerSlash = /~1/g;
const jsonPointerTilde = /~0/g;
import { join } from "path";
import { isWindows } from "./is-windows";
// RegExp patterns to URL-encode special characters in local filesystem paths
const urlEncodePatterns = [
[/\?/g, "%3F"],
[/#/g, "%23"],
] as [RegExp, string][];
// RegExp patterns to URL-decode special characters for local filesystem paths
const urlDecodePatterns = [/%23/g, "#", /%24/g, "$", /%26/g, "&", /%2C/g, ",", /%40/g, "@"];
export const parse = (u: string | URL) => new URL(u);
/**
* Returns resolved target URL relative to a base URL in a manner similar to that of a Web browser resolving an anchor tag HREF.
*
* @returns
*/
export function resolve(from: string, to: string) {
const fromUrl = new URL(convertPathToPosix(from), "resolve://");
const resolvedUrl = new URL(convertPathToPosix(to), fromUrl);
const endSpaces = to.match(/(\s*)$/)?.[1] || "";
if (resolvedUrl.protocol === "resolve:") {
// `from` is a relative URL.
const { pathname, search, hash } = resolvedUrl;
return pathname + search + hash + endSpaces;
}
return resolvedUrl.toString() + endSpaces;
}
/**
* Returns the current working directory (in Node) or the current page URL (in browsers).
*
* @returns
*/
export function cwd() {
if (typeof window !== "undefined") {
return location.href;
}
const path = process.cwd();
const lastChar = path.slice(-1);
if (lastChar === "/" || lastChar === "\\") {
return path;
} else {
return path + "/";
}
}
/**
* Returns the protocol of the given URL, or `undefined` if it has no protocol.
*
* @param path
* @returns
*/
export function getProtocol(path: string | undefined) {
const match = protocolPattern.exec(path || "");
if (match) {
return match[1].toLowerCase();
}
return undefined;
}
/**
* Returns the lowercased file extension of the given URL,
* or an empty string if it has no extension.
*
* @param path
* @returns
*/
export function getExtension(path: any) {
const lastDot = path.lastIndexOf(".");
if (lastDot >= 0) {
return stripQuery(path.substr(lastDot).toLowerCase());
}
return "";
}
/**
* Removes the query, if any, from the given path.
*
* @param path
* @returns
*/
export function stripQuery(path: any) {
const queryIndex = path.indexOf("?");
if (queryIndex >= 0) {
path = path.substr(0, queryIndex);
}
return path;
}
/**
* Returns the hash (URL fragment), of the given path.
* If there is no hash, then the root hash ("#") is returned.
*
* @param path
* @returns
*/
export function getHash(path: undefined | string) {
if (!path) {
return "#";
}
const hashIndex = path.indexOf("#");
if (hashIndex >= 0) {
return path.substring(hashIndex);
}
return "#";
}
/**
* Removes the hash (URL fragment), if any, from the given path.
*
* @param path
* @returns
*/
export function stripHash(path?: string | undefined) {
if (!path) {
return "";
}
const hashIndex = path.indexOf("#");
if (hashIndex >= 0) {
path = path.substring(0, hashIndex);
}
return path;
}
/**
* Determines whether the given path is an HTTP(S) URL.
*
* @param path
* @returns
*/
export function isHttp(path: string) {
const protocol = getProtocol(path);
if (protocol === "http" || protocol === "https") {
return true;
} else if (protocol === undefined) {
// There is no protocol. If we're running in a browser, then assume it's HTTP.
return typeof window !== "undefined";
} else {
// It's some other protocol, such as "ftp://", "mongodb://", etc.
return false;
}
}
/**
* Determines whether the given path is a filesystem path.
* This includes "file://" URLs.
*
* @param path
* @returns
*/
export function isFileSystemPath(path: string | undefined) {
// @ts-ignore
if (typeof window !== "undefined" || (typeof process !== "undefined" && process.browser)) {
// We're running in a browser, so assume that all paths are URLs.
// This way, even relative paths will be treated as URLs rather than as filesystem paths
return false;
}
const protocol = getProtocol(path);
return protocol === undefined || protocol === "file";
}
/**
* Converts a filesystem path to a properly-encoded URL.
*
* This is intended to handle situations where JSON Schema $Ref Parser is called
* with a filesystem path that contains characters which are not allowed in URLs.
*
* @example
* The following filesystem paths would be converted to the following URLs:
*
* <"!@#$%^&*+=?'>.json ==> %3C%22!@%23$%25%5E&*+=%3F\'%3E.json
* C:\\My Documents\\File (1).json ==> C:/My%20Documents/File%20(1).json
* file://Project #42/file.json ==> file://Project%20%2342/file.json
*
* @param path
* @returns
*/
export function fromFileSystemPath(path: string) {
// Step 1: On Windows, replace backslashes with forward slashes,
// rather than encoding them as "%5C"
if (isWindows()) {
const projectDir = cwd();
const upperPath = path.toUpperCase();
const projectDirPosixPath = convertPathToPosix(projectDir);
const posixUpper = projectDirPosixPath.toUpperCase();
const hasProjectDir = upperPath.includes(posixUpper);
const hasProjectUri = upperPath.includes(posixUpper);
const isAbsolutePath =
win32?.isAbsolute(path) ||
path.startsWith("http://") ||
path.startsWith("https://") ||
path.startsWith("file://");
if (!(hasProjectDir || hasProjectUri || isAbsolutePath) && !projectDir.startsWith("http")) {
path = join(projectDir, path);
}
path = convertPathToPosix(path);
}
// Step 2: `encodeURI` will take care of MOST characters
path = encodeURI(path);
// Step 3: Manually encode characters that are not encoded by `encodeURI`.
// This includes characters such as "#" and "?", which have special meaning in URLs,
// but are just normal characters in a filesystem path.
for (const pattern of urlEncodePatterns) {
path = path.replace(pattern[0], pattern[1]);
}
return path;
}
/**
* Converts a URL to a local filesystem path.
*/
export function toFileSystemPath(path: string | undefined, keepFileProtocol?: boolean): string {
// Step 1: `decodeURI` will decode characters such as Cyrillic characters, spaces, etc.
path = decodeURI(path!);
// Step 2: Manually decode characters that are not decoded by `decodeURI`.
// This includes characters such as "#" and "?", which have special meaning in URLs,
// but are just normal characters in a filesystem path.
for (let i = 0; i < urlDecodePatterns.length; i += 2) {
path = path.replace(urlDecodePatterns[i], urlDecodePatterns[i + 1] as string);
}
// Step 3: If it's a "file://" URL, then format it consistently
// or convert it to a local filesystem path
let isFileUrl = path.substr(0, 7).toLowerCase() === "file://";
if (isFileUrl) {
// Strip-off the protocol, and the initial "/", if there is one
path = path[7] === "/" ? path.substr(8) : path.substr(7);
// insert a colon (":") after the drive letter on Windows
if (isWindows() && path[1] === "/") {
path = path[0] + ":" + path.substr(1);
}
if (keepFileProtocol) {
// Return the consistently-formatted "file://" URL
path = "file:///" + path;
} else {
// Convert the "file://" URL to a local filesystem path.
// On Windows, it will start with something like "C:/".
// On Posix, it will start with "/"
isFileUrl = false;
path = isWindows() ? path : "/" + path;
}
}
// Step 4: Normalize Windows paths (unless it's a "file://" URL)
if (isWindows() && !isFileUrl) {
// Replace forward slashes with backslashes
path = path.replace(forwardSlashPattern, "\\");
// Capitalize the drive letter
if (path.substr(1, 2) === ":\\") {
path = path[0].toUpperCase() + path.substr(1);
}
}
return path;
}
/**
* Converts a $ref pointer to a valid JSON Path.
*
* @param pointer
* @returns
*/
export function safePointerToPath(pointer: any) {
if (pointer.length <= 1 || pointer[0] !== "#" || pointer[1] !== "/") {
return [];
}
return pointer
.slice(2)
.split("/")
.map((value: any) => {
return decodeURIComponent(value).replace(jsonPointerSlash, "/").replace(jsonPointerTilde, "~");
});
}
export function relative(from: string, to: string) {
if (!isFileSystemPath(from) || !isFileSystemPath(to)) {
return resolve(from, to);
}
const fromDir = path.dirname(stripHash(from));
const toPath = stripHash(to);
const result = path.relative(fromDir, toPath);
return result + getHash(to);
}