@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.
@@ -820,6 +820,462 @@ test("parseMessageTemplate()", () => {
820
820
  parseMessageTemplate("Hello, {{world!", { foo: 123 }),
821
821
  ["Hello, {world!"],
822
822
  );
823
+ assertEquals(
824
+ parseMessageTemplate("Hello, {user.name}!", {
825
+ user: { name: "foo", email: "foo@example.com" },
826
+ }),
827
+ ["Hello, ", "foo", "!"],
828
+ );
829
+ assertEquals(
830
+ parseMessageTemplate("Email: {user.email}", {
831
+ user: { name: "foo", email: "foo@example.com" },
832
+ }),
833
+ ["Email: ", "foo@example.com", ""],
834
+ );
835
+ assertEquals(
836
+ parseMessageTemplate("Tier: {order.customer.profile.tier}", {
837
+ order: {
838
+ customer: {
839
+ profile: {
840
+ tier: "premium",
841
+ },
842
+ },
843
+ },
844
+ }),
845
+ ["Tier: ", "premium", ""],
846
+ );
847
+ assertEquals(
848
+ parseMessageTemplate("Missing: {user.email}", {
849
+ user: { name: "foo" },
850
+ }),
851
+ ["Missing: ", undefined, ""],
852
+ );
853
+ assertEquals(
854
+ parseMessageTemplate("Deep missing: {user.profile.email}", {
855
+ user: { name: "foo" },
856
+ }),
857
+ ["Deep missing: ", undefined, ""],
858
+ );
859
+ assertEquals(
860
+ parseMessageTemplate("First user: {users[0]}", {
861
+ users: ["foo", "bar", "baz"],
862
+ }),
863
+ ["First user: ", "foo", ""],
864
+ );
865
+ assertEquals(
866
+ parseMessageTemplate("Third user: {users[2]}", {
867
+ users: ["foo", "bar", "baz"],
868
+ }),
869
+ ["Third user: ", "baz", ""],
870
+ );
871
+ assertEquals(
872
+ parseMessageTemplate("Admin: {users[0].name}", {
873
+ users: [
874
+ { name: "foo", role: "admin" },
875
+ { name: "bar", role: "user" },
876
+ ],
877
+ }),
878
+ ["Admin: ", "foo", ""],
879
+ );
880
+ assertEquals(
881
+ parseMessageTemplate("User role: {users[1].role}", {
882
+ users: [
883
+ { name: "foo", role: "admin" },
884
+ { name: "bar", role: "user" },
885
+ ],
886
+ }),
887
+ ["User role: ", "user", ""],
888
+ );
889
+ assertEquals(
890
+ parseMessageTemplate("Beyond: {users[5]}", {
891
+ users: ["foo", "bar"],
892
+ }),
893
+ ["Beyond: ", undefined, ""],
894
+ );
895
+ assertEquals(
896
+ parseMessageTemplate("Invalid: {user[0]}", {
897
+ user: "foo",
898
+ }),
899
+ ["Invalid: ", undefined, ""],
900
+ );
901
+ assertEquals(
902
+ parseMessageTemplate('Full name: {user["full-name"]}', {
903
+ user: { "full-name": "foo bar", "user-id": 123 },
904
+ }),
905
+ ["Full name: ", "foo bar", ""],
906
+ );
907
+ assertEquals(
908
+ parseMessageTemplate('User ID: {user["user-id"]}', {
909
+ user: { "full-name": "foo bar", "user-id": 123 },
910
+ }),
911
+ ["User ID: ", 123, ""],
912
+ );
913
+ assertEquals(
914
+ parseMessageTemplate("Name: {user['full-name']}", {
915
+ user: { "full-name": "foo bar", "nick-name": "fb" },
916
+ }),
917
+ ["Name: ", "foo bar", ""],
918
+ );
919
+ assertEquals(
920
+ parseMessageTemplate('Nick: {user["nick-name"]}', {
921
+ user: { "full-name": "foo bar", "nick-name": "fb" },
922
+ }),
923
+ ["Nick: ", "fb", ""],
924
+ );
925
+ assertEquals(
926
+ parseMessageTemplate('Custom: {data["custom field"]}', {
927
+ data: { "custom field": "value" },
928
+ }),
929
+ ["Custom: ", "value", ""],
930
+ );
931
+ assertEquals(
932
+ parseMessageTemplate('First user: {users[0]["full-name"]}', {
933
+ users: [{ "full-name": "foo bar" }, { "full-name": "bar baz" }],
934
+ }),
935
+ ["First user: ", "foo bar", ""],
936
+ );
937
+ assertEquals(
938
+ parseMessageTemplate("Name: {user?.name}", {
939
+ user: { name: "foo" },
940
+ }),
941
+ ["Name: ", "foo", ""],
942
+ );
943
+ assertEquals(
944
+ parseMessageTemplate("Email: {user?.profile?.email}", {
945
+ user: { name: "foo" },
946
+ }),
947
+ ["Email: ", undefined, ""],
948
+ );
949
+ assertEquals(
950
+ parseMessageTemplate("Item: {data?.items?.[0]?.name}", {
951
+ data: null,
952
+ }),
953
+ ["Item: ", undefined, ""],
954
+ );
955
+ assertEquals(
956
+ parseMessageTemplate("Name: {user?.name}", {
957
+ user: null,
958
+ }),
959
+ ["Name: ", undefined, ""],
960
+ );
961
+ assertEquals(
962
+ parseMessageTemplate(
963
+ 'Email: {users[0]?.profile?.["contact-info"]?.email}',
964
+ {
965
+ users: [
966
+ {
967
+ profile: {
968
+ "contact-info": {
969
+ email: "foo@example.com",
970
+ },
971
+ },
972
+ },
973
+ ],
974
+ },
975
+ ),
976
+ ["Email: ", "foo@example.com", ""],
977
+ );
978
+ assertEquals(
979
+ parseMessageTemplate("Hello, {user}!", {
980
+ user: "foo",
981
+ count: 42,
982
+ }),
983
+ ["Hello, ", "foo", "!"],
984
+ );
985
+ assertEquals(
986
+ parseMessageTemplate("Dot property: {user.name}", {
987
+ "user.name": "foo",
988
+ "user.email": "foo@example.com",
989
+ }),
990
+ ["Dot property: ", "foo", ""],
991
+ );
992
+ assertEquals(
993
+ parseMessageTemplate("All: {*}", {
994
+ user: { name: "foo" },
995
+ count: 42,
996
+ }),
997
+ ["All: ", { user: { name: "foo" }, count: 42 }, ""],
998
+ );
999
+ assertEquals(
1000
+ parseMessageTemplate('Malformed: {user["name}', {
1001
+ user: { name: "foo" },
1002
+ }),
1003
+ ["Malformed: ", undefined, ""],
1004
+ );
1005
+ assertEquals(
1006
+ parseMessageTemplate("Empty: {user[]}", {
1007
+ user: { name: "foo" },
1008
+ }),
1009
+ ["Empty: ", undefined, ""],
1010
+ );
1011
+ assertEquals(
1012
+ parseMessageTemplate("Invalid: {users[abc]}", {
1013
+ users: ["foo", "bar"],
1014
+ }),
1015
+ ["Invalid: ", undefined, ""],
1016
+ );
1017
+ assertEquals(
1018
+ parseMessageTemplate("Protected: {foo.constructor}", {
1019
+ foo: { bar: 123 },
1020
+ }),
1021
+ ["Protected: ", undefined, ""],
1022
+ );
1023
+ assertEquals(
1024
+ parseMessageTemplate("Protected: {foo.prototype}", {
1025
+ foo: { bar: 123 },
1026
+ }),
1027
+ ["Protected: ", undefined, ""],
1028
+ );
1029
+ assertEquals(
1030
+ parseMessageTemplate("Protected: {foo.__proto__}", {
1031
+ foo: { bar: 123 },
1032
+ }),
1033
+ ["Protected: ", undefined, ""],
1034
+ );
1035
+
1036
+ // Boundary conditions
1037
+ assertEquals(parseMessageTemplate("", {}), [""]);
1038
+ assertEquals(
1039
+ parseMessageTemplate("no placeholders", {}),
1040
+ ["no placeholders"],
1041
+ );
1042
+ assertEquals(parseMessageTemplate("{value}", { value: 1 }), ["", 1, ""]);
1043
+ assertEquals(
1044
+ parseMessageTemplate("A {x}{y} B", { x: 1, y: 2 }),
1045
+ ["A ", 1, "", 2, " B"],
1046
+ );
1047
+ assertEquals(
1048
+ parseMessageTemplate("Deep: {a.b.c.d.e}", {
1049
+ a: { b: { c: { d: { e: 5 } } } },
1050
+ }),
1051
+ ["Deep: ", 5, ""],
1052
+ );
1053
+ assertEquals(
1054
+ parseMessageTemplate("2D: {m[1][0]}", { m: [[0, 1], [2, 3]] }),
1055
+ ["2D: ", 2, ""],
1056
+ );
1057
+
1058
+ // Parsing error cases
1059
+ assertEquals(
1060
+ parseMessageTemplate("Missing: {user", { user: 1 }),
1061
+ ["Missing: {user"],
1062
+ );
1063
+ assertEquals(parseMessageTemplate("Extra: user}", {}), ["Extra: user}"]);
1064
+ assertEquals(
1065
+ parseMessageTemplate("Bad: {user.}", { user: { name: "x" } }),
1066
+ ["Bad: ", undefined, ""],
1067
+ );
1068
+ assertEquals(
1069
+ parseMessageTemplate("Bad: {.user}", { user: { name: "x" } }),
1070
+ ["Bad: ", undefined, ""],
1071
+ );
1072
+ assertEquals(
1073
+ parseMessageTemplate("Bad: {user..name}", { user: { name: "x" } }),
1074
+ ["Bad: ", undefined, ""],
1075
+ );
1076
+ assertEquals(
1077
+ parseMessageTemplate("Bad idx: {arr[-1]}", { arr: [1] }),
1078
+ ["Bad idx: ", undefined, ""],
1079
+ );
1080
+ assertEquals(
1081
+ parseMessageTemplate("Float idx: {arr[1.5]}", { arr: [1, 2] }),
1082
+ ["Float idx: ", undefined, ""],
1083
+ );
1084
+ assertEquals(
1085
+ parseMessageTemplate('Bad quote: {user["na\\"}', {
1086
+ user: { 'na"': "v" },
1087
+ }),
1088
+ ["Bad quote: ", undefined, ""],
1089
+ );
1090
+ assertEquals(parseMessageTemplate("Empty: {}", { a: 1 }), [
1091
+ "Empty: ",
1092
+ undefined,
1093
+ "",
1094
+ ]);
1095
+
1096
+ // Type conversion - ensure values are not stringified
1097
+ assertEquals(parseMessageTemplate("Num: {n}", { n: 0 }), ["Num: ", 0, ""]);
1098
+ assertEquals(parseMessageTemplate("Bool: {b}", { b: true }), [
1099
+ "Bool: ",
1100
+ true,
1101
+ "",
1102
+ ]);
1103
+ assertEquals(parseMessageTemplate("Null: {x}", { x: null }), [
1104
+ "Null: ",
1105
+ null,
1106
+ "",
1107
+ ]);
1108
+ assertEquals(parseMessageTemplate("Undef: {x}", {}), [
1109
+ "Undef: ",
1110
+ undefined,
1111
+ "",
1112
+ ]);
1113
+ const testSymbol = Symbol("test");
1114
+ assertEquals(parseMessageTemplate("Sym: {s}", { s: testSymbol }), [
1115
+ "Sym: ",
1116
+ testSymbol,
1117
+ "",
1118
+ ]);
1119
+ assertEquals(parseMessageTemplate("BigInt: {b}", { b: 10n }), [
1120
+ "BigInt: ",
1121
+ 10n,
1122
+ "",
1123
+ ]);
1124
+ const testFn = () => {};
1125
+ assertEquals(parseMessageTemplate("Fn: {fn}", { fn: testFn }), [
1126
+ "Fn: ",
1127
+ testFn,
1128
+ "",
1129
+ ]);
1130
+ const testDate = new Date(0);
1131
+ assertEquals(parseMessageTemplate("Date: {d}", { d: testDate }), [
1132
+ "Date: ",
1133
+ testDate,
1134
+ "",
1135
+ ]);
1136
+ const testRegex = /x/;
1137
+ assertEquals(parseMessageTemplate("RegExp: {r}", { r: testRegex }), [
1138
+ "RegExp: ",
1139
+ testRegex,
1140
+ "",
1141
+ ]);
1142
+ assertEquals(
1143
+ parseMessageTemplate("Nested arr: {a[1][0]}", { a: [[0], [1, 2]] }),
1144
+ ["Nested arr: ", 1, ""],
1145
+ );
1146
+
1147
+ // Complex patterns
1148
+ assertEquals(
1149
+ parseMessageTemplate("Hi {first} {last}", { first: "A", last: "B" }),
1150
+ ["Hi ", "A", " ", "B", ""],
1151
+ );
1152
+ assertEquals(parseMessageTemplate("{a}{b}{c}", { a: 1, b: 2, c: 3 }), [
1153
+ "",
1154
+ 1,
1155
+ "",
1156
+ 2,
1157
+ "",
1158
+ 3,
1159
+ "",
1160
+ ]);
1161
+ assertEquals(
1162
+ parseMessageTemplate("Mix: {users?.[0].profile['full-name']}", {
1163
+ users: [{ profile: { "full-name": "X" } }],
1164
+ }),
1165
+ ["Mix: ", "X", ""],
1166
+ );
1167
+ assertEquals(
1168
+ parseMessageTemplate("Len: {arr.length}", { arr: [1, 2, 3] }),
1169
+ ["Len: ", 3, ""],
1170
+ );
1171
+ assertEquals(
1172
+ parseMessageTemplate("All: {*}, id={id}", { id: 1, a: 2 }),
1173
+ ["All: ", { id: 1, a: 2 }, ", id=", 1, ""],
1174
+ );
1175
+
1176
+ // Security - block dangerous props at any depth
1177
+ assertEquals(
1178
+ parseMessageTemplate("Blocked: {user.profile.constructor}", {
1179
+ user: { profile: {} },
1180
+ }),
1181
+ ["Blocked: ", undefined, ""],
1182
+ );
1183
+ assertEquals(
1184
+ parseMessageTemplate('Blocked: {user["__proto__"]}', { user: {} }),
1185
+ ["Blocked: ", undefined, ""],
1186
+ );
1187
+ assertEquals(
1188
+ parseMessageTemplate('Blocked: {user.profile["constructor"]}', {
1189
+ user: { profile: {} },
1190
+ }),
1191
+ ["Blocked: ", undefined, ""],
1192
+ );
1193
+ assertEquals(
1194
+ parseMessageTemplate('Blocked: {obj["prototype"]}', { obj: {} }),
1195
+ ["Blocked: ", undefined, ""],
1196
+ );
1197
+
1198
+ // Optional chaining variants
1199
+ assertEquals(
1200
+ parseMessageTemplate("Root opt: {arr?.[0]}", { arr: ["x"] }),
1201
+ ["Root opt: ", "x", ""],
1202
+ );
1203
+ assertEquals(
1204
+ parseMessageTemplate("Opt mid: {a?.b.c}", { a: null }),
1205
+ ["Opt mid: ", undefined, ""],
1206
+ );
1207
+ assertEquals(
1208
+ parseMessageTemplate("Opt end: {a.b?.c}", { a: { b: null } }),
1209
+ ["Opt end: ", undefined, ""],
1210
+ );
1211
+ assertEquals(
1212
+ parseMessageTemplate('Opt quoted: {obj?.["k-v"]}', { obj: { "k-v": 1 } }),
1213
+ ["Opt quoted: ", 1, ""],
1214
+ );
1215
+ assertEquals(
1216
+ parseMessageTemplate("Opt after idx: {list[0]?.name}", {
1217
+ list: [{ name: "x" }],
1218
+ }),
1219
+ ["Opt after idx: ", "x", ""],
1220
+ );
1221
+
1222
+ // Unicode and special characters
1223
+ assertEquals(
1224
+ parseMessageTemplate('Emoji: {data["πŸ˜€"]}', { data: { "πŸ˜€": 1 } }),
1225
+ ["Emoji: ", 1, ""],
1226
+ );
1227
+ assertEquals(
1228
+ parseMessageTemplate('Astral: {data["πŸ˜πŸ™"]}', { data: { "πŸ˜πŸ™": "ok" } }),
1229
+ ["Astral: ", "ok", ""],
1230
+ );
1231
+ assertEquals(
1232
+ parseMessageTemplate(String.raw`Quotes: {data["quo\"te"]}`, {
1233
+ data: { 'quo"te': 1 },
1234
+ }),
1235
+ ["Quotes: ", 1, ""],
1236
+ );
1237
+ assertEquals(
1238
+ parseMessageTemplate(String.raw`SQuotes: {data['sin\'gle']}`, {
1239
+ data: { "sin'gle": 2 },
1240
+ }),
1241
+ ["SQuotes: ", 2, ""],
1242
+ );
1243
+ assertEquals(
1244
+ parseMessageTemplate(String.raw`Backslash: {data["back\\slash"]}`, {
1245
+ data: { "back\\slash": 3 },
1246
+ }),
1247
+ ["Backslash: ", 3, ""],
1248
+ );
1249
+ assertEquals(
1250
+ parseMessageTemplate(String.raw`Newline: {data["line\nbreak"]}`, {
1251
+ data: { "line\nbreak": 4 },
1252
+ }),
1253
+ ["Newline: ", 4, ""],
1254
+ );
1255
+ assertEquals(
1256
+ parseMessageTemplate(String.raw`Tab: {data["tab\tseparated"]}`, {
1257
+ data: { "tab\tseparated": 5 },
1258
+ }),
1259
+ ["Tab: ", 5, ""],
1260
+ );
1261
+ assertEquals(
1262
+ parseMessageTemplate(String.raw`Multiple: {data["a\nb\tc"]}`, {
1263
+ data: { "a\nb\tc": 6 },
1264
+ }),
1265
+ ["Multiple: ", 6, ""],
1266
+ );
1267
+ assertEquals(
1268
+ parseMessageTemplate(String.raw`Unicode: {data["smile\u263A"]}`, {
1269
+ data: { "smile☺": 7 },
1270
+ }),
1271
+ ["Unicode: ", 7, ""],
1272
+ );
1273
+ assertEquals(
1274
+ parseMessageTemplate('Dot in key: {data["a.b c"]}', {
1275
+ data: { "a.b c": 1 },
1276
+ }),
1277
+ ["Dot in key: ", 1, ""],
1278
+ );
823
1279
  });
824
1280
 
825
1281
  test("renderMessage()", () => {