Skip to content
File

Blob: src/node/util.ts

typescript455 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import { default as internalTypes } from 'node-internal:internal_types';
6import { default as utilImpl } from 'node-internal:util';
7import { isDeepStrictEqual as _isDeepStrictEqual } from 'node-internal:internal_comparisons';
8import { Buffer } from 'node-internal:internal_buffer';
9 
10import {
11 validateFunction,
12 validateAbortSignal,
13 validateObject,
14 kValidateObjectAllowObjects,
15 validateString,
16 validateBoolean,
17 validateOneOf,
18} from 'node-internal:validators';
19 
20import { debuglog } from 'node-internal:debuglog';
21export const debug = debuglog;
22export { debuglog };
23 
24import {
25 ERR_INVALID_ARG_TYPE,
26 ERR_METHOD_NOT_IMPLEMENTED,
27} from 'node-internal:internal_errors';
28 
29import {
30 inspect,
31 format,
32 formatWithOptions,
33 stripVTControlCharacters,
34} from 'node-internal:internal_inspect';
35 
36import { callbackify, parseEnv } from 'node-internal:internal_utils';
37import {
38 isReadableStream,
39 isWritableStream,
40 isNodeStream,
41} from 'node-internal:streams_util';
42 
43let isBoolean: ((val: unknown) => boolean) | undefined = undefined;
44let isBuffer: ((val: unknown) => boolean) | undefined = undefined;
45let isDate: ((val: unknown) => boolean) | undefined = undefined;
46let isError: ((val: unknown) => boolean) | undefined = undefined;
47let isFunction: ((val: unknown) => boolean) | undefined = undefined;
48let isNull: ((val: unknown) => boolean) | undefined = undefined;
49let isNullOrUndefined: ((val: unknown) => boolean) | undefined = undefined;
50let isNumber: ((val: unknown) => boolean) | undefined = undefined;
51let isObject: ((val: unknown) => boolean) | undefined = undefined;
52let isPrimitive: ((val: unknown) => boolean) | undefined = undefined;
53let isRegExp: ((val: unknown) => boolean) | undefined = undefined;
54let isString: ((val: unknown) => boolean) | undefined = undefined;
55let isSymbol: ((val: unknown) => boolean) | undefined = undefined;
56let isUndefined: ((val: unknown) => boolean) | undefined = undefined;
57 
58if (!Cloudflare.compatibilityFlags.remove_nodejs_compat_eol_v23) {
59 isBoolean = (val: unknown): boolean => typeof val === 'boolean';
60 isBuffer = (val: unknown): boolean => Buffer.isBuffer(val);
61 isDate = (val: unknown): boolean => val instanceof Date;
62 isError = (val: unknown): boolean => Error.isError(val);
63 isFunction = (val: unknown): boolean => typeof val === 'function';
64 isNull = (val: unknown): boolean => val === null;
65 isNullOrUndefined = (val: unknown): boolean => val == null;
66 isNumber = (val: unknown): boolean => typeof val === 'number';
67 isObject = (val: unknown): boolean => val != null && typeof val === 'object';
68 isPrimitive = (val: unknown): boolean =>
69 val === null || (typeof val !== 'object' && typeof val !== 'function');
70 isRegExp = (val: unknown): boolean => val instanceof RegExp;
71 isString = (val: unknown): boolean => typeof val === 'string';
72 isSymbol = (val: unknown): boolean => typeof val === 'symbol';
73 isUndefined = (val: unknown): boolean => val === undefined;
74}
75 
76export {
77 inspect,
78 format,
79 formatWithOptions,
80 stripVTControlCharacters,
81 
82 // EOL methods
83 isBoolean,
84 isBuffer,
85 isDate,
86 isError,
87 isFunction,
88 isNull,
89 isNullOrUndefined,
90 isNumber,
91 isObject,
92 isPrimitive,
93 isRegExp,
94 isString,
95 isSymbol,
96 isUndefined,
97};
98export function isArray(val: unknown): boolean {
99 return Array.isArray(val);
100}
101export { callbackify, parseEnv } from 'node-internal:internal_utils';
102export const types = internalTypes;
103 
104export const { MIMEParams, MIMEType } = utilImpl;
105 
106const kCustomPromisifiedSymbol = Symbol.for('nodejs.util.promisify.custom');
107const kCustomPromisifyArgsSymbol = Symbol.for(
108 'nodejs.util.promisify.custom.args'
109);
110 
111// TODO(later): Proper type signature for promisify.
112/* eslint-disable */
113export function promisify(original: Function): Function {
114 validateFunction(original, 'original');
115 
116 if ((original as any)[kCustomPromisifiedSymbol]) {
117 const fn = (original as any)[kCustomPromisifiedSymbol];
118 
119 validateFunction(fn, 'util.promisify.custom');
120 
121 return Object.defineProperty(fn, kCustomPromisifiedSymbol, {
122 value: fn,
123 enumerable: false,
124 writable: false,
125 configurable: true,
126 });
127 }
128 
129 // Names to create an object from in case the callback receives multiple
130 // arguments, e.g. ['bytesRead', 'buffer'] for fs.read.
131 const argumentNames = (original as any)[kCustomPromisifyArgsSymbol];
132 
133 function fn(this: any, ...args: any[]) {
134 return new Promise((resolve, reject) => {
135 args.push((err: unknown, ...values: unknown[]) => {
136 if (err) {
137 return reject(err);
138 }
139 if (argumentNames !== undefined && values.length > 1) {
140 const obj = {};
141 for (let i = 0; i < argumentNames.length; i++)
142 (obj as any)[argumentNames[i]] = values[i];
143 resolve(obj);
144 } else {
145 resolve(values[0]);
146 }
147 });
148 Reflect.apply(original, this, args);
149 });
150 }
151 
152 Object.setPrototypeOf(fn, Object.getPrototypeOf(original));
153 
154 Object.defineProperty(fn, kCustomPromisifiedSymbol, {
155 value: fn,
156 enumerable: false,
157 writable: false,
158 configurable: true,
159 });
160 
161 const descriptors = Object.getOwnPropertyDescriptors(original);
162 const propertiesValues = Object.values(descriptors);
163 for (let i = 0; i < propertiesValues.length; i++) {
164 // We want to use null-prototype objects to not rely on globally mutable
165 // %Object.prototype%.
166 Object.setPrototypeOf(propertiesValues[i], null);
167 }
168 return Object.defineProperties(fn, descriptors);
169}
170 
171promisify.custom = kCustomPromisifiedSymbol;
172/* eslint-enable */
173 
174export function inherits(
175 ctor: Function | null | undefined, // eslint-disable-line @typescript-eslint/no-unsafe-function-type
176 superCtor: Function | null | undefined // eslint-disable-line @typescript-eslint/no-unsafe-function-type
177): void {
178 if (ctor == null) throw new ERR_INVALID_ARG_TYPE('ctor', 'Function', ctor);
179 
180 if (superCtor === undefined || superCtor === null)
181 throw new ERR_INVALID_ARG_TYPE('superCtor', 'Function', superCtor);
182 
183 if (superCtor.prototype === undefined) {
184 throw new ERR_INVALID_ARG_TYPE(
185 'superCtor.prototype',
186 'Object',
187 superCtor.prototype
188 );
189 }
190 Object.defineProperty(ctor, 'super_', {
191 value: superCtor,
192 writable: true,
193 configurable: true,
194 });
195 Object.setPrototypeOf(ctor.prototype, superCtor.prototype); // eslint-disable-line @typescript-eslint/no-unsafe-argument
196}
197 
198export function _extend<T extends Record<string, unknown>>(
199 target: T,
200 source: unknown
201): T {
202 // Don't do anything if source isn't an object
203 if (source === null || typeof source !== 'object') return target;
204 
205 const keys = Object.keys(source);
206 let i = keys.length;
207 while (i--) {
208 (target as Record<string, unknown>)[keys[i] as keyof typeof source] = (
209 source as Record<string, unknown>
210 )[keys[i] as keyof typeof source];
211 }
212 return target;
213}
214 
215export const TextDecoder = globalThis.TextDecoder;
216export const TextEncoder = globalThis.TextEncoder;
217 
218export function toUSVString(input: string): string {
219 return input.toWellFormed();
220}
221 
222function pad(n: unknown): string {
223 return `${n}`.padStart(2, '0');
224}
225 
226// prettier-ignore
227const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
228 'Oct', 'Nov', 'Dec'];
229 
230function timestamp(): string {
231 const d = new Date();
232 const t = [pad(d.getHours()), pad(d.getMinutes()), pad(d.getSeconds())].join(
233 ':'
234 );
235 return `${d.getDate()} ${months[d.getMonth()]} ${t}`;
236}
237 
238export function log(...args: unknown[]): void {
239 console.log('%s - %s', timestamp(), format(...args));
240}
241 
242export function parseArgs(): unknown {
243 // We currently have no plans to implement the util.parseArgs API.
244 throw new Error('node:util parseArgs is not implemented');
245}
246 
247export function transferableAbortController(): void {
248 throw new Error('node:util transferableAbortController is not implemented');
249}
250 
251export function transferableAbortSignal(): void {
252 throw new Error('node:util transferableAbortSignal is not implemented');
253}
254 
255export async function aborted(
256 signal: unknown,
257 resource: unknown
258 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
259): Promise<Event | void> {
260 if (signal === undefined) {
261 throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
262 }
263 // Node.js defines that the resource is held weakly such that if it is GC'ed, we
264 // will drop the event handler on the signal and the promise will remain pending
265 // forever. We don't want GC to be observable in the same way so we won't support
266 // this additional option. Unfortunately Node.js does not make this argument optional.
267 // We'll just ignore it.
268 validateAbortSignal(signal, 'signal');
269 validateObject(resource, 'resource', kValidateObjectAllowObjects);
270 if (signal.aborted) return Promise.resolve();
271 const { promise, resolve } = Promise.withResolvers<Event>();
272 const opts = { __proto__: null, once: true };
273 signal.addEventListener('abort', resolve, opts);
274 return promise;
275}
276 
277export function deprecate<T = unknown>(
278 fn: T,
279 _1?: string,
280 _2?: string,
281 _3?: boolean
282): T {
283 // TODO(soon): Node.js's implementation wraps the given function in a new function that
284 // logs a warning to the console if the function is called. Do we want to support that?
285 // For now, we're just going to silently return the input method unmodified.
286 return fn;
287}
288 
289// Node.js originally introduced the API with the name `getCallSite()` as an experimental
290// API but then renamed it to `getCallSites()` soon after. We had already implemented the
291// API with the original name in a release. To avoid the possibility of breaking, we export
292// the function using both names.
293export const getCallSite = utilImpl.getCallSites.bind(utilImpl);
294export const getCallSites = utilImpl.getCallSites.bind(utilImpl);
295 
296export function isDeepStrictEqual(a: unknown, b: unknown): boolean {
297 return _isDeepStrictEqual(a, b);
298}
299 
300export function getSystemErrorMap(): void {
301 throw new Error('node:util getSystemErrorMap is not implemented');
302}
303 
304export function getSystemErrorName(): void {
305 throw new Error('node:util getSystemErrorName is not implemented');
306}
307 
308export function getSystemErrorMessage(): void {
309 throw new Error('node:util getSystemErrorMessage is not implemented');
310}
311 
312function escapeStyleCode(code: number | undefined): string {
313 if (code === undefined) return '';
314 return `\u001b[${code}m`;
315}
316 
317// TODO(soon): We do not yet implement process.stdout, so to ensure the correct
318// behaviour, we use a placeholder value internally.
319 
320interface StyleTextOptions {
321 validateStream?: boolean;
322 stream?: ReadableStream | WritableStream;
323}
324 
325type TTYStream = (ReadableStream | WritableStream) & {
326 isTTY: true;
327 getColorDepth(): number;
328};
329 
330function isTTYStream(stream: unknown): stream is TTYStream {
331 return (
332 stream != null &&
333 typeof stream === 'object' &&
334 'isTTY' in stream &&
335 !!stream.isTTY &&
336 typeof (stream as TTYStream).getColorDepth === 'function'
337 );
338}
339 
340// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
341const stdoutPlaceholder: StyleTextOptions['stream'] = Object.create(null);
342export function styleText(
343 format: string | string[],
344 text: unknown,
345 { validateStream = true, stream = stdoutPlaceholder }: StyleTextOptions = {}
346): string {
347 validateString(text, 'text');
348 // eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
349 if (validateStream !== true)
350 validateBoolean(validateStream, 'options.validateStream');
351 
352 let skipColorize: boolean = false;
353 if (validateStream) {
354 if (
355 !isReadableStream(stream) &&
356 !isWritableStream(stream) &&
357 !isNodeStream(stream) &&
358 stream !== stdoutPlaceholder
359 ) {
360 throw new ERR_INVALID_ARG_TYPE(
361 'stream',
362 ['ReadableStream', 'WritableStream', 'Stream'],
363 stream
364 );
365 }
366 
367 // If the stream is falsy or should not be colorized, set skipColorize to true
368 skipColorize = isTTYStream(stream) ? stream.getColorDepth() > 2 : true;
369 }
370 
371 // If the format is not an array, convert it to an array
372 const formatArray = Array.isArray(format) ? format : [format];
373 
374 let left = '';
375 let right = '';
376 for (const key of formatArray) {
377 if (key === 'none') continue;
378 const formatCodes =
379 typeof key === 'string' ? inspect.colors[key] : undefined;
380 // If the format is not a valid style, throw an error
381 if (formatCodes == null) {
382 validateOneOf(key, 'format', Object.keys(inspect.colors));
383 }
384 if (skipColorize) continue;
385 left += escapeStyleCode(formatCodes ? formatCodes[0] : undefined);
386 right = `${escapeStyleCode(formatCodes ? formatCodes[1] : undefined)}${right}`;
387 }
388 
389 return skipColorize ? text : `${left}${text}${right}`;
390}
391 
392export function _errnoException(): void {
393 // TODO(soon): We might support this in the future.
394 throw new ERR_METHOD_NOT_IMPLEMENTED('_errnoException');
395}
396 
397export function _exceptionWithHostPort(): void {
398 // TODO(soon): We might support this in the future.
399 throw new ERR_METHOD_NOT_IMPLEMENTED('_exceptionWithHostPort');
400}
401 
402export default {
403 types,
404 callbackify,
405 promisify,
406 inspect,
407 format,
408 formatWithOptions,
409 stripVTControlCharacters,
410 inherits,
411 _extend,
412 MIMEParams,
413 MIMEType,
414 toUSVString,
415 log,
416 aborted,
417 debuglog,
418 debug,
419 deprecate,
420 getSystemErrorMap,
421 getSystemErrorMessage,
422 getSystemErrorName,
423 // Node.js originally exposed TextEncoder and TextDecoder off the util
424 // module originally, so let's just go ahead and do the same.
425 TextEncoder,
426 TextDecoder,
427 parseArgs,
428 parseEnv,
429 styleText,
430 transferableAbortController,
431 transferableAbortSignal,
432 getCallSite,
433 getCallSites,
434 isDeepStrictEqual,
435 _errnoException,
436 _exceptionWithHostPort,
437 
438 isArray,
439 // EOL methods
440 isBoolean,
441 isBuffer,
442 isDate,
443 isError,
444 isFunction,
445 isNull,
446 isNullOrUndefined,
447 isNumber,
448 isObject,
449 isPrimitive,
450 isRegExp,
451 isString,
452 isSymbol,
453 isUndefined,
454};