Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fill out more of the node:util implementation #1804

Merged
merged 10 commits into from
Mar 22, 2024
77 changes: 77 additions & 0 deletions src/node/internal/debuglog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) 2017-2023 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0
//
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
/* todo: the following is adopted code, enabling linting one day */
/* eslint-disable */

import {
format,
formatWithOptions,
} from 'node-internal:internal_inspect';

let debugImpls : object = {};

function debuglogImpl(set: string) {
if ((debugImpls as any)[set] === undefined) {
(debugImpls as any)[set] = function debug(...args : any[]) {
const msg = formatWithOptions({ }, ...args);
console.log(format('%s: %s\n', set, msg));
};
}
return (debugImpls as any)[set];
}

// In Node.js' implementation, debuglog availability is determined by the NODE_DEBUG
// environment variable. However, we don't have access to the environment variables
// in the same way. Instead, we'll just always enable debuglog on the requested sets.
export function debuglog(set : string, cb? : (debug : (...args : any[]) => void) => void) {
function init() {
set = set.toUpperCase();
}
let debug = (...args : any[]) => {
init();
debug = debuglogImpl(set);
if (typeof cb === 'function') {
cb(debug);
}
switch (args.length) {
case 1: return debug(args[0]);
case 2: return debug(args[0], args[1]);
default: return debug(...args);
}
};
const logger = (...args : any[]) => {
switch (args.length) {
case 1: return debug(args[0]);
case 2: return debug(args[0], args[1]);
default: return debug(...args);
}
};
Object.defineProperty(logger, 'enabled', {
get() { return true; },
configurable: true,
enumerable: true,
});
return logger;
}
126 changes: 113 additions & 13 deletions src/node/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@ import { default as internalTypes } from 'node-internal:internal_types';
import { default as utilImpl } from 'node-internal:util';

import {
validateFunction
validateFunction,
validateAbortSignal,
validateObject,
} from 'node-internal:validators';

import {
debuglog,
} from 'node-internal:debuglog';
export const debug = debuglog;
export { debuglog };

import {
ERR_FALSY_VALUE_REJECTION,
ERR_INVALID_ARG_TYPE,
Expand Down Expand Up @@ -171,6 +179,74 @@ export function _extend(target: Object, source: Object) {
return target;
}

export const TextDecoder = globalThis.TextDecoder;
export const TextEncoder = globalThis.TextEncoder;

export function toUSVString(input : any) {
// TODO(cleanup): Apparently the typescript types for this aren't available yet?
return (`${input}` as any).toWellFormed();
}

function pad(n: any) : string {
return `${n}`.padStart(2, '0');
}

const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
'Oct', 'Nov', 'Dec'];

function timestamp() : string {
const d = new Date();
const t = [
pad(d.getHours()),
pad(d.getMinutes()),
pad(d.getSeconds()),
].join(':');
return `${d.getDate()} ${months[d.getMonth()]} ${t}`;
}

export function log(...args : any[]) {
console.log('%s - %s', timestamp(), format(...args));
}

export function parseArgs(..._ : any[]) : any {
// We currently have no plans to implement the util.parseArgs API.
throw new Error('node:util parseArgs is not implemented');
}

export function transferableAbortController(..._ : any[]) : any {
throw new Error('node:util transferableAbortController is not implemented');
}

export function transferableAbortSignal(..._ : any[]) : any {
throw new Error('node:util transferableAbortSignal is not implemented');
}

export async function aborted(signal: AbortSignal, resource: object) {
if (signal === undefined) {
throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
}
// Node.js defines that the resource is held weakly such that if it is gc'd, we
// will drop the event handler on the signal and the promise will remain pending
// forever. We don't want gc to be observable in the same way so we won't support
// this additional option. Unfortunately Node.js does not make this argument optional.
// We'll just ignore it.
validateAbortSignal(signal, 'signal');
validateObject(resource, 'resource', { allowArray: true, allowFunction: true });
if (signal.aborted) return Promise.resolve();
// TODO(cleanup): Apparently withResolvers isn't part of type defs we use yet
const { promise, resolve } = (Promise as any).withResolvers();
const opts = { __proto__: null, once: true };
signal.addEventListener('abort', resolve, opts);
return promise;
}

export function deprecate(fn: Function, _1?: string, _2?: string, _3? : boolean) {
// TODO(soon): Node.js's implementation wraps the given function in a new function that
// logs a warning to the console if the function is called. Do we want to support that?
// For now, we're just going to silently return the input method unmodified.
return fn;
}

export default {
types,
callbackify,
Expand All @@ -183,18 +259,42 @@ export default {
_extend,
MIMEParams,
MIMEType,
toUSVString,
log,
aborted,
debuglog,
debug,
deprecate,
// Node.js originally exposed TextEncoder and TextDecoder off the util
// module originally, so let's just go ahead and do the same.
TextEncoder,
TextDecoder,
// We currently have no plans to implement the following APIs but we want
// to provide throwing placeholders for them. We may eventually come back
// around and implement these later.
parseArgs,
transferableAbortController,
transferableAbortSignal,
};

// Node.js util APIs we're currently not supporting
// TODO(soon): Revisit these
//
// debug/debuglog -- The semantics of these depend on configuration through environment
// variables to enable specific debug categories. We have no notion
// of that in the runtime currently and it's not yet clear if we should.
// deprecate -- Not clear how broadly this is used in the ecosystem outside of node.js
// getSystemErrorMap/getSystemErrorName -- libuv specific. No use in workerd?
// is{Type} variants -- these are deprecated in Node. Use util.types
// toUSVString -- Not clear how broadly this is used in the ecosystem outside of node.js.
// also this is soon to be obsoleted by toWellFormed in the language.
// transferableAbortSignal/transferableAbortController -- postMessage and worker threads
// are not implemented in workerd. No use case for these.
// * util._errnoException
// * util._exceptionWithHostPort
// * util.getSystemErrorMap
// * util.getSystemErrorName
jasnell marked this conversation as resolved.
Show resolved Hide resolved
// * util.isArray
// * util.isBoolean
// * util.isBuffer
// * util.isDate
// * util.isDeepStrictEqual
// * util.isError
// * util.isFunction
// * util.isNull
// * util.isNullOrUndefined
// * util.isNumber
// * util.isObject
// * util.isPrimitive
// * util.isRegExp
// * util.isString
// * util.isSymbol
// * util.isUndefined
jasnell marked this conversation as resolved.
Show resolved Hide resolved
47 changes: 47 additions & 0 deletions src/workerd/api/node/util-nodejs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
// USE OR OTHER DEALINGS IN THE SOFTWARE.

import assert from 'node:assert';
import { mock } from 'node:test';
import util, { inspect } from 'node:util';

const remainingMustCallErrors = new Set();
Expand Down Expand Up @@ -3777,3 +3778,49 @@ export const utilInspectError = {
);
}
};

export const logTest = {
test() {
const original = console.log;
console.log = mock.fn();

util.log('test');

assert.strictEqual(console.log.mock.callCount(), 1);
const args = console.log.mock.calls[0].arguments;
assert.strictEqual(args.length, 3);
assert.strictEqual(args[0], '%s - %s');
// skipping the check on args[1] since it'll be a timestamp that changes
assert.strictEqual(args[2], 'test');

console.log = original;
}
};

export const aborted = {
async test() {
const signal = AbortSignal.timeout(10);
await util.aborted(signal, {});

await assert.rejects(util.aborted({}, {}), {
message: 'The "signal" argument must be an instance of AbortSignal. ' +
'Received an instance of Object'
});
}
};

export const debuglog = {
test() {
const original = console.log;
console.log = mock.fn();

util.debuglog('test')('hello');

assert.strictEqual(console.log.mock.callCount(), 1);
const args = console.log.mock.calls[0].arguments;
assert.strictEqual(args.length, 1);
assert.strictEqual(args[0], 'TEST: hello\n');

console.log = original;
}
};
Loading