@logtape/logtape 1.2.0-dev.354 → 1.2.0-dev.359
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist/logger.cjs +224 -4
- package/dist/logger.js +224 -4
- package/dist/logger.js.map +1 -1
- package/package.json +1 -1
- package/src/logger.test.ts +456 -0
- package/src/logger.ts +305 -2
package/src/logger.ts
CHANGED
|
@@ -1381,11 +1381,309 @@ export class LoggerCtx implements Logger {
|
|
|
1381
1381
|
*/
|
|
1382
1382
|
const metaLogger = LoggerImpl.getLogger(["logtape", "meta"]);
|
|
1383
1383
|
|
|
1384
|
+
/**
|
|
1385
|
+
* Check if a property access key contains nested access patterns.
|
|
1386
|
+
* @param key The property key to check.
|
|
1387
|
+
* @returns True if the key contains nested access patterns.
|
|
1388
|
+
*/
|
|
1389
|
+
function isNestedAccess(key: string): boolean {
|
|
1390
|
+
return key.includes(".") || key.includes("[") || key.includes("?.");
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Safely access an own property from an object, blocking prototype pollution.
|
|
1395
|
+
*
|
|
1396
|
+
* @param obj The object to access the property from.
|
|
1397
|
+
* @param key The property key to access.
|
|
1398
|
+
* @returns The property value or undefined if not accessible.
|
|
1399
|
+
*/
|
|
1400
|
+
function getOwnProperty(obj: unknown, key: string): unknown {
|
|
1401
|
+
// Block dangerous prototype keys
|
|
1402
|
+
if (key === "__proto__" || key === "prototype" || key === "constructor") {
|
|
1403
|
+
return undefined;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
if ((typeof obj === "object" || typeof obj === "function") && obj !== null) {
|
|
1407
|
+
return Object.prototype.hasOwnProperty.call(obj, key)
|
|
1408
|
+
? (obj as Record<string, unknown>)[key]
|
|
1409
|
+
: undefined;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
return undefined;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Result of parsing a single segment from a property path.
|
|
1417
|
+
*/
|
|
1418
|
+
interface ParseSegmentResult {
|
|
1419
|
+
segment: string | number;
|
|
1420
|
+
nextIndex: number;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* Parse the next segment from a property path string.
|
|
1425
|
+
*
|
|
1426
|
+
* @param path The full property path string.
|
|
1427
|
+
* @param fromIndex The index to start parsing from.
|
|
1428
|
+
* @returns The parsed segment and next index, or null if parsing fails.
|
|
1429
|
+
*/
|
|
1430
|
+
function parseNextSegment(
|
|
1431
|
+
path: string,
|
|
1432
|
+
fromIndex: number,
|
|
1433
|
+
): ParseSegmentResult | null {
|
|
1434
|
+
const len = path.length;
|
|
1435
|
+
let i = fromIndex;
|
|
1436
|
+
|
|
1437
|
+
if (i >= len) return null;
|
|
1438
|
+
|
|
1439
|
+
let segment: string | number;
|
|
1440
|
+
|
|
1441
|
+
if (path[i] === "[") {
|
|
1442
|
+
// Bracket notation: [0] or ["prop"]
|
|
1443
|
+
i++;
|
|
1444
|
+
if (i >= len) return null;
|
|
1445
|
+
|
|
1446
|
+
if (path[i] === '"' || path[i] === "'") {
|
|
1447
|
+
// Quoted property name: ["prop-name"]
|
|
1448
|
+
const quote = path[i];
|
|
1449
|
+
i++;
|
|
1450
|
+
// Build segment with proper escape handling
|
|
1451
|
+
let segmentStr = "";
|
|
1452
|
+
while (i < len && path[i] !== quote) {
|
|
1453
|
+
if (path[i] === "\\") {
|
|
1454
|
+
i++; // Skip backslash
|
|
1455
|
+
if (i < len) {
|
|
1456
|
+
// Handle escape sequences according to JavaScript spec
|
|
1457
|
+
const escapeChar = path[i];
|
|
1458
|
+
switch (escapeChar) {
|
|
1459
|
+
case "n":
|
|
1460
|
+
segmentStr += "\n";
|
|
1461
|
+
break;
|
|
1462
|
+
case "t":
|
|
1463
|
+
segmentStr += "\t";
|
|
1464
|
+
break;
|
|
1465
|
+
case "r":
|
|
1466
|
+
segmentStr += "\r";
|
|
1467
|
+
break;
|
|
1468
|
+
case "b":
|
|
1469
|
+
segmentStr += "\b";
|
|
1470
|
+
break;
|
|
1471
|
+
case "f":
|
|
1472
|
+
segmentStr += "\f";
|
|
1473
|
+
break;
|
|
1474
|
+
case "v":
|
|
1475
|
+
segmentStr += "\v";
|
|
1476
|
+
break;
|
|
1477
|
+
case "0":
|
|
1478
|
+
segmentStr += "\0";
|
|
1479
|
+
break;
|
|
1480
|
+
case "\\":
|
|
1481
|
+
segmentStr += "\\";
|
|
1482
|
+
break;
|
|
1483
|
+
case '"':
|
|
1484
|
+
segmentStr += '"';
|
|
1485
|
+
break;
|
|
1486
|
+
case "'":
|
|
1487
|
+
segmentStr += "'";
|
|
1488
|
+
break;
|
|
1489
|
+
case "u":
|
|
1490
|
+
// Unicode escape: \uXXXX
|
|
1491
|
+
if (i + 4 < len) {
|
|
1492
|
+
const hex = path.slice(i + 1, i + 5);
|
|
1493
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
1494
|
+
if (!Number.isNaN(codePoint)) {
|
|
1495
|
+
segmentStr += String.fromCharCode(codePoint);
|
|
1496
|
+
i += 4; // Skip the 4 hex digits
|
|
1497
|
+
} else {
|
|
1498
|
+
// Invalid unicode escape, keep as-is
|
|
1499
|
+
segmentStr += escapeChar;
|
|
1500
|
+
}
|
|
1501
|
+
} else {
|
|
1502
|
+
// Not enough characters for unicode escape
|
|
1503
|
+
segmentStr += escapeChar;
|
|
1504
|
+
}
|
|
1505
|
+
break;
|
|
1506
|
+
default:
|
|
1507
|
+
// For any other character after \, just add it as-is
|
|
1508
|
+
segmentStr += escapeChar;
|
|
1509
|
+
}
|
|
1510
|
+
i++;
|
|
1511
|
+
}
|
|
1512
|
+
} else {
|
|
1513
|
+
segmentStr += path[i];
|
|
1514
|
+
i++;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
if (i >= len) return null;
|
|
1518
|
+
segment = segmentStr;
|
|
1519
|
+
i++; // Skip closing quote
|
|
1520
|
+
} else {
|
|
1521
|
+
// Array index: [0]
|
|
1522
|
+
const startIndex = i;
|
|
1523
|
+
while (
|
|
1524
|
+
i < len && path[i] !== "]" && path[i] !== "'" && path[i] !== '"'
|
|
1525
|
+
) {
|
|
1526
|
+
i++;
|
|
1527
|
+
}
|
|
1528
|
+
if (i >= len) return null;
|
|
1529
|
+
const indexStr = path.slice(startIndex, i);
|
|
1530
|
+
// Empty bracket is invalid
|
|
1531
|
+
if (indexStr.length === 0) return null;
|
|
1532
|
+
const indexNum = Number(indexStr);
|
|
1533
|
+
segment = Number.isNaN(indexNum) ? indexStr : indexNum;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Skip closing bracket
|
|
1537
|
+
while (i < len && path[i] !== "]") i++;
|
|
1538
|
+
if (i < len) i++;
|
|
1539
|
+
} else {
|
|
1540
|
+
// Dot notation: prop
|
|
1541
|
+
const startIndex = i;
|
|
1542
|
+
while (
|
|
1543
|
+
i < len && path[i] !== "." && path[i] !== "[" && path[i] !== "?" &&
|
|
1544
|
+
path[i] !== "]"
|
|
1545
|
+
) {
|
|
1546
|
+
i++;
|
|
1547
|
+
}
|
|
1548
|
+
segment = path.slice(startIndex, i);
|
|
1549
|
+
// Empty segment is invalid (e.g., leading dot, double dot, trailing dot)
|
|
1550
|
+
if (segment.length === 0) return null;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Skip dot separator
|
|
1554
|
+
if (i < len && path[i] === ".") i++;
|
|
1555
|
+
|
|
1556
|
+
return { segment, nextIndex: i };
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/**
|
|
1560
|
+
* Access a property or index on an object or array.
|
|
1561
|
+
*
|
|
1562
|
+
* @param obj The object or array to access.
|
|
1563
|
+
* @param segment The property key or array index.
|
|
1564
|
+
* @returns The accessed value or undefined if not accessible.
|
|
1565
|
+
*/
|
|
1566
|
+
function accessProperty(obj: unknown, segment: string | number): unknown {
|
|
1567
|
+
if (typeof segment === "string") {
|
|
1568
|
+
return getOwnProperty(obj, segment);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Numeric index for arrays
|
|
1572
|
+
if (Array.isArray(obj) && segment >= 0 && segment < obj.length) {
|
|
1573
|
+
return obj[segment];
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
return undefined;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* Resolve a nested property path from an object.
|
|
1581
|
+
*
|
|
1582
|
+
* There are two types of property access patterns:
|
|
1583
|
+
* 1. Array/index access: [0] or ["prop"]
|
|
1584
|
+
* 2. Property access: prop or prop?.next
|
|
1585
|
+
*
|
|
1586
|
+
* @param obj The object to traverse.
|
|
1587
|
+
* @param path The property path (e.g., "user.name", "users[0].email", "user['full-name']").
|
|
1588
|
+
* @returns The resolved value or undefined if path doesn't exist.
|
|
1589
|
+
*/
|
|
1590
|
+
function resolvePropertyPath(obj: unknown, path: string): unknown {
|
|
1591
|
+
if (obj == null) return undefined;
|
|
1592
|
+
|
|
1593
|
+
// Check for invalid paths
|
|
1594
|
+
if (path.length === 0 || path.endsWith(".")) return undefined;
|
|
1595
|
+
|
|
1596
|
+
let current: unknown = obj;
|
|
1597
|
+
let i = 0;
|
|
1598
|
+
const len = path.length;
|
|
1599
|
+
|
|
1600
|
+
while (i < len) {
|
|
1601
|
+
// Handle optional chaining
|
|
1602
|
+
const isOptional = path.slice(i, i + 2) === "?.";
|
|
1603
|
+
if (isOptional) {
|
|
1604
|
+
i += 2;
|
|
1605
|
+
if (current == null) return undefined;
|
|
1606
|
+
} else if (current == null) {
|
|
1607
|
+
return undefined;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Parse the next segment
|
|
1611
|
+
const result = parseNextSegment(path, i);
|
|
1612
|
+
if (result === null) return undefined;
|
|
1613
|
+
|
|
1614
|
+
const { segment, nextIndex } = result;
|
|
1615
|
+
i = nextIndex;
|
|
1616
|
+
|
|
1617
|
+
// Access the property/index
|
|
1618
|
+
current = accessProperty(current, segment);
|
|
1619
|
+
if (current === undefined) {
|
|
1620
|
+
return undefined;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
return current;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1384
1627
|
/**
|
|
1385
1628
|
* Parse a message template into a message template array and a values array.
|
|
1386
|
-
*
|
|
1629
|
+
*
|
|
1630
|
+
* Placeholders to be replaced with `values` are indicated by keys in curly braces
|
|
1631
|
+
* (e.g., `{value}`). The system supports both simple property access and nested
|
|
1632
|
+
* property access patterns:
|
|
1633
|
+
*
|
|
1634
|
+
* **Simple property access:**
|
|
1635
|
+
* ```ts
|
|
1636
|
+
* parseMessageTemplate("Hello, {user}!", { user: "foo" })
|
|
1637
|
+
* // Returns: ["Hello, ", "foo", "!"]
|
|
1638
|
+
* ```
|
|
1639
|
+
*
|
|
1640
|
+
* **Nested property access (dot notation):**
|
|
1641
|
+
* ```ts
|
|
1642
|
+
* parseMessageTemplate("Hello, {user.name}!", {
|
|
1643
|
+
* user: { name: "foo", email: "foo@example.com" }
|
|
1644
|
+
* })
|
|
1645
|
+
* // Returns: ["Hello, ", "foo", "!"]
|
|
1646
|
+
* ```
|
|
1647
|
+
*
|
|
1648
|
+
* **Array indexing:**
|
|
1649
|
+
* ```ts
|
|
1650
|
+
* parseMessageTemplate("First: {users[0]}", {
|
|
1651
|
+
* users: ["foo", "bar", "baz"]
|
|
1652
|
+
* })
|
|
1653
|
+
* // Returns: ["First: ", "foo", ""]
|
|
1654
|
+
* ```
|
|
1655
|
+
*
|
|
1656
|
+
* **Bracket notation for special property names:**
|
|
1657
|
+
* ```ts
|
|
1658
|
+
* parseMessageTemplate("Name: {user[\"full-name\"]}", {
|
|
1659
|
+
* user: { "full-name": "foo bar" }
|
|
1660
|
+
* })
|
|
1661
|
+
* // Returns: ["Name: ", "foo bar", ""]
|
|
1662
|
+
* ```
|
|
1663
|
+
*
|
|
1664
|
+
* **Optional chaining for safe navigation:**
|
|
1665
|
+
* ```ts
|
|
1666
|
+
* parseMessageTemplate("Email: {user?.profile?.email}", {
|
|
1667
|
+
* user: { name: "foo" }
|
|
1668
|
+
* })
|
|
1669
|
+
* // Returns: ["Email: ", undefined, ""]
|
|
1670
|
+
* ```
|
|
1671
|
+
*
|
|
1672
|
+
* **Wildcard patterns:**
|
|
1673
|
+
* - `{*}` - Replaced with the entire properties object
|
|
1674
|
+
* - `{ key-with-whitespace }` - Whitespace is trimmed when looking up keys
|
|
1675
|
+
*
|
|
1676
|
+
* **Escaping:**
|
|
1677
|
+
* - `{{` and `}}` are escaped literal braces
|
|
1678
|
+
*
|
|
1679
|
+
* **Error handling:**
|
|
1680
|
+
* - Non-existent paths return `undefined`
|
|
1681
|
+
* - Malformed expressions resolve to `undefined` without throwing errors
|
|
1682
|
+
* - Out of bounds array access returns `undefined`
|
|
1683
|
+
*
|
|
1684
|
+
* @param template The message template string containing placeholders.
|
|
1387
1685
|
* @param properties The values to replace placeholders with.
|
|
1388
|
-
* @returns The message template array
|
|
1686
|
+
* @returns The message template array with values interleaved between text segments.
|
|
1389
1687
|
*/
|
|
1390
1688
|
export function parseMessageTemplate(
|
|
1391
1689
|
template: string,
|
|
@@ -1447,6 +1745,11 @@ export function parseMessageTemplate(
|
|
|
1447
1745
|
// Key has no leading/trailing whitespace
|
|
1448
1746
|
prop = properties[key];
|
|
1449
1747
|
}
|
|
1748
|
+
|
|
1749
|
+
// If property not found directly and this looks like nested access, try nested resolution
|
|
1750
|
+
if (prop === undefined && isNestedAccess(trimmedKey)) {
|
|
1751
|
+
prop = resolvePropertyPath(properties, trimmedKey);
|
|
1752
|
+
}
|
|
1450
1753
|
}
|
|
1451
1754
|
|
|
1452
1755
|
message.push(prop);
|