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

FED-466: Fix console logs that require formatting #52

Merged
merged 14 commits into from
Oct 20, 2022
Merged
87 changes: 87 additions & 0 deletions lib/src/util/console_log_formatter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// @dart = 2.7

// This code was adapted to Dart from
// https://github.com/nodejs/node-v0.x-archive/blob/master/lib/util.js
//
// 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.

@JS()
library console_log_formatter;

import 'package:js/js.dart';

@JS('JSON.stringify')
external String _jsonStringify(dynamic object);

final _formatRegExp = RegExp('%[sdifoOj%]');

/// A simple formatter implementation for dart.
///
/// If specifier is:
/// - `%s` (String): results in `arg.toString()`.
/// - `%d` or `%i` (Number): results in `num.tryParse(arg)`.
/// - `%f` (Float/Double): results in `double.tryParse(arg)`.
/// - `%o` or '%O' or '%j' (object/JSON): results in `JSON.stringify(arg)`.
///
/// see: https://console.spec.whatwg.org/#formatter
String format(dynamic f, List<dynamic> arguments) {
if (f is! String) {
return [f, ...arguments].join(' ');
}

var str = '';
if (f is String) {
var i = 0;
final len = arguments.length;
str += f.replaceAllMapped(_formatRegExp, (m) {
final x = m[0];
if (x == '%%') return '%';
if (i >= len) return x;
switch (x) {
case '%s':
return arguments[i++].toString();
case '%i':
case '%d':
case '%f':
return num.tryParse(arguments[i++].toString()).toString();
case '%o':
case '%O':
case '%j':
try {
final argToStringify = arguments[i++];
return _jsonStringify(argToStringify);
} catch (_) {
return '[Circular]';
}
break;
default:
return x;
}
});

if (i < len) {
str += ' ${arguments.skip(i).join(' ')}';
}
}

return str;
}
78 changes: 69 additions & 9 deletions lib/src/util/console_log_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.

@JS()
library console_log_utils;

import 'dart:async';
import 'dart:html';
import 'dart:js';
import 'dart:js_util';

import 'package:js/js.dart';
import 'package:meta/meta.dart';
import 'package:react/react_client/react_interop.dart';

import 'console_log_formatter.dart';

/// Runs a provided [callback] and afterwards [print]s each log that occurs during the runtime
/// of that function.
///
Expand Down Expand Up @@ -89,26 +97,51 @@ void Function() startSpyingOnConsoleLogs({
@required void Function(String) onLog,
}) {
final logTypeToCapture = configuration.logType == 'all' ? ConsoleConfig.types : [configuration.logType];
final consoleRefs = <String, JsFunction>{};
final consoleRefs = <String, /* JavascriptFunction */ dynamic>{};
final consolePropertyDescriptors = <String, dynamic>{};
final _console = getProperty(window, 'console');

_resetPropTypeWarningCache();

// Bind to the current zone so the callback isn't called in the top-level zone.
final boundOnLog = Zone.current.bindUnaryCallback(onLog);

for (final config in logTypeToCapture) {
consoleRefs[config] = context['console'][config] as JsFunction;
context['console'][config] = JsFunction.withThis((self, [message, arg1, arg2, arg3, arg4, arg5]) {
// NOTE: Using console.log or print within this function will cause an infinite
// loop when the logType is set to `log`.
boundOnLog(message?.toString());
consoleRefs[config].apply([message, arg1, arg2, arg3, arg4, arg5], thisArg: self);
});
consolePropertyDescriptors[config] = _getOwnPropertyDescriptor(_console, config);
consoleRefs[config] = getProperty(_console, config);
final newDescriptor = _assign(
newObject(),
consolePropertyDescriptors[config],
jsify({
'value': allowInteropCaptureThis((
self, [
message,
arg1 = _undefined,
arg2 = _undefined,
arg3 = _undefined,
arg4 = _undefined,
arg5 = _undefined,
arg6 = _undefined,
arg7 = _undefined,
arg8 = _undefined,
arg9 = _undefined,
arg10 = _undefined,
]) {
// NOTE: Using console.log or print within this function will cause an infinite
// loop when the logType is set to `log`.
final args = [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10]
..removeWhere((arg) => arg == _undefined);
boundOnLog(format(message, args));
_jsFunctionApply(consoleRefs[config], [message, ...args], thisArg: self);
})
}),
);
_defineProperty(_console, config, newDescriptor);
}

void stopSpying() {
for (final config in logTypeToCapture) {
context['console'][config] = consoleRefs[config];
_defineProperty(_console, config, consolePropertyDescriptors[config]);
}
}

Expand Down Expand Up @@ -152,3 +185,30 @@ class ConsoleConfig {
/// Captures calls to `console.log`, `console.warn`, and `console.error`.
static const ConsoleConfig all = ConsoleConfig._('all');
}

dynamic _jsFunctionApply(dynamic jsFunction, List<dynamic> args, {dynamic thisArg}) {
return callMethod(jsFunction, 'apply', [thisArg, jsify(args)]);
}

const _undefined = _Undefined();

/// Represents and unused argument
class _Undefined {
const _Undefined();
}

@JS('Object.assign')
external dynamic _assign(dynamic object, dynamic otherObject, [dynamic anotherObject]);

@JS('Object.getOwnPropertyDescriptor')
external _PropertyDescriptor _getOwnPropertyDescriptor(dynamic object, String propertyName);

@JS('Object.defineProperty')
external void _defineProperty(dynamic object, String propertyName, dynamic descriptor);

@JS('Object.prototype.hasOwnProperty')
external bool _hasOwnProperty(dynamic object, String name);

@JS()
@anonymous
class _PropertyDescriptor {}
Loading