Skip to content

Commit bfcfe9a

Browse files
authored
Fill out more of the node:util implementation (#1804)
1 parent fac7b76 commit bfcfe9a

File tree

3 files changed

+237
-13
lines changed

3 files changed

+237
-13
lines changed

src/node/internal/debuglog.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) 2017-2023 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+
//
5+
// Copyright Joyent, Inc. and other Node contributors.
6+
//
7+
// Permission is hereby granted, free of charge, to any person obtaining a
8+
// copy of this software and associated documentation files (the
9+
// "Software"), to deal in the Software without restriction, including
10+
// without limitation the rights to use, copy, modify, merge, publish,
11+
// distribute, sublicense, and/or sell copies of the Software, and to permit
12+
// persons to whom the Software is furnished to do so, subject to the
13+
// following conditions:
14+
//
15+
// The above copyright notice and this permission notice shall be included
16+
// in all copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
25+
/* todo: the following is adopted code, enabling linting one day */
26+
/* eslint-disable */
27+
28+
import {
29+
format,
30+
formatWithOptions,
31+
} from 'node-internal:internal_inspect';
32+
33+
let debugImpls : object = {};
34+
35+
function debuglogImpl(set: string) {
36+
if ((debugImpls as any)[set] === undefined) {
37+
(debugImpls as any)[set] = function debug(...args : any[]) {
38+
const msg = formatWithOptions({ }, ...args);
39+
console.log(format('%s: %s\n', set, msg));
40+
};
41+
}
42+
return (debugImpls as any)[set];
43+
}
44+
45+
// In Node.js' implementation, debuglog availability is determined by the NODE_DEBUG
46+
// environment variable. However, we don't have access to the environment variables
47+
// in the same way. Instead, we'll just always enable debuglog on the requested sets.
48+
export function debuglog(set : string, cb? : (debug : (...args : any[]) => void) => void) {
49+
function init() {
50+
set = set.toUpperCase();
51+
}
52+
let debug = (...args : any[]) => {
53+
init();
54+
debug = debuglogImpl(set);
55+
if (typeof cb === 'function') {
56+
cb(debug);
57+
}
58+
switch (args.length) {
59+
case 1: return debug(args[0]);
60+
case 2: return debug(args[0], args[1]);
61+
default: return debug(...args);
62+
}
63+
};
64+
const logger = (...args : any[]) => {
65+
switch (args.length) {
66+
case 1: return debug(args[0]);
67+
case 2: return debug(args[0], args[1]);
68+
default: return debug(...args);
69+
}
70+
};
71+
Object.defineProperty(logger, 'enabled', {
72+
get() { return true; },
73+
configurable: true,
74+
enumerable: true,
75+
});
76+
return logger;
77+
}

src/node/util.ts

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@ import { default as internalTypes } from 'node-internal:internal_types';
99
import { default as utilImpl } from 'node-internal:util';
1010

1111
import {
12-
validateFunction
12+
validateFunction,
13+
validateAbortSignal,
14+
validateObject,
1315
} from 'node-internal:validators';
1416

17+
import {
18+
debuglog,
19+
} from 'node-internal:debuglog';
20+
export const debug = debuglog;
21+
export { debuglog };
22+
1523
import {
1624
ERR_FALSY_VALUE_REJECTION,
1725
ERR_INVALID_ARG_TYPE,
@@ -171,6 +179,74 @@ export function _extend(target: Object, source: Object) {
171179
return target;
172180
}
173181

182+
export const TextDecoder = globalThis.TextDecoder;
183+
export const TextEncoder = globalThis.TextEncoder;
184+
185+
export function toUSVString(input : any) {
186+
// TODO(cleanup): Apparently the typescript types for this aren't available yet?
187+
return (`${input}` as any).toWellFormed();
188+
}
189+
190+
function pad(n: any) : string {
191+
return `${n}`.padStart(2, '0');
192+
}
193+
194+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
195+
'Oct', 'Nov', 'Dec'];
196+
197+
function timestamp() : string {
198+
const d = new Date();
199+
const t = [
200+
pad(d.getHours()),
201+
pad(d.getMinutes()),
202+
pad(d.getSeconds()),
203+
].join(':');
204+
return `${d.getDate()} ${months[d.getMonth()]} ${t}`;
205+
}
206+
207+
export function log(...args : any[]) {
208+
console.log('%s - %s', timestamp(), format(...args));
209+
}
210+
211+
export function parseArgs(..._ : any[]) : any {
212+
// We currently have no plans to implement the util.parseArgs API.
213+
throw new Error('node:util parseArgs is not implemented');
214+
}
215+
216+
export function transferableAbortController(..._ : any[]) : any {
217+
throw new Error('node:util transferableAbortController is not implemented');
218+
}
219+
220+
export function transferableAbortSignal(..._ : any[]) : any {
221+
throw new Error('node:util transferableAbortSignal is not implemented');
222+
}
223+
224+
export async function aborted(signal: AbortSignal, resource: object) {
225+
if (signal === undefined) {
226+
throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
227+
}
228+
// Node.js defines that the resource is held weakly such that if it is gc'd, we
229+
// will drop the event handler on the signal and the promise will remain pending
230+
// forever. We don't want gc to be observable in the same way so we won't support
231+
// this additional option. Unfortunately Node.js does not make this argument optional.
232+
// We'll just ignore it.
233+
validateAbortSignal(signal, 'signal');
234+
validateObject(resource, 'resource', { allowArray: true, allowFunction: true });
235+
if (signal.aborted) return Promise.resolve();
236+
// TODO(cleanup): Apparently withResolvers isn't part of type defs we use yet
237+
const { promise, resolve } = (Promise as any).withResolvers();
238+
const opts = { __proto__: null, once: true };
239+
signal.addEventListener('abort', resolve, opts);
240+
return promise;
241+
}
242+
243+
export function deprecate(fn: Function, _1?: string, _2?: string, _3? : boolean) {
244+
// TODO(soon): Node.js's implementation wraps the given function in a new function that
245+
// logs a warning to the console if the function is called. Do we want to support that?
246+
// For now, we're just going to silently return the input method unmodified.
247+
return fn;
248+
}
249+
174250
export default {
175251
types,
176252
callbackify,
@@ -183,18 +259,42 @@ export default {
183259
_extend,
184260
MIMEParams,
185261
MIMEType,
262+
toUSVString,
263+
log,
264+
aborted,
265+
debuglog,
266+
debug,
267+
deprecate,
268+
// Node.js originally exposed TextEncoder and TextDecoder off the util
269+
// module originally, so let's just go ahead and do the same.
270+
TextEncoder,
271+
TextDecoder,
272+
// We currently have no plans to implement the following APIs but we want
273+
// to provide throwing placeholders for them. We may eventually come back
274+
// around and implement these later.
275+
parseArgs,
276+
transferableAbortController,
277+
transferableAbortSignal,
186278
};
187279

188280
// Node.js util APIs we're currently not supporting
189-
// TODO(soon): Revisit these
190-
//
191-
// debug/debuglog -- The semantics of these depend on configuration through environment
192-
// variables to enable specific debug categories. We have no notion
193-
// of that in the runtime currently and it's not yet clear if we should.
194-
// deprecate -- Not clear how broadly this is used in the ecosystem outside of node.js
195-
// getSystemErrorMap/getSystemErrorName -- libuv specific. No use in workerd?
196-
// is{Type} variants -- these are deprecated in Node. Use util.types
197-
// toUSVString -- Not clear how broadly this is used in the ecosystem outside of node.js.
198-
// also this is soon to be obsoleted by toWellFormed in the language.
199-
// transferableAbortSignal/transferableAbortController -- postMessage and worker threads
200-
// are not implemented in workerd. No use case for these.
281+
// * util._errnoException
282+
// * util._exceptionWithHostPort
283+
// * util.getSystemErrorMap
284+
// * util.getSystemErrorName
285+
// * util.isArray
286+
// * util.isBoolean
287+
// * util.isBuffer
288+
// * util.isDate
289+
// * util.isDeepStrictEqual
290+
// * util.isError
291+
// * util.isFunction
292+
// * util.isNull
293+
// * util.isNullOrUndefined
294+
// * util.isNumber
295+
// * util.isObject
296+
// * util.isPrimitive
297+
// * util.isRegExp
298+
// * util.isString
299+
// * util.isSymbol
300+
// * util.isUndefined

src/workerd/api/node/util-nodejs-test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
// USE OR OTHER DEALINGS IN THE SOFTWARE.
2525

2626
import assert from 'node:assert';
27+
import { mock } from 'node:test';
2728
import util, { inspect } from 'node:util';
2829

2930
const remainingMustCallErrors = new Set();
@@ -3777,3 +3778,49 @@ export const utilInspectError = {
37773778
);
37783779
}
37793780
};
3781+
3782+
export const logTest = {
3783+
test() {
3784+
const original = console.log;
3785+
console.log = mock.fn();
3786+
3787+
util.log('test');
3788+
3789+
assert.strictEqual(console.log.mock.callCount(), 1);
3790+
const args = console.log.mock.calls[0].arguments;
3791+
assert.strictEqual(args.length, 3);
3792+
assert.strictEqual(args[0], '%s - %s');
3793+
// skipping the check on args[1] since it'll be a timestamp that changes
3794+
assert.strictEqual(args[2], 'test');
3795+
3796+
console.log = original;
3797+
}
3798+
};
3799+
3800+
export const aborted = {
3801+
async test() {
3802+
const signal = AbortSignal.timeout(10);
3803+
await util.aborted(signal, {});
3804+
3805+
await assert.rejects(util.aborted({}, {}), {
3806+
message: 'The "signal" argument must be an instance of AbortSignal. ' +
3807+
'Received an instance of Object'
3808+
});
3809+
}
3810+
};
3811+
3812+
export const debuglog = {
3813+
test() {
3814+
const original = console.log;
3815+
console.log = mock.fn();
3816+
3817+
util.debuglog('test')('hello');
3818+
3819+
assert.strictEqual(console.log.mock.callCount(), 1);
3820+
const args = console.log.mock.calls[0].arguments;
3821+
assert.strictEqual(args.length, 1);
3822+
assert.strictEqual(args[0], 'TEST: hello\n');
3823+
3824+
console.log = original;
3825+
}
3826+
};

0 commit comments

Comments
 (0)