mirror of
https://github.com/denoland/deno.git
synced 2024-11-24 15:19:26 -05:00
fix(ext/node): use primordials in ext/node/polyfills/_util (#21444)
This commit is contained in:
parent
3a74fa60ca
commit
b24356d9b9
7 changed files with 344 additions and 167 deletions
|
@ -21,12 +21,20 @@
|
|||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
// TODO(petamoriken): enable prefer-primordials for node polyfills
|
||||
// deno-lint-ignore-file prefer-primordials
|
||||
|
||||
// These are simplified versions of the "real" errors in Node.
|
||||
|
||||
import { primordials } from "ext:core/mod.js";
|
||||
import { nextTick } from "ext:deno_node/_next_tick.ts";
|
||||
const {
|
||||
ArrayPrototypePop,
|
||||
Error,
|
||||
FunctionPrototypeApply,
|
||||
FunctionPrototypeBind,
|
||||
ObjectDefineProperties,
|
||||
ObjectGetOwnPropertyDescriptors,
|
||||
PromisePrototypeThen,
|
||||
TypeError,
|
||||
} = primordials;
|
||||
|
||||
class NodeFalsyValueRejectionError extends Error {
|
||||
public reason: unknown;
|
||||
|
@ -98,25 +106,26 @@ function callbackify<ResultT>(
|
|||
}
|
||||
|
||||
const callbackified = function (this: unknown, ...args: unknown[]) {
|
||||
const maybeCb = args.pop();
|
||||
const maybeCb = ArrayPrototypePop(args);
|
||||
if (typeof maybeCb !== "function") {
|
||||
throw new NodeInvalidArgTypeError("last");
|
||||
}
|
||||
const cb = (...args: unknown[]) => {
|
||||
maybeCb.apply(this, args);
|
||||
FunctionPrototypeApply(maybeCb, this, args);
|
||||
};
|
||||
original.apply(this, args).then(
|
||||
PromisePrototypeThen(
|
||||
FunctionPrototypeApply(this, args),
|
||||
(ret: unknown) => {
|
||||
nextTick(cb.bind(this, null, ret));
|
||||
nextTick(FunctionPrototypeBind(cb, this, null, ret));
|
||||
},
|
||||
(rej: unknown) => {
|
||||
rej = rej || new NodeFalsyValueRejectionError(rej);
|
||||
nextTick(cb.bind(this, rej));
|
||||
nextTick(FunctionPrototypeBind(cb, this, rej));
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const descriptors = Object.getOwnPropertyDescriptors(original);
|
||||
const descriptors = ObjectGetOwnPropertyDescriptors(original);
|
||||
// It is possible to manipulate a functions `length` or `name` property. This
|
||||
// guards against the manipulation.
|
||||
if (typeof descriptors.length.value === "number") {
|
||||
|
@ -125,7 +134,7 @@ function callbackify<ResultT>(
|
|||
if (typeof descriptors.name.value === "string") {
|
||||
descriptors.name.value += "Callbackified";
|
||||
}
|
||||
Object.defineProperties(callbackified, descriptors);
|
||||
ObjectDefineProperties(callbackified, descriptors);
|
||||
return callbackified;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
// TODO(petamoriken): enable prefer-primordials for node polyfills
|
||||
// deno-lint-ignore-file prefer-primordials
|
||||
import { primordials } from "ext:core/mod.js";
|
||||
const {
|
||||
Error,
|
||||
} = primordials;
|
||||
|
||||
/** Assertion error class for node compat layer's internal code. */
|
||||
export class NodeCompatAssertionError extends Error {
|
||||
|
|
|
@ -2,8 +2,12 @@
|
|||
// This module is vendored from std/async/delay.ts
|
||||
// (with some modifications)
|
||||
|
||||
// TODO(petamoriken): enable prefer-primordials for node polyfills
|
||||
// deno-lint-ignore-file prefer-primordials
|
||||
import { primordials } from "ext:core/mod.js";
|
||||
import { clearTimeout, setTimeout } from "ext:deno_web/02_timers.js";
|
||||
const {
|
||||
Promise,
|
||||
PromiseReject,
|
||||
} = primordials;
|
||||
|
||||
/** Resolve a Promise after a given amount of milliseconds. */
|
||||
export function delay(
|
||||
|
@ -12,12 +16,12 @@ export function delay(
|
|||
): Promise<void> {
|
||||
const { signal } = options;
|
||||
if (signal?.aborted) {
|
||||
return Promise.reject(new DOMException("Delay was aborted.", "AbortError"));
|
||||
return PromiseReject(signal.reason);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const abort = () => {
|
||||
clearTimeout(i);
|
||||
reject(new DOMException("Delay was aborted.", "AbortError"));
|
||||
reject(signal!.reason);
|
||||
};
|
||||
const done = () => {
|
||||
signal?.removeEventListener("abort", abort);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
const { ops } = globalThis.__bootstrap.core;
|
||||
import { core } from "ext:core/mod.js";
|
||||
const ops = core.ops;
|
||||
|
||||
export type OSType = "windows" | "linux" | "darwin" | "freebsd" | "openbsd";
|
||||
|
||||
|
|
|
@ -1,15 +1,43 @@
|
|||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// vendored from std/assert/mod.ts
|
||||
|
||||
// TODO(petamoriken): enable prefer-primordials for node polyfills
|
||||
// deno-lint-ignore-file prefer-primordials
|
||||
|
||||
import { primordials } from "ext:core/mod.js";
|
||||
import { URLPrototype } from "ext:deno_url/00_url.js";
|
||||
import { red } from "ext:deno_node/_util/std_fmt_colors.ts";
|
||||
import {
|
||||
buildMessage,
|
||||
diff,
|
||||
diffstr,
|
||||
} from "ext:deno_node/_util/std_testing_diff.ts";
|
||||
const {
|
||||
DatePrototype,
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypeMap,
|
||||
DatePrototypeGetTime,
|
||||
Error,
|
||||
NumberIsNaN,
|
||||
Object,
|
||||
ObjectIs,
|
||||
ObjectKeys,
|
||||
ObjectPrototypeIsPrototypeOf,
|
||||
ReflectHas,
|
||||
ReflectOwnKeys,
|
||||
RegExpPrototype,
|
||||
RegExpPrototypeTest,
|
||||
SafeMap,
|
||||
SafeRegExp,
|
||||
String,
|
||||
StringPrototypeReplace,
|
||||
StringPrototypeSplit,
|
||||
SymbolIterator,
|
||||
TypeError,
|
||||
WeakMapPrototype,
|
||||
WeakSetPrototype,
|
||||
WeakRefPrototype,
|
||||
WeakRefPrototypeDeref,
|
||||
} = primordials;
|
||||
|
||||
const FORMAT_PATTERN = new SafeRegExp(/(?=["\\])/g);
|
||||
|
||||
/** Converts the input into a string. Objects, Sets and Maps are sorted so as to
|
||||
* make tests less flaky */
|
||||
|
@ -26,7 +54,7 @@ export function format(v: unknown): string {
|
|||
// getters should be true in assertEquals.
|
||||
getters: true,
|
||||
})
|
||||
: `"${String(v).replace(/(?=["\\])/g, "\\")}"`;
|
||||
: `"${StringPrototypeReplace(String(v), FORMAT_PATTERN, "\\")}"`;
|
||||
}
|
||||
|
||||
const CAN_NOT_DISPLAY = "[Cannot display]";
|
||||
|
@ -38,56 +66,75 @@ export class AssertionError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
function isKeyedCollection(x: unknown): x is Set<unknown> {
|
||||
return [Symbol.iterator, "size"].every((k) => k in (x as Set<unknown>));
|
||||
function isKeyedCollection(
|
||||
x: unknown,
|
||||
): x is { size: number; entries(): Iterable<[unknown, unknown]> } {
|
||||
return ReflectHas(x, SymbolIterator) && ReflectHas(x, "size");
|
||||
}
|
||||
|
||||
/** Deep equality comparison used in assertions */
|
||||
export function equal(c: unknown, d: unknown): boolean {
|
||||
const seen = new Map();
|
||||
const seen = new SafeMap();
|
||||
return (function compare(a: unknown, b: unknown): boolean {
|
||||
// Have to render RegExp & Date for string comparison
|
||||
// unless it's mistreated as object
|
||||
if (
|
||||
a &&
|
||||
b &&
|
||||
((a instanceof RegExp && b instanceof RegExp) ||
|
||||
(a instanceof URL && b instanceof URL))
|
||||
((ObjectPrototypeIsPrototypeOf(RegExpPrototype, a) &&
|
||||
ObjectPrototypeIsPrototypeOf(RegExpPrototype, b)) ||
|
||||
(ObjectPrototypeIsPrototypeOf(URLPrototype, a) &&
|
||||
ObjectPrototypeIsPrototypeOf(URLPrototype, b)))
|
||||
) {
|
||||
return String(a) === String(b);
|
||||
}
|
||||
if (a instanceof Date && b instanceof Date) {
|
||||
const aTime = a.getTime();
|
||||
const bTime = b.getTime();
|
||||
if (
|
||||
ObjectPrototypeIsPrototypeOf(DatePrototype, a) &&
|
||||
ObjectPrototypeIsPrototypeOf(DatePrototype, b)
|
||||
) {
|
||||
const aTime = DatePrototypeGetTime(a);
|
||||
const bTime = DatePrototypeGetTime(b);
|
||||
// Check for NaN equality manually since NaN is not
|
||||
// equal to itself.
|
||||
if (Number.isNaN(aTime) && Number.isNaN(bTime)) {
|
||||
if (NumberIsNaN(aTime) && NumberIsNaN(bTime)) {
|
||||
return true;
|
||||
}
|
||||
return aTime === bTime;
|
||||
}
|
||||
if (typeof a === "number" && typeof b === "number") {
|
||||
return Number.isNaN(a) && Number.isNaN(b) || a === b;
|
||||
return NumberIsNaN(a) && NumberIsNaN(b) || a === b;
|
||||
}
|
||||
if (Object.is(a, b)) {
|
||||
if (ObjectIs(a, b)) {
|
||||
return true;
|
||||
}
|
||||
if (a && typeof a === "object" && b && typeof b === "object") {
|
||||
if (a && b && !constructorsEqual(a, b)) {
|
||||
return false;
|
||||
}
|
||||
if (a instanceof WeakMap || b instanceof WeakMap) {
|
||||
if (!(a instanceof WeakMap && b instanceof WeakMap)) return false;
|
||||
if (
|
||||
ObjectPrototypeIsPrototypeOf(WeakMapPrototype, a) ||
|
||||
ObjectPrototypeIsPrototypeOf(WeakMapPrototype, b)
|
||||
) {
|
||||
if (
|
||||
!(ObjectPrototypeIsPrototypeOf(WeakMapPrototype, a) &&
|
||||
ObjectPrototypeIsPrototypeOf(WeakMapPrototype, b))
|
||||
) return false;
|
||||
throw new TypeError("cannot compare WeakMap instances");
|
||||
}
|
||||
if (a instanceof WeakSet || b instanceof WeakSet) {
|
||||
if (!(a instanceof WeakSet && b instanceof WeakSet)) return false;
|
||||
if (
|
||||
ObjectPrototypeIsPrototypeOf(WeakSetPrototype, a) ||
|
||||
ObjectPrototypeIsPrototypeOf(WeakSetPrototype, b)
|
||||
) {
|
||||
if (
|
||||
!(ObjectPrototypeIsPrototypeOf(WeakSetPrototype, a) &&
|
||||
ObjectPrototypeIsPrototypeOf(WeakSetPrototype, b))
|
||||
) return false;
|
||||
throw new TypeError("cannot compare WeakSet instances");
|
||||
}
|
||||
if (seen.get(a) === b) {
|
||||
return true;
|
||||
}
|
||||
if (Object.keys(a || {}).length !== Object.keys(b || {}).length) {
|
||||
if (ObjectKeys(a || {}).length !== ObjectKeys(b || {}).length) {
|
||||
return false;
|
||||
}
|
||||
seen.set(a, b);
|
||||
|
@ -98,7 +145,10 @@ export function equal(c: unknown, d: unknown): boolean {
|
|||
|
||||
let unmatchedEntries = a.size;
|
||||
|
||||
// TODO(petamoriken): use primordials
|
||||
// deno-lint-ignore prefer-primordials
|
||||
for (const [aKey, aValue] of a.entries()) {
|
||||
// deno-lint-ignore prefer-primordials
|
||||
for (const [bKey, bValue] of b.entries()) {
|
||||
/* Given that Map keys can be references, we need
|
||||
* to ensure that they are also deeply equal */
|
||||
|
@ -111,27 +161,34 @@ export function equal(c: unknown, d: unknown): boolean {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return unmatchedEntries === 0;
|
||||
}
|
||||
|
||||
const merged = { ...a, ...b };
|
||||
for (
|
||||
const key of [
|
||||
...Object.getOwnPropertyNames(merged),
|
||||
...Object.getOwnPropertySymbols(merged),
|
||||
]
|
||||
) {
|
||||
const keys = ReflectOwnKeys(merged);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
const key = keys[i];
|
||||
type Key = keyof typeof merged;
|
||||
if (!compare(a && a[key as Key], b && b[key as Key])) {
|
||||
return false;
|
||||
}
|
||||
if (((key in a) && (!(key in b))) || ((key in b) && (!(key in a)))) {
|
||||
if (
|
||||
(ReflectHas(a, key) && !ReflectHas(b, key)) ||
|
||||
(ReflectHas(b, key) && !ReflectHas(a, key))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (a instanceof WeakRef || b instanceof WeakRef) {
|
||||
if (!(a instanceof WeakRef && b instanceof WeakRef)) return false;
|
||||
return compare(a.deref(), b.deref());
|
||||
|
||||
if (
|
||||
ObjectPrototypeIsPrototypeOf(WeakRefPrototype, a) ||
|
||||
ObjectPrototypeIsPrototypeOf(WeakRefPrototype, b)
|
||||
) {
|
||||
if (
|
||||
!(ObjectPrototypeIsPrototypeOf(WeakRefPrototype, a) &&
|
||||
ObjectPrototypeIsPrototypeOf(WeakRefPrototype, b))
|
||||
) return false;
|
||||
return compare(WeakRefPrototypeDeref(a), WeakRefPrototypeDeref(b));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -166,8 +223,14 @@ export function assertEquals<T>(actual: T, expected: T, msg?: string) {
|
|||
(typeof expected === "string");
|
||||
const diffResult = stringDiff
|
||||
? diffstr(actual as string, expected as string)
|
||||
: diff(actualString.split("\n"), expectedString.split("\n"));
|
||||
const diffMsg = buildMessage(diffResult, { stringDiff }).join("\n");
|
||||
: diff(
|
||||
StringPrototypeSplit(actualString, "\n"),
|
||||
StringPrototypeSplit(expectedString, "\n"),
|
||||
);
|
||||
const diffMsg = ArrayPrototypeJoin(
|
||||
buildMessage(diffResult, { stringDiff }),
|
||||
"\n",
|
||||
);
|
||||
message = `Values are not equal:\n${diffMsg}`;
|
||||
} catch {
|
||||
message = `\n${red(red(CAN_NOT_DISPLAY))} + \n\n`;
|
||||
|
@ -209,7 +272,7 @@ export function assertStrictEquals<T>(
|
|||
expected: T,
|
||||
msg?: string,
|
||||
): asserts actual is T {
|
||||
if (Object.is(actual, expected)) {
|
||||
if (ObjectIs(actual, expected)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -222,10 +285,13 @@ export function assertStrictEquals<T>(
|
|||
const expectedString = format(expected);
|
||||
|
||||
if (actualString === expectedString) {
|
||||
const withOffset = actualString
|
||||
.split("\n")
|
||||
.map((l) => ` ${l}`)
|
||||
.join("\n");
|
||||
const withOffset = ArrayPrototypeJoin(
|
||||
ArrayPrototypeMap(
|
||||
StringPrototypeSplit(actualString, "\n"),
|
||||
(l: string) => ` ${l}`,
|
||||
),
|
||||
"\n",
|
||||
);
|
||||
message =
|
||||
`Values have the same structure but are not reference-equal:\n\n${
|
||||
red(withOffset)
|
||||
|
@ -236,8 +302,14 @@ export function assertStrictEquals<T>(
|
|||
(typeof expected === "string");
|
||||
const diffResult = stringDiff
|
||||
? diffstr(actual as string, expected as string)
|
||||
: diff(actualString.split("\n"), expectedString.split("\n"));
|
||||
const diffMsg = buildMessage(diffResult, { stringDiff }).join("\n");
|
||||
: diff(
|
||||
StringPrototypeSplit(actualString, "\n"),
|
||||
StringPrototypeSplit(expectedString, "\n"),
|
||||
);
|
||||
const diffMsg = ArrayPrototypeJoin(
|
||||
buildMessage(diffResult, { stringDiff }),
|
||||
"\n",
|
||||
);
|
||||
message = `Values are not strictly equal:\n${diffMsg}`;
|
||||
} catch {
|
||||
message = `\n${CAN_NOT_DISPLAY} + \n\n`;
|
||||
|
@ -255,7 +327,7 @@ export function assertNotStrictEquals<T>(
|
|||
expected: T,
|
||||
msg?: string,
|
||||
) {
|
||||
if (!Object.is(actual, expected)) {
|
||||
if (!ObjectIs(actual, expected)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -268,10 +340,11 @@ export function assertNotStrictEquals<T>(
|
|||
* then throw. */
|
||||
export function assertMatch(
|
||||
actual: string,
|
||||
// deno-lint-ignore prefer-primordials
|
||||
expected: RegExp,
|
||||
msg?: string,
|
||||
) {
|
||||
if (!expected.test(actual)) {
|
||||
if (!RegExpPrototypeTest(expected, actual)) {
|
||||
if (!msg) {
|
||||
msg = `actual: "${actual}" expected to match: "${expected}"`;
|
||||
}
|
||||
|
@ -283,10 +356,11 @@ export function assertMatch(
|
|||
* then throw. */
|
||||
export function assertNotMatch(
|
||||
actual: string,
|
||||
// deno-lint-ignore prefer-primordials
|
||||
expected: RegExp,
|
||||
msg?: string,
|
||||
) {
|
||||
if (expected.test(actual)) {
|
||||
if (RegExpPrototypeTest(expected, actual)) {
|
||||
if (!msg) {
|
||||
msg = `actual: "${actual}" expected to not match: "${expected}"`;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This file is vendored from std/fmt/colors.ts
|
||||
|
||||
// TODO(petamoriken): enable prefer-primordials for node polyfills
|
||||
// deno-lint-ignore-file prefer-primordials
|
||||
import { primordials } from "ext:core/mod.js";
|
||||
const {
|
||||
ArrayPrototypeJoin,
|
||||
MathMax,
|
||||
MathMin,
|
||||
MathTrunc,
|
||||
SafeRegExp,
|
||||
StringPrototypeReplace,
|
||||
} = primordials;
|
||||
|
||||
// TODO(kt3k): Initialize this at the start of runtime
|
||||
// based on Deno.noColor
|
||||
|
@ -11,6 +18,7 @@ const noColor = false;
|
|||
interface Code {
|
||||
open: string;
|
||||
close: string;
|
||||
// deno-lint-ignore prefer-primordials
|
||||
regexp: RegExp;
|
||||
}
|
||||
|
||||
|
@ -47,9 +55,9 @@ export function getColorEnabled(): boolean {
|
|||
*/
|
||||
function code(open: number[], close: number): Code {
|
||||
return {
|
||||
open: `\x1b[${open.join(";")}m`,
|
||||
open: `\x1b[${ArrayPrototypeJoin(open, ";")}m`,
|
||||
close: `\x1b[${close}m`,
|
||||
regexp: new RegExp(`\\x1b\\[${close}m`, "g"),
|
||||
regexp: new SafeRegExp(`\\x1b\\[${close}m`, "g"),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -60,7 +68,9 @@ function code(open: number[], close: number): Code {
|
|||
*/
|
||||
function run(str: string, code: Code): string {
|
||||
return enabled
|
||||
? `${code.open}${str.replace(code.regexp, code.open)}${code.close}`
|
||||
? `${code.open}${
|
||||
StringPrototypeReplace(str, code.regexp, code.open)
|
||||
}${code.close}`
|
||||
: str;
|
||||
}
|
||||
|
||||
|
@ -401,7 +411,7 @@ export function bgBrightWhite(str: string): string {
|
|||
* @param min number to truncate from
|
||||
*/
|
||||
function clampAndTruncate(n: number, max = 255, min = 0): number {
|
||||
return Math.trunc(Math.max(Math.min(n, max), min));
|
||||
return MathTrunc(MathMax(MathMin(n, max), min));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -505,11 +515,11 @@ export function bgRgb24(str: string, color: number | Rgb): string {
|
|||
}
|
||||
|
||||
// https://github.com/chalk/ansi-regex/blob/02fa893d619d3da85411acc8fd4e2eea0e95a9d9/index.js
|
||||
const ANSI_PATTERN = new RegExp(
|
||||
[
|
||||
const ANSI_PATTERN = new SafeRegExp(
|
||||
ArrayPrototypeJoin([
|
||||
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
|
||||
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))",
|
||||
].join("|"),
|
||||
], "|"),
|
||||
"g",
|
||||
);
|
||||
|
||||
|
@ -518,5 +528,5 @@ const ANSI_PATTERN = new RegExp(
|
|||
* @param string to remove ANSI escape codes from
|
||||
*/
|
||||
export function stripColor(string: string): string {
|
||||
return string.replace(ANSI_PATTERN, "");
|
||||
return StringPrototypeReplace(string, ANSI_PATTERN, "");
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This file was vendored from std/testing/_diff.ts
|
||||
|
||||
// TODO(petamoriken): enable prefer-primordials for node polyfills
|
||||
// deno-lint-ignore-file prefer-primordials
|
||||
|
||||
import { primordials } from "ext:core/mod.js";
|
||||
import {
|
||||
bgGreen,
|
||||
bgRed,
|
||||
|
@ -13,6 +11,30 @@ import {
|
|||
red,
|
||||
white,
|
||||
} from "ext:deno_node/_util/std_fmt_colors.ts";
|
||||
const {
|
||||
ArrayFrom,
|
||||
ArrayPrototypeFilter,
|
||||
ArrayPrototypeForEach,
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypeMap,
|
||||
ArrayPrototypePop,
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypePushApply,
|
||||
ArrayPrototypeReverse,
|
||||
ArrayPrototypeShift,
|
||||
ArrayPrototypeSlice,
|
||||
ArrayPrototypeSplice,
|
||||
ArrayPrototypeSome,
|
||||
ArrayPrototypeUnshift,
|
||||
SafeArrayIterator,
|
||||
SafeRegExp,
|
||||
StringPrototypeSplit,
|
||||
StringPrototypeReplace,
|
||||
StringPrototypeTrim,
|
||||
MathMin,
|
||||
ObjectFreeze,
|
||||
Uint32Array,
|
||||
} = primordials;
|
||||
|
||||
interface FarthestPoint {
|
||||
y: number;
|
||||
|
@ -28,7 +50,7 @@ export enum DiffType {
|
|||
export interface DiffResult<T> {
|
||||
type: DiffType;
|
||||
value: T;
|
||||
details?: Array<DiffResult<T>>;
|
||||
details?: DiffResult<T>[];
|
||||
}
|
||||
|
||||
const REMOVED = 1;
|
||||
|
@ -38,11 +60,11 @@ const ADDED = 3;
|
|||
function createCommon<T>(A: T[], B: T[], reverse?: boolean): T[] {
|
||||
const common = [];
|
||||
if (A.length === 0 || B.length === 0) return [];
|
||||
for (let i = 0; i < Math.min(A.length, B.length); i += 1) {
|
||||
for (let i = 0; i < MathMin(A.length, B.length); i += 1) {
|
||||
if (
|
||||
A[reverse ? A.length - i - 1 : i] === B[reverse ? B.length - i - 1 : i]
|
||||
) {
|
||||
common.push(A[reverse ? A.length - i - 1 : i]);
|
||||
ArrayPrototypePush(common, A[reverse ? A.length - i - 1 : i]);
|
||||
} else {
|
||||
return common;
|
||||
}
|
||||
|
@ -55,44 +77,56 @@ function createCommon<T>(A: T[], B: T[], reverse?: boolean): T[] {
|
|||
* @param A Actual value
|
||||
* @param B Expected value
|
||||
*/
|
||||
export function diff<T>(A: T[], B: T[]): Array<DiffResult<T>> {
|
||||
export function diff<T>(A: T[], B: T[]): DiffResult<T>[] {
|
||||
const prefixCommon = createCommon(A, B);
|
||||
const suffixCommon = createCommon(
|
||||
A.slice(prefixCommon.length),
|
||||
B.slice(prefixCommon.length),
|
||||
const suffixCommon = ArrayPrototypeReverse(createCommon(
|
||||
ArrayPrototypeSlice(A, prefixCommon.length),
|
||||
ArrayPrototypeSlice(B, prefixCommon.length),
|
||||
true,
|
||||
).reverse();
|
||||
));
|
||||
A = suffixCommon.length
|
||||
? A.slice(prefixCommon.length, -suffixCommon.length)
|
||||
: A.slice(prefixCommon.length);
|
||||
? ArrayPrototypeSlice(A, prefixCommon.length, -suffixCommon.length)
|
||||
: ArrayPrototypeSlice(A, prefixCommon.length);
|
||||
B = suffixCommon.length
|
||||
? B.slice(prefixCommon.length, -suffixCommon.length)
|
||||
: B.slice(prefixCommon.length);
|
||||
? ArrayPrototypeSlice(B, prefixCommon.length, -suffixCommon.length)
|
||||
: ArrayPrototypeSlice(B, prefixCommon.length);
|
||||
const swapped = B.length > A.length;
|
||||
[A, B] = swapped ? [B, A] : [A, B];
|
||||
if (swapped) {
|
||||
const temp = A;
|
||||
A = B;
|
||||
B = temp;
|
||||
}
|
||||
const M = A.length;
|
||||
const N = B.length;
|
||||
if (!M && !N && !suffixCommon.length && !prefixCommon.length) return [];
|
||||
if (!N) {
|
||||
if (
|
||||
M === 0 && N === 0 && suffixCommon.length === 0 && prefixCommon.length === 0
|
||||
) return [];
|
||||
if (N === 0) {
|
||||
return [
|
||||
...prefixCommon.map(
|
||||
(c): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
|
||||
...new SafeArrayIterator(
|
||||
ArrayPrototypeMap(
|
||||
prefixCommon,
|
||||
(c: T): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
|
||||
),
|
||||
...A.map(
|
||||
(a): DiffResult<typeof a> => ({
|
||||
),
|
||||
...new SafeArrayIterator(
|
||||
ArrayPrototypeMap(A, (a: T): DiffResult<typeof a> => ({
|
||||
type: swapped ? DiffType.added : DiffType.removed,
|
||||
value: a,
|
||||
}),
|
||||
})),
|
||||
),
|
||||
...new SafeArrayIterator(
|
||||
ArrayPrototypeMap(
|
||||
suffixCommon,
|
||||
(c: T): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
|
||||
),
|
||||
...suffixCommon.map(
|
||||
(c): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
|
||||
),
|
||||
];
|
||||
}
|
||||
const offset = N;
|
||||
const delta = M - N;
|
||||
const size = M + N + 1;
|
||||
const fp: FarthestPoint[] = Array.from(
|
||||
const fp: FarthestPoint[] = ArrayFrom(
|
||||
{ length: size },
|
||||
() => ({ y: -1, id: -1 }),
|
||||
);
|
||||
|
@ -114,13 +148,13 @@ export function diff<T>(A: T[], B: T[]): Array<DiffResult<T>> {
|
|||
B: T[],
|
||||
current: FarthestPoint,
|
||||
swapped: boolean,
|
||||
): Array<{
|
||||
): {
|
||||
type: DiffType;
|
||||
value: T;
|
||||
}> {
|
||||
}[] {
|
||||
const M = A.length;
|
||||
const N = B.length;
|
||||
const result = [];
|
||||
const result: DiffResult<T>[] = [];
|
||||
let a = M - 1;
|
||||
let b = N - 1;
|
||||
let j = routes[current.id];
|
||||
|
@ -129,19 +163,19 @@ export function diff<T>(A: T[], B: T[]): Array<DiffResult<T>> {
|
|||
if (!j && !type) break;
|
||||
const prev = j;
|
||||
if (type === REMOVED) {
|
||||
result.unshift({
|
||||
ArrayPrototypeUnshift(result, {
|
||||
type: swapped ? DiffType.removed : DiffType.added,
|
||||
value: B[b],
|
||||
});
|
||||
b -= 1;
|
||||
} else if (type === ADDED) {
|
||||
result.unshift({
|
||||
ArrayPrototypeUnshift(result, {
|
||||
type: swapped ? DiffType.added : DiffType.removed,
|
||||
value: A[a],
|
||||
});
|
||||
a -= 1;
|
||||
} else {
|
||||
result.unshift({ type: DiffType.common, value: A[a] });
|
||||
ArrayPrototypeUnshift(result, { type: DiffType.common, value: A[a] });
|
||||
a -= 1;
|
||||
b -= 1;
|
||||
}
|
||||
|
@ -234,16 +268,40 @@ export function diff<T>(A: T[], B: T[]): Array<DiffResult<T>> {
|
|||
);
|
||||
}
|
||||
return [
|
||||
...prefixCommon.map(
|
||||
(c): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
|
||||
...new SafeArrayIterator(
|
||||
ArrayPrototypeMap(
|
||||
prefixCommon,
|
||||
(c: T): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
|
||||
),
|
||||
),
|
||||
...new SafeArrayIterator(backTrace(A, B, fp[delta + offset], swapped)),
|
||||
...new SafeArrayIterator(
|
||||
ArrayPrototypeMap(
|
||||
suffixCommon,
|
||||
(c: T): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
|
||||
),
|
||||
...backTrace(A, B, fp[delta + offset], swapped),
|
||||
...suffixCommon.map(
|
||||
(c): DiffResult<typeof c> => ({ type: DiffType.common, value: c }),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
const ESCAPE_PATTERN = new SafeRegExp(/([\b\f\t\v])/g);
|
||||
const ESCAPE_MAP = ObjectFreeze({
|
||||
"\b": "\\b",
|
||||
"\f": "\\f",
|
||||
"\t": "\\t",
|
||||
"\v": "\\v",
|
||||
});
|
||||
const LINE_BREAK_GLOBAL_PATTERN = new SafeRegExp(/\r\n|\r|\n/g);
|
||||
|
||||
const LINE_BREAK_PATTERN = new SafeRegExp(/(\n|\r\n)/);
|
||||
const WHITESPACE_PATTERN = new SafeRegExp(/\s+/);
|
||||
const WHITESPACE_SYMBOL_PATTERN = new SafeRegExp(
|
||||
/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/,
|
||||
);
|
||||
const LATIN_CHARACTER_PATTERN = new SafeRegExp(
|
||||
/^[a-zA-Z\u{C0}-\u{FF}\u{D8}-\u{F6}\u{F8}-\u{2C6}\u{2C8}-\u{2D7}\u{2DE}-\u{2FF}\u{1E00}-\u{1EFF}]+$/u,
|
||||
);
|
||||
|
||||
/**
|
||||
* Renders the differences between the actual and expected strings
|
||||
* Partially inspired from https://github.com/kpdecker/jsdiff
|
||||
|
@ -254,44 +312,44 @@ export function diffstr(A: string, B: string) {
|
|||
function unescape(string: string): string {
|
||||
// unescape invisible characters.
|
||||
// ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#escape_sequences
|
||||
return string
|
||||
.replaceAll("\b", "\\b")
|
||||
.replaceAll("\f", "\\f")
|
||||
.replaceAll("\t", "\\t")
|
||||
.replaceAll("\v", "\\v")
|
||||
.replaceAll( // does not remove line breaks
|
||||
/\r\n|\r|\n/g,
|
||||
(str) => str === "\r" ? "\\r" : str === "\n" ? "\\n\n" : "\\r\\n\r\n",
|
||||
return StringPrototypeReplace(
|
||||
StringPrototypeReplace(
|
||||
string,
|
||||
ESCAPE_PATTERN,
|
||||
(c: string) => ESCAPE_MAP[c],
|
||||
),
|
||||
LINE_BREAK_GLOBAL_PATTERN, // does not remove line breaks
|
||||
(str: string) =>
|
||||
str === "\r" ? "\\r" : str === "\n" ? "\\n\n" : "\\r\\n\r\n",
|
||||
);
|
||||
}
|
||||
|
||||
function tokenize(string: string, { wordDiff = false } = {}): string[] {
|
||||
if (wordDiff) {
|
||||
// Split string on whitespace symbols
|
||||
const tokens = string.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/);
|
||||
// Extended Latin character set
|
||||
const words =
|
||||
/^[a-zA-Z\u{C0}-\u{FF}\u{D8}-\u{F6}\u{F8}-\u{2C6}\u{2C8}-\u{2D7}\u{2DE}-\u{2FF}\u{1E00}-\u{1EFF}]+$/u;
|
||||
const tokens = StringPrototypeSplit(string, WHITESPACE_SYMBOL_PATTERN);
|
||||
|
||||
// Join boundary splits that we do not consider to be boundaries and merge empty strings surrounded by word chars
|
||||
for (let i = 0; i < tokens.length - 1; i++) {
|
||||
if (
|
||||
!tokens[i + 1] && tokens[i + 2] && words.test(tokens[i]) &&
|
||||
words.test(tokens[i + 2])
|
||||
!tokens[i + 1] && tokens[i + 2] &&
|
||||
LATIN_CHARACTER_PATTERN.test(tokens[i]) &&
|
||||
LATIN_CHARACTER_PATTERN.test(tokens[i + 2])
|
||||
) {
|
||||
tokens[i] += tokens[i + 2];
|
||||
tokens.splice(i + 1, 2);
|
||||
ArrayPrototypeSplice(tokens, i + 1, 2);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
return tokens.filter((token) => token);
|
||||
return ArrayPrototypeFilter(tokens, (token: string) => token);
|
||||
} else {
|
||||
// Split string on new lines symbols
|
||||
const tokens = [], lines = string.split(/(\n|\r\n)/);
|
||||
const tokens: string[] = [],
|
||||
lines: string[] = StringPrototypeSplit(string, LINE_BREAK_PATTERN);
|
||||
|
||||
// Ignore final empty token when text ends with a newline
|
||||
if (!lines[lines.length - 1]) {
|
||||
lines.pop();
|
||||
if (lines[lines.length - 1] === "") {
|
||||
ArrayPrototypePop(lines);
|
||||
}
|
||||
|
||||
// Merge the content and line separators into single tokens
|
||||
|
@ -299,7 +357,7 @@ export function diffstr(A: string, B: string) {
|
|||
if (i % 2) {
|
||||
tokens[tokens.length - 1] += lines[i];
|
||||
} else {
|
||||
tokens.push(lines[i]);
|
||||
ArrayPrototypePush(tokens, lines[i]);
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
|
@ -310,14 +368,19 @@ export function diffstr(A: string, B: string) {
|
|||
// and merge "space-diff" if surrounded by word-diff for cleaner displays
|
||||
function createDetails(
|
||||
line: DiffResult<string>,
|
||||
tokens: Array<DiffResult<string>>,
|
||||
tokens: DiffResult<string>[],
|
||||
) {
|
||||
return tokens.filter(({ type }) =>
|
||||
type === line.type || type === DiffType.common
|
||||
).map((result, i, t) => {
|
||||
return ArrayPrototypeMap(
|
||||
ArrayPrototypeFilter(
|
||||
tokens,
|
||||
({ type }: DiffResult<string>) =>
|
||||
type === line.type || type === DiffType.common,
|
||||
),
|
||||
(result: DiffResult<string>, i: number, t: DiffResult<string>[]) => {
|
||||
if (
|
||||
(result.type === DiffType.common) && (t[i - 1]) &&
|
||||
(t[i - 1]?.type === t[i + 1]?.type) && /\s+/.test(result.value)
|
||||
(t[i - 1]?.type === t[i + 1]?.type) &&
|
||||
WHITESPACE_PATTERN.test(result.value)
|
||||
) {
|
||||
return {
|
||||
...result,
|
||||
|
@ -325,7 +388,8 @@ export function diffstr(A: string, B: string) {
|
|||
};
|
||||
}
|
||||
return result;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Compute multi-line diff
|
||||
|
@ -334,32 +398,36 @@ export function diffstr(A: string, B: string) {
|
|||
tokenize(`${unescape(B)}\n`),
|
||||
);
|
||||
|
||||
const added = [], removed = [];
|
||||
for (const result of diffResult) {
|
||||
const added: DiffResult<string>[] = [], removed: DiffResult<string>[] = [];
|
||||
for (let i = 0; i < diffResult.length; ++i) {
|
||||
const result = diffResult[i];
|
||||
if (result.type === DiffType.added) {
|
||||
added.push(result);
|
||||
ArrayPrototypePush(added, result);
|
||||
}
|
||||
if (result.type === DiffType.removed) {
|
||||
removed.push(result);
|
||||
ArrayPrototypePush(removed, result);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute word-diff
|
||||
const aLines = added.length < removed.length ? added : removed;
|
||||
const bLines = aLines === removed ? added : removed;
|
||||
for (const a of aLines) {
|
||||
let tokens = [] as Array<DiffResult<string>>,
|
||||
for (let i = 0; i < aLines.length; ++i) {
|
||||
const a = aLines[i];
|
||||
let tokens = [] as DiffResult<string>[],
|
||||
b: undefined | DiffResult<string>;
|
||||
// Search another diff line with at least one common token
|
||||
while (bLines.length) {
|
||||
b = bLines.shift();
|
||||
while (bLines.length !== 0) {
|
||||
b = ArrayPrototypeShift(bLines);
|
||||
tokens = diff(
|
||||
tokenize(a.value, { wordDiff: true }),
|
||||
tokenize(b?.value ?? "", { wordDiff: true }),
|
||||
);
|
||||
if (
|
||||
tokens.some(({ type, value }) =>
|
||||
type === DiffType.common && value.trim().length
|
||||
ArrayPrototypeSome(
|
||||
tokens,
|
||||
({ type, value }) =>
|
||||
type === DiffType.common && StringPrototypeTrim(value).length,
|
||||
)
|
||||
) {
|
||||
break;
|
||||
|
@ -418,26 +486,35 @@ export function buildMessage(
|
|||
{ stringDiff = false } = {},
|
||||
): string[] {
|
||||
const messages: string[] = [], diffMessages: string[] = [];
|
||||
messages.push("");
|
||||
messages.push("");
|
||||
messages.push(
|
||||
ArrayPrototypePush(messages, "");
|
||||
ArrayPrototypePush(messages, "");
|
||||
ArrayPrototypePush(
|
||||
messages,
|
||||
` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${
|
||||
green(bold("Expected"))
|
||||
}`,
|
||||
);
|
||||
messages.push("");
|
||||
messages.push("");
|
||||
diffResult.forEach((result: DiffResult<string>) => {
|
||||
ArrayPrototypePush(messages, "");
|
||||
ArrayPrototypePush(messages, "");
|
||||
ArrayPrototypeForEach(diffResult, (result: DiffResult<string>) => {
|
||||
const c = createColor(result.type);
|
||||
const line = result.details?.map((detail) =>
|
||||
|
||||
const line = result.details != null
|
||||
? ArrayPrototypeJoin(
|
||||
ArrayPrototypeMap(result.details, (detail) =>
|
||||
detail.type !== DiffType.common
|
||||
? createColor(detail.type, { background: true })(detail.value)
|
||||
: detail.value
|
||||
).join("") ?? result.value;
|
||||
diffMessages.push(c(`${createSign(result.type)}${line}`));
|
||||
: detail.value),
|
||||
"",
|
||||
)
|
||||
: result.value;
|
||||
ArrayPrototypePush(diffMessages, c(`${createSign(result.type)}${line}`));
|
||||
});
|
||||
messages.push(...(stringDiff ? [diffMessages.join("")] : diffMessages));
|
||||
messages.push("");
|
||||
ArrayPrototypePushApply(
|
||||
messages,
|
||||
stringDiff ? [ArrayPrototypeJoin(diffMessages, "")] : diffMessages,
|
||||
);
|
||||
ArrayPrototypePush(messages, "");
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue