@player-tools/metrics-output-plugin 0.12.1-next.1 → 0.12.1-next.3

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.
@@ -778,4 +778,612 @@ describe("WriteMetricsPlugin", () => {
778
778
  // Clean up
779
779
  fs.unlinkSync(outputPath);
780
780
  });
781
+
782
+ test("rootProperties function error is handled and recorded", async () => {
783
+ const service = new PlayerLanguageService();
784
+
785
+ service.addLSPPlugin(
786
+ new MetricsOutput({
787
+ outputDir: TEST_DIR,
788
+ fileName: "rootprops_error",
789
+ rootProperties: () => {
790
+ throw new Error("boom");
791
+ },
792
+ stats: { ok: 1 },
793
+ }),
794
+ );
795
+
796
+ await service.setAssetTypesFromModule([
797
+ Types,
798
+ ReferenceAssetsWebPluginManifest,
799
+ ]);
800
+
801
+ const doc = TextDocument.create("file:///err.json", "json", 1, "{}");
802
+ await service.validateTextDocument(doc);
803
+
804
+ const out = path.join(TEST_DIR, "rootprops_error.json");
805
+ expect(fs.existsSync(out)).toBe(true);
806
+
807
+ const json = JSON.parse(fs.readFileSync(out, "utf-8"));
808
+ // The catch assigns an error object at the root
809
+ expect(json).toHaveProperty("error");
810
+ expect(json.content).toHaveProperty("/err.json");
811
+
812
+ fs.unlinkSync(out);
813
+ });
814
+
815
+ test("defaults outputDir to process.cwd()", async () => {
816
+ const service = new PlayerLanguageService();
817
+ const tempDir = path.resolve("target_default");
818
+ if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
819
+
820
+ const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(tempDir);
821
+
822
+ service.addLSPPlugin(
823
+ new MetricsOutput({
824
+ // no outputDir on purpose
825
+ fileName: "default_dir",
826
+ stats: { a: 1 },
827
+ }),
828
+ );
829
+
830
+ await service.setAssetTypesFromModule([
831
+ Types,
832
+ ReferenceAssetsWebPluginManifest,
833
+ ]);
834
+
835
+ const doc = TextDocument.create("file:///default.json", "json", 1, "{}");
836
+ await service.validateTextDocument(doc);
837
+
838
+ const out = path.join(tempDir, "default_dir.json");
839
+ expect(fs.existsSync(out)).toBe(true);
840
+
841
+ // cleanup
842
+ fs.unlinkSync(out);
843
+ fs.rmdirSync(tempDir);
844
+ cwdSpy.mockRestore();
845
+ });
846
+
847
+ test("defaults fileName to 'metrics' when omitted", async () => {
848
+ const service = new PlayerLanguageService();
849
+ const tempDir = path.resolve("target_default_name");
850
+ if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
851
+
852
+ service.addLSPPlugin(
853
+ new MetricsOutput({
854
+ outputDir: tempDir, // no fileName on purpose
855
+ stats: { a: 1 },
856
+ }),
857
+ );
858
+
859
+ await service.setAssetTypesFromModule([
860
+ Types,
861
+ ReferenceAssetsWebPluginManifest,
862
+ ]);
863
+
864
+ const doc = TextDocument.create(
865
+ "file:///default-name.json",
866
+ "json",
867
+ 1,
868
+ "{}",
869
+ );
870
+ await service.validateTextDocument(doc);
871
+
872
+ expect(fs.existsSync(path.join(tempDir, "metrics.json"))).toBe(true);
873
+
874
+ fs.unlinkSync(path.join(tempDir, "metrics.json"));
875
+ fs.rmdirSync(tempDir);
876
+ });
877
+
878
+ test("treats valid non-object existing metrics as empty", async () => {
879
+ const dir = path.resolve("target_non_object");
880
+ const name = "preexisting_non_object";
881
+ const outPath = path.join(dir, `${name}.json`);
882
+ fs.mkdirSync(dir, { recursive: true });
883
+
884
+ // Seed a valid JSON that is NOT an object → hits the else branch (': {}')
885
+ fs.writeFileSync(outPath, "123", "utf-8"); // could also be "true", "\"str\"", or "null"
886
+
887
+ const service = new PlayerLanguageService();
888
+ service.addLSPPlugin(
889
+ new MetricsOutput({
890
+ outputDir: dir,
891
+ fileName: name,
892
+ stats: { metric: 42 },
893
+ }),
894
+ );
895
+ await service.setAssetTypesFromModule([
896
+ Types,
897
+ ReferenceAssetsWebPluginManifest,
898
+ ]);
899
+
900
+ const doc = TextDocument.create("file:///x.json", "json", 1, "{}");
901
+ await service.validateTextDocument(doc);
902
+
903
+ const json = JSON.parse(fs.readFileSync(outPath, "utf-8"));
904
+ expect(json.content["/x.json"].stats.metric).toBe(42);
905
+ // No root pulled from preexisting since it wasn't an object
906
+ fs.unlinkSync(outPath);
907
+ fs.rmdirSync(dir);
908
+ });
909
+
910
+ describe("Re-running validation", () => {
911
+ const MULTI_TEST_DIR = path.resolve("target_multi");
912
+ const MULTI_TEST_FILE = "multi_metrics.json";
913
+ const MULTI_TEST_PATH = path.join(MULTI_TEST_DIR, MULTI_TEST_FILE);
914
+
915
+ beforeEach(() => {
916
+ // Create test directory for multi-validation tests
917
+ if (!fs.existsSync(MULTI_TEST_DIR)) {
918
+ fs.mkdirSync(MULTI_TEST_DIR, { recursive: true });
919
+ }
920
+ });
921
+
922
+ afterEach(() => {
923
+ // Clean up test files
924
+ try {
925
+ if (fs.existsSync(MULTI_TEST_PATH)) {
926
+ fs.unlinkSync(MULTI_TEST_PATH);
927
+ }
928
+ if (fs.existsSync(MULTI_TEST_DIR)) {
929
+ fs.rmdirSync(MULTI_TEST_DIR);
930
+ }
931
+ } catch (e) {
932
+ console.debug("Test cleanup failed, but tests may still be valid:", e);
933
+ }
934
+ });
935
+
936
+ test("Auto-append: creates new file if none exists", async () => {
937
+ // Ensure no existing file
938
+ if (fs.existsSync(MULTI_TEST_PATH)) {
939
+ fs.unlinkSync(MULTI_TEST_PATH);
940
+ }
941
+
942
+ // Create service
943
+ const service = new PlayerLanguageService();
944
+ service.addLSPPlugin(
945
+ new MetricsOutput({
946
+ outputDir: MULTI_TEST_DIR,
947
+ fileName: MULTI_TEST_FILE.replace(".json", ""),
948
+ stats: {
949
+ complexity: () => 20,
950
+ },
951
+ }),
952
+ );
953
+
954
+ await service.setAssetTypesFromModule([
955
+ Types,
956
+ ReferenceAssetsWebPluginManifest,
957
+ ]);
958
+
959
+ // Validate a document
960
+ const doc = TextDocument.create("file:///new.json", "json", 1, "{}");
961
+ await service.validateTextDocument(doc);
962
+
963
+ // Check that new file was created
964
+ const result = JSON.parse(fs.readFileSync(MULTI_TEST_PATH, "utf-8"));
965
+ expect(result.content).toHaveProperty("/new.json");
966
+ expect(result.content["/new.json"].stats.complexity).toBe(20);
967
+ });
968
+
969
+ test("Auto-append: if metrics.json exists, new file entries are appended and existing ones are preserved", async () => {
970
+ // Seed an existing metrics file (what already exists on disk)
971
+ const existingMetrics = {
972
+ content: {
973
+ "/stage1.json": {
974
+ stats: { complexity: 10 },
975
+ features: { stable: true },
976
+ },
977
+ },
978
+ timestamp: "stage1-timestamp",
979
+ };
980
+ fs.writeFileSync(MULTI_TEST_PATH, JSON.stringify(existingMetrics));
981
+
982
+ // Create service that will add a new file entry
983
+ const service = new PlayerLanguageService();
984
+ service.addLSPPlugin(
985
+ new MetricsOutput({
986
+ outputDir: MULTI_TEST_DIR,
987
+ fileName: MULTI_TEST_FILE.replace(".json", ""),
988
+ stats: {
989
+ complexity: () => 20,
990
+ },
991
+ features: {
992
+ stable: () => false,
993
+ },
994
+ }),
995
+ );
996
+
997
+ await service.setAssetTypesFromModule([
998
+ Types,
999
+ ReferenceAssetsWebPluginManifest,
1000
+ ]);
1001
+
1002
+ // Validate a different document to trigger append
1003
+ const doc = TextDocument.create("file:///stage2.json", "json", 1, "{}");
1004
+ await service.validateTextDocument(doc);
1005
+
1006
+ // The resulting content should have the union of file entries with their respective data
1007
+ const result = JSON.parse(fs.readFileSync(MULTI_TEST_PATH, "utf-8"));
1008
+ expect(result.content).toEqual({
1009
+ "/stage1.json": {
1010
+ stats: { complexity: 10 },
1011
+ features: { stable: true },
1012
+ },
1013
+ "/stage2.json": {
1014
+ stats: { complexity: 20 },
1015
+ features: { stable: false },
1016
+ },
1017
+ });
1018
+ });
1019
+
1020
+ test("Auto-append: root properties are preserved and new ones appended when file exists", async () => {
1021
+ // Existing file with root metadata
1022
+ const existingMetrics = {
1023
+ content: {
1024
+ "/stage1.json": { stats: { metric: 1 } },
1025
+ },
1026
+ root: {
1027
+ metadata: {
1028
+ project: { name: "metrics-service" },
1029
+ },
1030
+ },
1031
+ };
1032
+ fs.writeFileSync(MULTI_TEST_PATH, JSON.stringify(existingMetrics));
1033
+
1034
+ const service = new PlayerLanguageService();
1035
+ service.addLSPPlugin(
1036
+ new MetricsOutput({
1037
+ outputDir: MULTI_TEST_DIR,
1038
+ fileName: MULTI_TEST_FILE.replace(".json", ""),
1039
+ rootProperties: {
1040
+ root: {
1041
+ metadata: { project: { version: "1.0.0" } },
1042
+ build: { ci: true },
1043
+ },
1044
+ },
1045
+ stats: { metric: () => 42 },
1046
+ }),
1047
+ );
1048
+
1049
+ await service.setAssetTypesFromModule([
1050
+ Types,
1051
+ ReferenceAssetsWebPluginManifest,
1052
+ ]);
1053
+
1054
+ // Validate a document to trigger write
1055
+ const doc = TextDocument.create("file:///stage2.json", "json", 1, "{}");
1056
+ await service.validateTextDocument(doc);
1057
+
1058
+ const result = JSON.parse(fs.readFileSync(MULTI_TEST_PATH, "utf-8"));
1059
+
1060
+ // Root properties merge (preserve existing, append new)
1061
+ expect(result.root.metadata.project).toEqual(
1062
+ expect.objectContaining({ name: "metrics-service", version: "1.0.0" }),
1063
+ );
1064
+ expect(result.root.build).toEqual(expect.objectContaining({ ci: true }));
1065
+
1066
+ // Content includes both files with their own stats
1067
+ expect(result.content).toHaveProperty("/stage1.json");
1068
+ expect(result.content["/stage1.json"].stats.metric).toBe(1);
1069
+ expect(result.content).toHaveProperty("/stage2.json");
1070
+ expect(result.content["/stage2.json"].stats.metric).toBe(42);
1071
+ });
1072
+
1073
+ test("Auto-append:loads existing metrics and merges", async () => {
1074
+ const service = new PlayerLanguageService();
1075
+ const dir = path.resolve("target_existing_ok");
1076
+ const name = "preexisting_ok";
1077
+ const outPath = path.join(dir, `${name}.json`);
1078
+
1079
+ fs.mkdirSync(dir, { recursive: true });
1080
+ // Seed a valid JSON object so parsed && typeof parsed === "object" is true
1081
+ fs.writeFileSync(
1082
+ outPath,
1083
+ JSON.stringify(
1084
+ {
1085
+ root: true,
1086
+ content: {
1087
+ "/seed.json": { stats: { seeded: 1 } },
1088
+ },
1089
+ },
1090
+ null,
1091
+ 2,
1092
+ ),
1093
+ "utf-8",
1094
+ );
1095
+
1096
+ service.addLSPPlugin(
1097
+ new MetricsOutput({
1098
+ outputDir: dir,
1099
+ fileName: name, // matches the seeded file
1100
+ stats: { added: 2 },
1101
+ }),
1102
+ );
1103
+
1104
+ await service.setAssetTypesFromModule([
1105
+ Types,
1106
+ ReferenceAssetsWebPluginManifest,
1107
+ ]);
1108
+
1109
+ const doc = TextDocument.create("file:///seed.json", "json", 1, "{}");
1110
+ await service.validateTextDocument(doc);
1111
+
1112
+ const json = JSON.parse(fs.readFileSync(outPath, "utf-8"));
1113
+ expect(json.root).toBe(true); // came from existing file
1114
+ expect(json.content["/seed.json"].stats).toMatchObject({
1115
+ seeded: 1,
1116
+ added: 2,
1117
+ });
1118
+
1119
+ fs.unlinkSync(outPath);
1120
+ fs.rmdirSync(dir);
1121
+ });
1122
+
1123
+ test("Deep merge: nested objects are merged (preserve existing, extend new) across runs", async () => {
1124
+ // Seed an existing metrics file with nested content and realistic root metadata
1125
+ const existingMetrics = {
1126
+ content: {
1127
+ "/nested.json": {
1128
+ stats: {
1129
+ metrics: { warnings: 1, byType: { deprecations: 1 } },
1130
+ nested: { x: 10 },
1131
+ },
1132
+ },
1133
+ },
1134
+ root: {
1135
+ metadata: {
1136
+ project: {
1137
+ name: "metrics-service",
1138
+ info: { repo: "tools" },
1139
+ },
1140
+ },
1141
+ },
1142
+ };
1143
+ fs.writeFileSync(MULTI_TEST_PATH, JSON.stringify(existingMetrics));
1144
+
1145
+ // Create service which writes overlapping nested values
1146
+ const service = new PlayerLanguageService();
1147
+ service.addLSPPlugin(
1148
+ new MetricsOutput({
1149
+ outputDir: MULTI_TEST_DIR,
1150
+ fileName: MULTI_TEST_FILE.replace(".json", ""),
1151
+ rootProperties: {
1152
+ root: {
1153
+ metadata: {
1154
+ project: { info: { branch: "main" } },
1155
+ build: { ci: true },
1156
+ },
1157
+ },
1158
+ },
1159
+ stats: {
1160
+ metrics: () => ({ warnings: 2, byType: { errors: 1 } }), // overwrite primitive and extend nested
1161
+ nested: () => ({ y: 20 }), // extend nested
1162
+ },
1163
+ }),
1164
+ );
1165
+
1166
+ await service.setAssetTypesFromModule([
1167
+ Types,
1168
+ ReferenceAssetsWebPluginManifest,
1169
+ ]);
1170
+
1171
+ // Validate a document to trigger write for the same file path
1172
+ const doc = TextDocument.create("file:///nested.json", "json", 1, "{}");
1173
+ await service.validateTextDocument(doc);
1174
+
1175
+ // Verify deep merge result
1176
+ const result = JSON.parse(fs.readFileSync(MULTI_TEST_PATH, "utf-8"));
1177
+ expect(result.content["/nested.json"].stats).toEqual(
1178
+ expect.objectContaining({
1179
+ metrics: expect.objectContaining({
1180
+ warnings: 2,
1181
+ byType: expect.objectContaining({ deprecations: 1, errors: 1 }),
1182
+ }),
1183
+ nested: expect.objectContaining({ x: 10, y: 20 }),
1184
+ }),
1185
+ );
1186
+ expect(result.root.metadata.project.name).toBe("metrics-service");
1187
+ expect(result.root.metadata.project.info).toEqual(
1188
+ expect.objectContaining({ repo: "tools", branch: "main" }),
1189
+ );
1190
+ expect(result.root.metadata.build).toEqual(
1191
+ expect.objectContaining({ ci: true }),
1192
+ );
1193
+ });
1194
+
1195
+ test("New root property on second run keeps content as last key", async () => {
1196
+ // First run writes initial root and first file entry
1197
+ const service1 = new PlayerLanguageService();
1198
+ service1.addLSPPlugin(
1199
+ new MetricsOutput({
1200
+ outputDir: MULTI_TEST_DIR,
1201
+ fileName: MULTI_TEST_FILE.replace(".json", ""),
1202
+ rootProperties: { initialRoot: true },
1203
+ stats: { metric: () => 1 },
1204
+ }),
1205
+ );
1206
+
1207
+ await service1.setAssetTypesFromModule([
1208
+ Types,
1209
+ ReferenceAssetsWebPluginManifest,
1210
+ ]);
1211
+
1212
+ const doc1 = TextDocument.create("file:///stage1.json", "json", 1, "{}");
1213
+ await service1.validateTextDocument(doc1);
1214
+
1215
+ // Second run adds a new root property and a different file entry
1216
+ const service2 = new PlayerLanguageService();
1217
+ service2.addLSPPlugin(
1218
+ new MetricsOutput({
1219
+ outputDir: MULTI_TEST_DIR,
1220
+ fileName: MULTI_TEST_FILE.replace(".json", ""),
1221
+ rootProperties: { newRoot: true },
1222
+ stats: { metric: () => 2 },
1223
+ }),
1224
+ );
1225
+
1226
+ await service2.setAssetTypesFromModule([
1227
+ Types,
1228
+ ReferenceAssetsWebPluginManifest,
1229
+ ]);
1230
+
1231
+ const doc2 = TextDocument.create("file:///stage2.json", "json", 1, "{}");
1232
+ await service2.validateTextDocument(doc2);
1233
+
1234
+ // Verify only that the last top-level key is "content"
1235
+ const parsed = JSON.parse(fs.readFileSync(MULTI_TEST_PATH, "utf-8"));
1236
+ const keys = Object.keys(parsed);
1237
+ expect(keys[keys.length - 1]).toBe("content");
1238
+ });
1239
+
1240
+ test("merges with existing metrics file (root and content)", async () => {
1241
+ const service = new PlayerLanguageService();
1242
+
1243
+ const fileName = "preexisting_merge";
1244
+ const outputPath = path.join(TEST_DIR, `${fileName}.json`);
1245
+
1246
+ // Seed an existing metrics file with root and nested content
1247
+ fs.writeFileSync(
1248
+ outputPath,
1249
+ JSON.stringify(
1250
+ {
1251
+ rootKey: "keep-me",
1252
+ content: {
1253
+ "existing.json": {
1254
+ stats: { existingStat: 1 },
1255
+ },
1256
+ },
1257
+ },
1258
+ null,
1259
+ 2,
1260
+ ),
1261
+ "utf-8",
1262
+ );
1263
+
1264
+ service.addLSPPlugin(
1265
+ new MetricsOutput({
1266
+ outputDir: TEST_DIR,
1267
+ fileName,
1268
+ rootProperties: { anotherRoot: true },
1269
+ stats: { newStat: 2 },
1270
+ }),
1271
+ );
1272
+
1273
+ await service.setAssetTypesFromModule([
1274
+ Types,
1275
+ ReferenceAssetsWebPluginManifest,
1276
+ ]);
1277
+
1278
+ const doc = TextDocument.create(
1279
+ "existing.json",
1280
+ "json",
1281
+ 1,
1282
+ JSON.stringify({ id: "ok" }),
1283
+ );
1284
+ await service.validateTextDocument(doc);
1285
+
1286
+ expect(fs.existsSync(outputPath)).toBe(true);
1287
+ const parsed = JSON.parse(fs.readFileSync(outputPath, "utf-8"));
1288
+
1289
+ // Root deep-merged
1290
+ expect(parsed.rootKey).toBe("keep-me");
1291
+ expect(parsed.anotherRoot).toBe(true);
1292
+
1293
+ // Content deep-merged for same file
1294
+ expect(parsed.content["existing.json"].stats).toMatchObject({
1295
+ existingStat: 1,
1296
+ newStat: 2,
1297
+ });
1298
+
1299
+ fs.unlinkSync(outputPath);
1300
+ });
1301
+
1302
+ test("Gracefully handles malformed existing metrics file and logs warning", async () => {
1303
+ // Seed an invalid JSON metrics file to trigger the parse error path
1304
+ fs.writeFileSync(MULTI_TEST_PATH, "{ invalid-json ");
1305
+
1306
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1307
+
1308
+ const service = new PlayerLanguageService();
1309
+ service.addLSPPlugin(
1310
+ new MetricsOutput({
1311
+ outputDir: MULTI_TEST_DIR,
1312
+ fileName: MULTI_TEST_FILE.replace(".json", ""),
1313
+ stats: { metric: () => 99 },
1314
+ }),
1315
+ );
1316
+
1317
+ await service.setAssetTypesFromModule([
1318
+ Types,
1319
+ ReferenceAssetsWebPluginManifest,
1320
+ ]);
1321
+
1322
+ const doc = TextDocument.create(
1323
+ "file:///malformed.json",
1324
+ "json",
1325
+ 1,
1326
+ "{}",
1327
+ );
1328
+ await service.validateTextDocument(doc);
1329
+
1330
+ // Should log a parse warning and still produce a valid metrics file
1331
+ expect(warnSpy).toHaveBeenCalled();
1332
+ const args = warnSpy.mock.calls[0][0] as string;
1333
+ expect(args).toContain("Could not parse existing metrics file");
1334
+
1335
+ const parsed = JSON.parse(fs.readFileSync(MULTI_TEST_PATH, "utf-8"));
1336
+ // normalizePath removes file:// and backslashes; our test doc uses file:///malformed.json
1337
+ expect(parsed.content).toHaveProperty("/malformed.json");
1338
+ expect(parsed.content["/malformed.json"].stats.metric).toBe(99);
1339
+
1340
+ warnSpy.mockRestore();
1341
+ });
1342
+
1343
+ test("loads and merges an existing metrics file", async () => {
1344
+ const service = new PlayerLanguageService();
1345
+ const fileName = "pre_merge";
1346
+ const outPath = path.join(TEST_DIR, `${fileName}.json`);
1347
+
1348
+ // Seed a valid existing metrics file
1349
+ fs.writeFileSync(
1350
+ outPath,
1351
+ JSON.stringify(
1352
+ {
1353
+ rootKey: "keep",
1354
+ content: {
1355
+ "/pre.json": { stats: { preStat: 1 } },
1356
+ },
1357
+ },
1358
+ null,
1359
+ 2,
1360
+ ),
1361
+ "utf-8",
1362
+ );
1363
+
1364
+ service.addLSPPlugin(
1365
+ new MetricsOutput({
1366
+ outputDir: TEST_DIR,
1367
+ fileName,
1368
+ stats: { newStat: 2 },
1369
+ }),
1370
+ );
1371
+
1372
+ await service.setAssetTypesFromModule([
1373
+ Types,
1374
+ ReferenceAssetsWebPluginManifest,
1375
+ ]);
1376
+ const doc = TextDocument.create("file:///pre.json", "json", 1, "{}");
1377
+ await service.validateTextDocument(doc);
1378
+
1379
+ const json = JSON.parse(fs.readFileSync(outPath, "utf-8"));
1380
+ expect(json.rootKey).toBe("keep"); // came from existing file
1381
+ expect(json.content["/pre.json"].stats).toMatchObject({
1382
+ preStat: 1,
1383
+ newStat: 2,
1384
+ });
1385
+
1386
+ fs.unlinkSync(outPath);
1387
+ });
1388
+ });
781
1389
  });