@playcademy/sdk 0.3.7-beta.1 → 0.3.7-beta.2
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/dist/index.js +12 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +14 -1
- package/dist/server/edge.d.ts +701 -0
- package/dist/server/edge.js +323 -0
- package/dist/server.d.ts +61 -46
- package/dist/server.js +222 -189
- package/package.json +6 -1
package/dist/server.js
CHANGED
|
@@ -1,3 +1,204 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, {
|
|
5
|
+
get: all[name],
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
set: (newValue) => all[name] = () => newValue
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
12
|
+
// ../utils/src/file-loader.ts
|
|
13
|
+
import { existsSync, readdirSync, statSync } from "fs";
|
|
14
|
+
import { readFile } from "fs/promises";
|
|
15
|
+
import { dirname, parse, resolve } from "path";
|
|
16
|
+
function findFilePath(filename, startDir, maxLevels = 3) {
|
|
17
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
18
|
+
let currentDir = resolve(startDir);
|
|
19
|
+
let levelsSearched = 0;
|
|
20
|
+
while (levelsSearched <= maxLevels) {
|
|
21
|
+
for (const fname of filenames) {
|
|
22
|
+
const filePath = resolve(currentDir, fname);
|
|
23
|
+
if (existsSync(filePath)) {
|
|
24
|
+
return {
|
|
25
|
+
path: filePath,
|
|
26
|
+
dir: currentDir,
|
|
27
|
+
filename: fname
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (levelsSearched >= maxLevels) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
const parentDir = dirname(currentDir);
|
|
35
|
+
if (parentDir === currentDir) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
const parsed = parse(currentDir);
|
|
39
|
+
if (parsed.root === currentDir) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
currentDir = parentDir;
|
|
43
|
+
levelsSearched++;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
async function loadFile(filename, options = {}) {
|
|
48
|
+
const {
|
|
49
|
+
cwd = process.cwd(),
|
|
50
|
+
required = false,
|
|
51
|
+
searchUp = false,
|
|
52
|
+
maxLevels = 3,
|
|
53
|
+
parseJson = false,
|
|
54
|
+
stripComments = false
|
|
55
|
+
} = options;
|
|
56
|
+
let fileResult;
|
|
57
|
+
if (searchUp) {
|
|
58
|
+
fileResult = findFilePath(filename, cwd, maxLevels);
|
|
59
|
+
} else {
|
|
60
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
61
|
+
fileResult = null;
|
|
62
|
+
for (const fname of filenames) {
|
|
63
|
+
const filePath = resolve(cwd, fname);
|
|
64
|
+
if (existsSync(filePath)) {
|
|
65
|
+
fileResult = {
|
|
66
|
+
path: filePath,
|
|
67
|
+
dir: cwd,
|
|
68
|
+
filename: fname
|
|
69
|
+
};
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!fileResult) {
|
|
75
|
+
if (required) {
|
|
76
|
+
const fileList = Array.isArray(filename) ? filename.join(" or ") : filename;
|
|
77
|
+
const message = searchUp ? `${fileList} not found in ${cwd} or up to ${maxLevels} parent directories` : `${fileList} not found at ${cwd}`;
|
|
78
|
+
throw new Error(message);
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
let content = await readFile(fileResult.path, "utf8");
|
|
84
|
+
if (parseJson) {
|
|
85
|
+
if (stripComments) {
|
|
86
|
+
content = stripJsonComments(content);
|
|
87
|
+
}
|
|
88
|
+
return JSON.parse(content);
|
|
89
|
+
}
|
|
90
|
+
return content;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new Error(`Failed to load ${fileResult.filename} from ${fileResult.path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function findFile(filename, options = {}) {
|
|
96
|
+
const { cwd = process.cwd(), searchUp = false, maxLevels = 3 } = options;
|
|
97
|
+
if (searchUp) {
|
|
98
|
+
return findFilePath(filename, cwd, maxLevels);
|
|
99
|
+
}
|
|
100
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
101
|
+
for (const fname of filenames) {
|
|
102
|
+
const filePath = resolve(cwd, fname);
|
|
103
|
+
if (existsSync(filePath)) {
|
|
104
|
+
return {
|
|
105
|
+
path: filePath,
|
|
106
|
+
dir: cwd,
|
|
107
|
+
filename: fname
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function stripJsonComments(jsonc) {
|
|
114
|
+
let result = jsonc.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
115
|
+
result = result.replace(/\/\/.*/g, "");
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
var init_file_loader = () => {};
|
|
119
|
+
|
|
120
|
+
// src/server/utils/config-loader.ts
|
|
121
|
+
var exports_config_loader = {};
|
|
122
|
+
__export(exports_config_loader, {
|
|
123
|
+
loadConfig: () => loadConfig,
|
|
124
|
+
findConfigPath: () => findConfigPath
|
|
125
|
+
});
|
|
126
|
+
import { resolve as resolve2 } from "path";
|
|
127
|
+
async function findConfigPath(configPath) {
|
|
128
|
+
if (configPath) {
|
|
129
|
+
return resolve2(configPath);
|
|
130
|
+
}
|
|
131
|
+
const result = await findFile(["playcademy.config.js", "playcademy.config.mjs", "playcademy.config.json"], {
|
|
132
|
+
searchUp: true,
|
|
133
|
+
maxLevels: 3
|
|
134
|
+
});
|
|
135
|
+
if (!result) {
|
|
136
|
+
throw new Error("playcademy.config.js not found. Please create a playcademy.config.js file or specify the config path.");
|
|
137
|
+
}
|
|
138
|
+
return result.path;
|
|
139
|
+
}
|
|
140
|
+
async function loadConfig(configPath) {
|
|
141
|
+
try {
|
|
142
|
+
let config;
|
|
143
|
+
let actualPath;
|
|
144
|
+
if (configPath) {
|
|
145
|
+
actualPath = resolve2(configPath);
|
|
146
|
+
if (actualPath.endsWith(".json")) {
|
|
147
|
+
config = await loadFile(actualPath, { required: true, parseJson: true });
|
|
148
|
+
} else {
|
|
149
|
+
const module = await import(actualPath);
|
|
150
|
+
config = module.default || module;
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
actualPath = await findConfigPath();
|
|
154
|
+
if (actualPath.endsWith(".json")) {
|
|
155
|
+
config = await loadFile(actualPath, { required: true, parseJson: true });
|
|
156
|
+
} else {
|
|
157
|
+
const module = await import(actualPath);
|
|
158
|
+
config = module.default || module;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!config || typeof config !== "object") {
|
|
162
|
+
throw new Error("Config file must export/contain an object");
|
|
163
|
+
}
|
|
164
|
+
validateConfig(config);
|
|
165
|
+
return config;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw new Error(`Failed to load config file: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function validateConfig(config) {
|
|
171
|
+
if (!config || typeof config !== "object") {
|
|
172
|
+
throw new Error("Configuration must be an object");
|
|
173
|
+
}
|
|
174
|
+
const cfg = config;
|
|
175
|
+
if (!cfg.name || typeof cfg.name !== "string") {
|
|
176
|
+
throw new Error("Missing required field: name");
|
|
177
|
+
}
|
|
178
|
+
if (cfg.timeback) {
|
|
179
|
+
if (typeof cfg.timeback !== "object") {
|
|
180
|
+
throw new Error("timeback must be an object if provided");
|
|
181
|
+
}
|
|
182
|
+
const tb = cfg.timeback;
|
|
183
|
+
if (!tb.course) {
|
|
184
|
+
throw new Error("timeback.course is required for TimeBack integration");
|
|
185
|
+
}
|
|
186
|
+
if (typeof tb.course !== "object") {
|
|
187
|
+
throw new Error("timeback.course must be an object");
|
|
188
|
+
}
|
|
189
|
+
const course = tb.course;
|
|
190
|
+
if (!course.subjects || !Array.isArray(course.subjects) || course.subjects.length === 0) {
|
|
191
|
+
throw new Error("timeback.course.subjects is required (array of subjects)");
|
|
192
|
+
}
|
|
193
|
+
if (!course.grades || !Array.isArray(course.grades) || course.grades.length === 0) {
|
|
194
|
+
throw new Error("timeback.course.grades is required (array of grade levels)");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
var init_config_loader = __esm(() => {
|
|
199
|
+
init_file_loader();
|
|
200
|
+
});
|
|
201
|
+
|
|
1
202
|
// src/core/guards.ts
|
|
2
203
|
var VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
|
3
204
|
var VALID_SUBJECTS = [
|
|
@@ -211,207 +412,27 @@ async function makeApiRequest(baseUrl, apiToken, endpoint, method = "GET", body)
|
|
|
211
412
|
return await response.json();
|
|
212
413
|
}
|
|
213
414
|
|
|
214
|
-
// src/server/
|
|
215
|
-
import { resolve as resolve2 } from "path";
|
|
216
|
-
|
|
217
|
-
// ../utils/src/file-loader.ts
|
|
218
|
-
import { existsSync, readdirSync, statSync } from "fs";
|
|
219
|
-
import { readFile } from "fs/promises";
|
|
220
|
-
import { dirname, parse, resolve } from "path";
|
|
221
|
-
function findFilePath(filename, startDir, maxLevels = 3) {
|
|
222
|
-
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
223
|
-
let currentDir = resolve(startDir);
|
|
224
|
-
let levelsSearched = 0;
|
|
225
|
-
while (levelsSearched <= maxLevels) {
|
|
226
|
-
for (const fname of filenames) {
|
|
227
|
-
const filePath = resolve(currentDir, fname);
|
|
228
|
-
if (existsSync(filePath)) {
|
|
229
|
-
return {
|
|
230
|
-
path: filePath,
|
|
231
|
-
dir: currentDir,
|
|
232
|
-
filename: fname
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (levelsSearched >= maxLevels) {
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
|
-
const parentDir = dirname(currentDir);
|
|
240
|
-
if (parentDir === currentDir) {
|
|
241
|
-
break;
|
|
242
|
-
}
|
|
243
|
-
const parsed = parse(currentDir);
|
|
244
|
-
if (parsed.root === currentDir) {
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
currentDir = parentDir;
|
|
248
|
-
levelsSearched++;
|
|
249
|
-
}
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
async function loadFile(filename, options = {}) {
|
|
253
|
-
const {
|
|
254
|
-
cwd = process.cwd(),
|
|
255
|
-
required = false,
|
|
256
|
-
searchUp = false,
|
|
257
|
-
maxLevels = 3,
|
|
258
|
-
parseJson = false,
|
|
259
|
-
stripComments = false
|
|
260
|
-
} = options;
|
|
261
|
-
let fileResult;
|
|
262
|
-
if (searchUp) {
|
|
263
|
-
fileResult = findFilePath(filename, cwd, maxLevels);
|
|
264
|
-
} else {
|
|
265
|
-
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
266
|
-
fileResult = null;
|
|
267
|
-
for (const fname of filenames) {
|
|
268
|
-
const filePath = resolve(cwd, fname);
|
|
269
|
-
if (existsSync(filePath)) {
|
|
270
|
-
fileResult = {
|
|
271
|
-
path: filePath,
|
|
272
|
-
dir: cwd,
|
|
273
|
-
filename: fname
|
|
274
|
-
};
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (!fileResult) {
|
|
280
|
-
if (required) {
|
|
281
|
-
const fileList = Array.isArray(filename) ? filename.join(" or ") : filename;
|
|
282
|
-
const message = searchUp ? `${fileList} not found in ${cwd} or up to ${maxLevels} parent directories` : `${fileList} not found at ${cwd}`;
|
|
283
|
-
throw new Error(message);
|
|
284
|
-
}
|
|
285
|
-
return null;
|
|
286
|
-
}
|
|
287
|
-
try {
|
|
288
|
-
let content = await readFile(fileResult.path, "utf8");
|
|
289
|
-
if (parseJson) {
|
|
290
|
-
if (stripComments) {
|
|
291
|
-
content = stripJsonComments(content);
|
|
292
|
-
}
|
|
293
|
-
return JSON.parse(content);
|
|
294
|
-
}
|
|
295
|
-
return content;
|
|
296
|
-
} catch (error) {
|
|
297
|
-
throw new Error(`Failed to load ${fileResult.filename} from ${fileResult.path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
async function findFile(filename, options = {}) {
|
|
301
|
-
const { cwd = process.cwd(), searchUp = false, maxLevels = 3 } = options;
|
|
302
|
-
if (searchUp) {
|
|
303
|
-
return findFilePath(filename, cwd, maxLevels);
|
|
304
|
-
}
|
|
305
|
-
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
306
|
-
for (const fname of filenames) {
|
|
307
|
-
const filePath = resolve(cwd, fname);
|
|
308
|
-
if (existsSync(filePath)) {
|
|
309
|
-
return {
|
|
310
|
-
path: filePath,
|
|
311
|
-
dir: cwd,
|
|
312
|
-
filename: fname
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
function stripJsonComments(jsonc) {
|
|
319
|
-
let result = jsonc.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
320
|
-
result = result.replace(/\/\/.*/g, "");
|
|
321
|
-
return result;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// src/server/utils/config-loader.ts
|
|
325
|
-
async function findConfigPath(configPath) {
|
|
326
|
-
if (configPath) {
|
|
327
|
-
return resolve2(configPath);
|
|
328
|
-
}
|
|
329
|
-
const result = await findFile(["playcademy.config.js", "playcademy.config.mjs", "playcademy.config.json"], {
|
|
330
|
-
searchUp: true,
|
|
331
|
-
maxLevels: 3
|
|
332
|
-
});
|
|
333
|
-
if (!result) {
|
|
334
|
-
throw new Error("playcademy.config.js not found. Please create a playcademy.config.js file or specify the config path.");
|
|
335
|
-
}
|
|
336
|
-
return result.path;
|
|
337
|
-
}
|
|
338
|
-
async function loadConfig(configPath) {
|
|
339
|
-
try {
|
|
340
|
-
let config;
|
|
341
|
-
let actualPath;
|
|
342
|
-
if (configPath) {
|
|
343
|
-
actualPath = resolve2(configPath);
|
|
344
|
-
if (actualPath.endsWith(".json")) {
|
|
345
|
-
config = await loadFile(actualPath, { required: true, parseJson: true });
|
|
346
|
-
} else {
|
|
347
|
-
const module = await import(actualPath);
|
|
348
|
-
config = module.default || module;
|
|
349
|
-
}
|
|
350
|
-
} else {
|
|
351
|
-
actualPath = await findConfigPath();
|
|
352
|
-
if (actualPath.endsWith(".json")) {
|
|
353
|
-
config = await loadFile(actualPath, { required: true, parseJson: true });
|
|
354
|
-
} else {
|
|
355
|
-
const module = await import(actualPath);
|
|
356
|
-
config = module.default || module;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
if (!config || typeof config !== "object") {
|
|
360
|
-
throw new Error("Config file must export/contain an object");
|
|
361
|
-
}
|
|
362
|
-
validateConfig(config);
|
|
363
|
-
return config;
|
|
364
|
-
} catch (error) {
|
|
365
|
-
throw new Error(`Failed to load config file: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
function validateConfig(config) {
|
|
369
|
-
if (!config || typeof config !== "object") {
|
|
370
|
-
throw new Error("Configuration must be an object");
|
|
371
|
-
}
|
|
372
|
-
const cfg = config;
|
|
373
|
-
if (!cfg.name || typeof cfg.name !== "string") {
|
|
374
|
-
throw new Error("Missing required field: name");
|
|
375
|
-
}
|
|
376
|
-
if (cfg.timeback) {
|
|
377
|
-
if (typeof cfg.timeback !== "object") {
|
|
378
|
-
throw new Error("timeback must be an object if provided");
|
|
379
|
-
}
|
|
380
|
-
const tb = cfg.timeback;
|
|
381
|
-
if (!tb.course) {
|
|
382
|
-
throw new Error("timeback.course is required for TimeBack integration");
|
|
383
|
-
}
|
|
384
|
-
if (typeof tb.course !== "object") {
|
|
385
|
-
throw new Error("timeback.course must be an object");
|
|
386
|
-
}
|
|
387
|
-
const course = tb.course;
|
|
388
|
-
if (!course.subjects || !Array.isArray(course.subjects) || course.subjects.length === 0) {
|
|
389
|
-
throw new Error("timeback.course.subjects is required (array of subjects)");
|
|
390
|
-
}
|
|
391
|
-
if (!course.grades || !Array.isArray(course.grades) || course.grades.length === 0) {
|
|
392
|
-
throw new Error("timeback.course.grades is required (array of grade levels)");
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// src/server/client.ts
|
|
415
|
+
// src/server/client-base.ts
|
|
398
416
|
class PlaycademyClient {
|
|
399
417
|
state;
|
|
400
418
|
constructor(state) {
|
|
401
419
|
this.state = state;
|
|
402
420
|
}
|
|
403
421
|
static async init(config) {
|
|
404
|
-
const { apiKey,
|
|
422
|
+
const { apiKey, baseUrl, gameId } = config;
|
|
405
423
|
if (!apiKey || typeof apiKey !== "string") {
|
|
406
424
|
throw new Error("[Playcademy SDK] apiKey is required");
|
|
407
425
|
}
|
|
408
|
-
|
|
409
|
-
|
|
426
|
+
if (!config.config) {
|
|
427
|
+
throw new Error("[Playcademy SDK] config is required in edge environments. " + "Pass config directly, or use PlaycademyClient from @playcademy/sdk/server " + "for filesystem-based config loading.");
|
|
428
|
+
}
|
|
429
|
+
const envBaseUrl = typeof process !== "undefined" ? process.env?.PLAYCADEMY_BASE_URL : undefined;
|
|
430
|
+
const finalBaseUrl = baseUrl || envBaseUrl || "https://hub.playcademy.net";
|
|
410
431
|
const state = {
|
|
411
432
|
apiKey,
|
|
412
433
|
baseUrl: finalBaseUrl,
|
|
413
434
|
gameId: gameId || "",
|
|
414
|
-
config:
|
|
435
|
+
config: config.config
|
|
415
436
|
};
|
|
416
437
|
const client = new PlaycademyClient(state);
|
|
417
438
|
if (!gameId) {
|
|
@@ -433,6 +454,18 @@ class PlaycademyClient {
|
|
|
433
454
|
}
|
|
434
455
|
timeback = createTimebackNamespace(this);
|
|
435
456
|
}
|
|
457
|
+
|
|
458
|
+
// src/server/client.ts
|
|
459
|
+
class PlaycademyClient2 extends PlaycademyClient {
|
|
460
|
+
static async init(config) {
|
|
461
|
+
if (config.config) {
|
|
462
|
+
return super.init(config);
|
|
463
|
+
}
|
|
464
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config_loader(), exports_config_loader));
|
|
465
|
+
const loadedConfig = await loadConfig2(config.configPath);
|
|
466
|
+
return super.init({ ...config, config: loadedConfig });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
436
469
|
// src/server/utils/verify-game-token.ts
|
|
437
470
|
async function verifyGameToken(gameToken, options) {
|
|
438
471
|
if (!gameToken || typeof gameToken !== "string") {
|
|
@@ -487,5 +520,5 @@ Please set the PLAYCADEMY_BASE_URL environment variable`);
|
|
|
487
520
|
}
|
|
488
521
|
export {
|
|
489
522
|
verifyGameToken,
|
|
490
|
-
PlaycademyClient
|
|
523
|
+
PlaycademyClient2 as PlaycademyClient
|
|
491
524
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcademy/sdk",
|
|
3
|
-
"version": "0.3.7-beta.
|
|
3
|
+
"version": "0.3.7-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
"import": "./dist/internal.js",
|
|
24
24
|
"require": "./dist/internal.js"
|
|
25
25
|
},
|
|
26
|
+
"./server/edge": {
|
|
27
|
+
"types": "./dist/server/edge.d.ts",
|
|
28
|
+
"import": "./dist/server/edge.js",
|
|
29
|
+
"require": "./dist/server/edge.js"
|
|
30
|
+
},
|
|
26
31
|
"./test": {
|
|
27
32
|
"import": "./src/test.ts",
|
|
28
33
|
"types": "./src/test.ts"
|