datagrok-tools 6.1.8 → 6.1.10

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.
@@ -0,0 +1,459 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NodeUsersDataSource = exports.NodeSharesDataSource = exports.NodeHttpDataSource = exports.NodeGroupsDataSource = exports.NodeFuncsDataSource = exports.NodeFilesDataSource = exports.NodeDapi = exports.NodeConnectionsDataSource = exports.NodeApiClient = void 0;
7
+ exports.ensureBodyId = ensureBodyId;
8
+ var _crypto = require("crypto");
9
+ /// Docs: [Grok Dapi](/docs/plans/grok-dapi/)
10
+
11
+ function ensureBodyId(body) {
12
+ if (body && typeof body === 'object' && !body.id) body.id = (0, _crypto.randomUUID)();
13
+ return body;
14
+ }
15
+ class NodeApiClient {
16
+ constructor(baseUrl, token) {
17
+ this.baseUrl = baseUrl;
18
+ this.token = token;
19
+ }
20
+ static async login(baseUrl, devKey) {
21
+ const res = await fetch(`${baseUrl}/users/login/dev/${devKey}`, {
22
+ method: 'POST'
23
+ });
24
+ const json = await res.json();
25
+ if (!json.token) throw new Error('Login failed. Check your developer key.');
26
+ return new NodeApiClient(baseUrl, json.token);
27
+ }
28
+ async request(method, path, body, headers) {
29
+ const url = `${this.baseUrl}${path}`;
30
+ const opts = {
31
+ method,
32
+ headers: {
33
+ 'Authorization': this.token,
34
+ 'Content-Type': 'application/json',
35
+ ...headers
36
+ }
37
+ };
38
+ if (body !== undefined) opts.body = JSON.stringify(body);
39
+ const res = await fetch(url, opts);
40
+ if (!res.ok) {
41
+ // Read as text first to avoid "Body has already been read" when JSON.parse fails
42
+ const rawText = await res.text();
43
+ let errBody;
44
+ try {
45
+ errBody = JSON.parse(rawText);
46
+ } catch {
47
+ errBody = {
48
+ error: rawText || `HTTP ${res.status}`
49
+ };
50
+ }
51
+ const err = {
52
+ error: errBody?.message ?? errBody?.error ?? `HTTP ${res.status}`,
53
+ source: errBody?.source ?? 'Server',
54
+ errorCode: errBody?.errorCode ?? res.status,
55
+ stackTrace: errBody?.stackTrace
56
+ };
57
+ throw Object.assign(new Error(err.error), {
58
+ apiError: err
59
+ });
60
+ }
61
+ if (res.status === 204 || res.headers.get('content-length') === '0') return null;
62
+ const ct = res.headers.get('content-type') ?? '';
63
+ if (ct.includes('application/json')) return res.json();
64
+ return res.text();
65
+ }
66
+ get(path) {
67
+ return this.request('GET', path);
68
+ }
69
+ post(path, body) {
70
+ return this.request('POST', path, body);
71
+ }
72
+ del(path) {
73
+ return this.request('DELETE', path);
74
+ }
75
+ }
76
+ exports.NodeApiClient = NodeApiClient;
77
+ function buildQuery(params) {
78
+ const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== '');
79
+ if (!entries.length) return '';
80
+ return '?' + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&');
81
+ }
82
+ class NodeHttpDataSource {
83
+ _filter = '';
84
+ _limit = 50;
85
+ _page = 0;
86
+ _order = '';
87
+ constructor(client, path) {
88
+ this.client = client;
89
+ this.path = path;
90
+ }
91
+ filter(w) {
92
+ this._filter = w;
93
+ return this;
94
+ }
95
+ by(n) {
96
+ this._limit = n;
97
+ return this;
98
+ }
99
+ page(n) {
100
+ this._page = n;
101
+ return this;
102
+ }
103
+ order(field, desc = false) {
104
+ this._order = desc ? `-${field}` : field;
105
+ return this;
106
+ }
107
+ async list() {
108
+ const q = buildQuery({
109
+ text: this._filter || undefined,
110
+ limit: this._limit,
111
+ page: this._page || undefined,
112
+ order: this._order || undefined
113
+ });
114
+ return this.client.get(`/public/v1/${this.path}${q}`);
115
+ }
116
+ async find(id) {
117
+ return this.client.get(`/public/v1/${this.path}/${encodeURIComponent(id.replace(':', '.'))}`);
118
+ }
119
+ async count() {
120
+ const q = buildQuery({
121
+ text: this._filter || undefined
122
+ });
123
+ const res = await this.client.get(`/public/v1/${this.path}/count${q}`);
124
+ return typeof res === 'number' ? res : res?.count ?? 0;
125
+ }
126
+ async delete(idOrEntity) {
127
+ const id = typeof idOrEntity === 'string' ? idOrEntity : idOrEntity?.id ?? '';
128
+ await this.client.del(`/public/v1/${this.path}/${encodeURIComponent(id)}`);
129
+ }
130
+ }
131
+ exports.NodeHttpDataSource = NodeHttpDataSource;
132
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
133
+ class NodeGroupsDataSource extends NodeHttpDataSource {
134
+ constructor(client) {
135
+ super(client, 'groups');
136
+ }
137
+ async save(group, saveRelations = false) {
138
+ const q = buildQuery({
139
+ saveRelations: saveRelations ? 'true' : undefined
140
+ });
141
+ return this.client.post(`/public/v1/groups${q}`, ensureBodyId(group));
142
+ }
143
+ async lookup(name) {
144
+ const q = buildQuery({
145
+ query: name
146
+ });
147
+ return this.client.get(`/public/v1/groups/lookup${q}`);
148
+ }
149
+ async resolve(idOrName, opts = {}) {
150
+ if (UUID_RE.test(idOrName)) return this.find(idOrName);
151
+ const matches = await this.lookup(idOrName);
152
+ let candidates = matches;
153
+ if (opts.personalOnly) candidates = matches.filter(g => g?.personal === true);
154
+ if (!candidates.length) {
155
+ const suffix = opts.personalOnly ? ' (personal)' : '';
156
+ throw new Error(`No group matching '${idOrName}'${suffix}`);
157
+ }
158
+ if (candidates.length > 1) {
159
+ const list = candidates.map(g => ` ${g.id} ${g.friendlyName ?? g.name ?? ''}`).join('\n');
160
+ throw new Error(`Multiple groups match '${idOrName}':\n${list}\nUse the ID to disambiguate.`);
161
+ }
162
+ return candidates[0];
163
+ }
164
+ async addMembers(group, members, isAdmin = false, personalOnly = false) {
165
+ const parent = await this.resolve(group);
166
+ const children = Array.isArray(parent.children) ? parent.children : [];
167
+ const results = [];
168
+ let mutated = false;
169
+ for (const m of members) {
170
+ let child;
171
+ try {
172
+ child = await this.resolve(m, {
173
+ personalOnly
174
+ });
175
+ } catch (err) {
176
+ results.push({
177
+ member: m,
178
+ status: 'error',
179
+ error: err?.message ?? String(err)
180
+ });
181
+ continue;
182
+ }
183
+ const existing = children.find(r => r?.child?.id === child.id);
184
+ if (existing) {
185
+ if (existing.isAdmin === isAdmin) {
186
+ results.push({
187
+ member: m,
188
+ status: 'noop'
189
+ });
190
+ } else {
191
+ existing.isAdmin = isAdmin;
192
+ mutated = true;
193
+ results.push({
194
+ member: m,
195
+ status: 'updated'
196
+ });
197
+ }
198
+ } else {
199
+ children.push({
200
+ parent: {
201
+ id: parent.id
202
+ },
203
+ child: {
204
+ id: child.id
205
+ },
206
+ isAdmin
207
+ });
208
+ mutated = true;
209
+ results.push({
210
+ member: m,
211
+ status: 'added'
212
+ });
213
+ }
214
+ }
215
+ if (mutated) {
216
+ parent.children = children;
217
+ await this.save(parent, true);
218
+ }
219
+ return results;
220
+ }
221
+ async removeMembers(group, members, personalOnly = false) {
222
+ const parent = await this.resolve(group);
223
+ const results = [];
224
+ const children = Array.isArray(parent.children) ? parent.children : [];
225
+ let mutated = false;
226
+ for (const m of members) {
227
+ let child;
228
+ try {
229
+ child = await this.resolve(m, {
230
+ personalOnly
231
+ });
232
+ } catch (err) {
233
+ results.push({
234
+ member: m,
235
+ status: 'error',
236
+ error: err?.message ?? String(err)
237
+ });
238
+ continue;
239
+ }
240
+ const idx = children.findIndex(r => r?.child?.id === child.id);
241
+ if (idx === -1) {
242
+ results.push({
243
+ member: m,
244
+ status: 'not-member'
245
+ });
246
+ } else {
247
+ children.splice(idx, 1);
248
+ mutated = true;
249
+ results.push({
250
+ member: m,
251
+ status: 'removed'
252
+ });
253
+ }
254
+ }
255
+ if (mutated) {
256
+ parent.children = children;
257
+ await this.save(parent, true);
258
+ }
259
+ return results;
260
+ }
261
+ async getMembers(group, admin) {
262
+ const parent = await this.resolve(group);
263
+ const q = buildQuery({
264
+ admin: admin === undefined ? undefined : String(admin)
265
+ });
266
+ return this.client.get(`/public/v1/groups/${encodeURIComponent(parent.id)}/members${q}`);
267
+ }
268
+ async getMemberships(group, admin) {
269
+ const parent = await this.resolve(group);
270
+ const q = buildQuery({
271
+ admin: admin === undefined ? undefined : String(admin)
272
+ });
273
+ return this.client.get(`/public/v1/groups/${encodeURIComponent(parent.id)}/memberships${q}`);
274
+ }
275
+ }
276
+ exports.NodeGroupsDataSource = NodeGroupsDataSource;
277
+ class NodeSharesDataSource {
278
+ constructor(client) {
279
+ this.client = client;
280
+ }
281
+ async share(entity, groups, access = 'View') {
282
+ const name = encodeURIComponent(entity.replace(':', '.'));
283
+ const q = buildQuery({
284
+ groups,
285
+ access
286
+ });
287
+ return this.client.post(`/public/v1/entities/${name}/shares${q}`);
288
+ }
289
+ async list(entityId) {
290
+ const q = buildQuery({
291
+ entityId
292
+ });
293
+ return this.client.get(`/privileges/permissions${q}`);
294
+ }
295
+ }
296
+ exports.NodeSharesDataSource = NodeSharesDataSource;
297
+ class NodeUsersDataSource extends NodeHttpDataSource {
298
+ constructor(client) {
299
+ super(client, 'users');
300
+ }
301
+ async save(user) {
302
+ return this.client.post('/public/v1/users', ensureBodyId(user));
303
+ }
304
+ }
305
+ exports.NodeUsersDataSource = NodeUsersDataSource;
306
+ class NodeConnectionsDataSource extends NodeHttpDataSource {
307
+ constructor(client) {
308
+ super(client, 'connections');
309
+ }
310
+ async save(conn, saveCredentials = false) {
311
+ const q = buildQuery({
312
+ saveCredentials: saveCredentials ? 'true' : undefined
313
+ });
314
+ return this.client.post(`/public/v1/connections${q}`, conn);
315
+ }
316
+ async test(conn) {
317
+ const result = await this.client.post(`/public/v1/connections/test`, conn);
318
+ const text = typeof result === 'string' ? result.replace(/^"|"$/g, '') : String(result ?? '');
319
+ if (text !== 'ok') throw new Error(text || 'Connection test failed');
320
+ }
321
+ }
322
+ exports.NodeConnectionsDataSource = NodeConnectionsDataSource;
323
+ class NodeFuncsDataSource extends NodeHttpDataSource {
324
+ async run(name, params) {
325
+ const normalizedName = name.replace(':', '.');
326
+ const result = await this.client.post(`/public/v1/functions/${encodeURIComponent(normalizedName)}/call`, params ?? {});
327
+ // Datagrok returns HTTP 200 with an ApiError body when the function doesn't exist
328
+ const parsed = typeof result === 'string' ? tryParseJson(result) : result;
329
+ if (parsed?.['#type'] === 'ApiError') {
330
+ const err = {
331
+ error: parsed.message ?? 'Function call failed',
332
+ errorCode: parsed.errorCode,
333
+ stackTrace: parsed.stackTrace
334
+ };
335
+ throw Object.assign(new Error(err.error), {
336
+ apiError: err
337
+ });
338
+ }
339
+ return result;
340
+ }
341
+ }
342
+ exports.NodeFuncsDataSource = NodeFuncsDataSource;
343
+ function tryParseJson(s) {
344
+ try {
345
+ return JSON.parse(s);
346
+ } catch {
347
+ return null;
348
+ }
349
+ }
350
+ class NodeFilesDataSource {
351
+ constructor(client) {
352
+ this.client = client;
353
+ }
354
+ splitPath(filePath) {
355
+ const colonIdx = filePath.indexOf(':');
356
+ if (colonIdx === -1) return {
357
+ connector: filePath.replace(':', '.'),
358
+ path: ''
359
+ };
360
+ const connector = filePath.slice(0, colonIdx).replace(':', '.');
361
+ const path = filePath.slice(colonIdx + 1).replace(/^\//, '');
362
+ return {
363
+ connector,
364
+ path
365
+ };
366
+ }
367
+ async list(filePath, recursive = false) {
368
+ const {
369
+ connector,
370
+ path
371
+ } = this.splitPath(filePath);
372
+ const q = buildQuery({
373
+ recursive: recursive ? 'true' : undefined
374
+ });
375
+ const seg = path ? `${connector}/${path}` : connector;
376
+ return this.client.get(`/public/v1/files/${seg}${q}`);
377
+ }
378
+ async get(filePath) {
379
+ const {
380
+ connector,
381
+ path
382
+ } = this.splitPath(filePath);
383
+ return this.client.get(`/public/v1/files/${connector}/${path}`);
384
+ }
385
+ async delete(filePath) {
386
+ const {
387
+ connector,
388
+ path
389
+ } = this.splitPath(filePath);
390
+ await this.client.del(`/public/v1/files/${connector}/${path}`);
391
+ }
392
+ }
393
+ exports.NodeFilesDataSource = NodeFilesDataSource;
394
+ class NodeDapi {
395
+ constructor(client) {
396
+ this.client = client;
397
+ }
398
+ get users() {
399
+ return new NodeUsersDataSource(this.client);
400
+ }
401
+ get groups() {
402
+ return new NodeGroupsDataSource(this.client);
403
+ }
404
+ get functions() {
405
+ return new NodeFuncsDataSource(this.client, 'functions');
406
+ }
407
+ get connections() {
408
+ return new NodeConnectionsDataSource(this.client);
409
+ }
410
+ get queries() {
411
+ return new NodeHttpDataSource(this.client, 'queries');
412
+ }
413
+ get scripts() {
414
+ return new NodeHttpDataSource(this.client, 'scripts');
415
+ }
416
+ get packages() {
417
+ return new NodeHttpDataSource(this.client, 'packages');
418
+ }
419
+ get reports() {
420
+ return new NodeHttpDataSource(this.client, 'reports');
421
+ }
422
+ get files() {
423
+ return new NodeFilesDataSource(this.client);
424
+ }
425
+ get shares() {
426
+ return new NodeSharesDataSource(this.client);
427
+ }
428
+ async raw(method, path, body) {
429
+ // Raw paths are relative to server root (e.g. /api/users/current).
430
+ // Strip the trailing /api from baseUrl to avoid double prefix.
431
+ const serverRoot = this.client.baseUrl.replace(/\/api\/?$/, '');
432
+ const url = `${serverRoot}${path}`;
433
+ const opts = {
434
+ method: method.toUpperCase(),
435
+ headers: {
436
+ 'Authorization': this.client.token,
437
+ 'Content-Type': 'application/json'
438
+ }
439
+ };
440
+ if (body !== undefined) opts.body = JSON.stringify(body);
441
+ const res = await fetch(url, opts);
442
+ const ct = res.headers.get('content-type') ?? '';
443
+ if (ct.includes('application/json')) return res.json();
444
+ return res.text();
445
+ }
446
+ async batch(request) {
447
+ return this.client.post('/public/v1/batch', request);
448
+ }
449
+ async describe(entityType) {
450
+ try {
451
+ return await this.client.get(`/public/v1/entity-types/${encodeURIComponent(entityType)}`);
452
+ } catch {
453
+ return await this.client.get(`/entities/types${buildQuery({
454
+ name: entityType
455
+ })}`);
456
+ }
457
+ }
458
+ }
459
+ exports.NodeDapi = NodeDapi;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createClient = createClient;
7
+ var _nodeDapi = require("./node-dapi");
8
+ var _testUtils = require("./test-utils");
9
+ async function createClient(hostArg) {
10
+ const {
11
+ url,
12
+ key
13
+ } = (0, _testUtils.getDevKey)(hostArg ?? '');
14
+ return _nodeDapi.NodeApiClient.login(url, key);
15
+ }
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.cellStr = cellStr;
7
+ exports.csvCell = csvCell;
8
+ exports.getKeys = getKeys;
9
+ exports.printBatchOutput = printBatchOutput;
10
+ exports.printError = printError;
11
+ exports.printOutput = printOutput;
12
+ /// Docs: [Grok Dapi](/docs/plans/grok-dapi/)
13
+
14
+ function printOutput(data, format) {
15
+ if (data === null || data === undefined) {
16
+ if (format !== 'quiet') console.log('(empty)');
17
+ return;
18
+ }
19
+ const rows = Array.isArray(data) ? data : [data];
20
+ switch (format) {
21
+ case 'json':
22
+ console.log(JSON.stringify(data, null, 2));
23
+ break;
24
+ case 'csv':
25
+ printCsv(rows);
26
+ break;
27
+ case 'quiet':
28
+ for (const row of rows) {
29
+ if (typeof row === 'object' && row !== null) console.log(row.id ?? row.name ?? JSON.stringify(row));else console.log(row);
30
+ }
31
+ break;
32
+ default:
33
+ printTable(rows);
34
+ }
35
+ }
36
+ function cellStr(v) {
37
+ if (v === null || v === undefined) return '';
38
+ if (typeof v === 'object') {
39
+ if (v.name) return v.name;
40
+ if (v.id) return v.id;
41
+ return JSON.stringify(v).slice(0, 40);
42
+ }
43
+ return String(v);
44
+ }
45
+ function printTable(rows) {
46
+ if (!rows.length) {
47
+ console.log('(no results)');
48
+ return;
49
+ }
50
+ if (typeof rows[0] !== 'object' || rows[0] === null) {
51
+ for (const r of rows) console.log(r);
52
+ return;
53
+ }
54
+ const keys = getKeys(rows);
55
+ const widths = {};
56
+ for (const k of keys) widths[k] = k.length;
57
+ for (const row of rows) for (const k of keys) widths[k] = Math.max(widths[k], cellStr(row[k]).length);
58
+ const header = keys.map(k => k.padEnd(widths[k])).join(' ');
59
+ const sep = keys.map(k => '-'.repeat(widths[k])).join(' ');
60
+ console.log(header);
61
+ console.log(sep);
62
+ for (const row of rows) console.log(keys.map(k => cellStr(row[k]).padEnd(widths[k])).join(' '));
63
+ }
64
+ function printCsv(rows) {
65
+ if (!rows.length) return;
66
+ if (typeof rows[0] !== 'object' || rows[0] === null) {
67
+ for (const r of rows) console.log(csvCell(String(r)));
68
+ return;
69
+ }
70
+ const keys = getKeys(rows);
71
+ console.log(keys.map(csvCell).join(','));
72
+ for (const row of rows) console.log(keys.map(k => csvCell(cellStr(row[k]))).join(','));
73
+ }
74
+ function csvCell(s) {
75
+ return s.includes(',') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g, '""')}"` : s;
76
+ }
77
+ function getKeys(rows) {
78
+ const seen = new Set();
79
+ const keys = [];
80
+ for (const row of rows) for (const k of Object.keys(row)) if (!seen.has(k)) {
81
+ seen.add(k);
82
+ keys.push(k);
83
+ }
84
+ return keys.slice(0, 12);
85
+ }
86
+ function printBatchOutput(response, format) {
87
+ if (format === 'json') {
88
+ console.log(JSON.stringify(response, null, 2));
89
+ return;
90
+ }
91
+ if (format === 'quiet') {
92
+ const s = response.summary;
93
+ console.log(`${s.total} total: ${s.succeeded} succeeded, ${s.failed} failed, ${s.partial} partial, ${s.skipped} skipped`);
94
+ return;
95
+ }
96
+
97
+ // table and csv: print summary then per-op rows
98
+ const s = response.summary;
99
+ console.log(`Summary: ${s.total} total ${s.succeeded} succeeded ${s.failed} failed ${s.partial} partial ${s.skipped} skipped`);
100
+ if (!response.results.length) return;
101
+ console.log('');
102
+ const rows = response.results.map(r => {
103
+ const brief = r.status === 'error' ? r.error?.error ?? 'error' : r.status === 'skipped' ? r.reason ?? 'skipped' : r.status === 'partial' ? `${r.summary?.succeeded}/${r.summary?.total} succeeded` : '';
104
+ return {
105
+ id: r.id ?? '',
106
+ action: r.action,
107
+ status: r.status,
108
+ detail: brief
109
+ };
110
+ });
111
+ if (format === 'csv') printCsv(rows);else printTable(rows);
112
+
113
+ // Write per-op errors to stderr
114
+ const errors = response.results.filter(r => r.status === 'error');
115
+ if (errors.length) process.stderr.write(JSON.stringify(errors.map(r => ({
116
+ id: r.id,
117
+ action: r.action,
118
+ error: r.error
119
+ })), null, 2) + '\n');
120
+ }
121
+ function printError(err) {
122
+ const apiErr = err?.apiError;
123
+ const out = apiErr ?? {
124
+ error: String(err?.message ?? err)
125
+ };
126
+ process.stderr.write(JSON.stringify(out, null, 2) + '\n');
127
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "#{PACKAGE_NAME_LOWERCASE}",
3
- "friendlyName": "#{PACKAGE_NAME}",
3
+ "friendlyName": "#{PACKAGE_FRIENDLY_NAME}",
4
4
  "version": "0.0.1",
5
5
  "description": "#{PACKAGE_NAME} package",
6
6
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.1.8",
3
+ "version": "6.1.10",
4
4
  "description": "Utility to upload and publish packages to Datagrok",
5
5
  "homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
6
6
  "dependencies": {
@@ -32,7 +32,11 @@
32
32
  "prepublishOnly": "babel bin --extensions .ts -d bin",
33
33
  "babel": "babel bin --extensions .ts -d bin",
34
34
  "build": "babel bin --extensions .ts -d bin",
35
- "debug-source-map": "babel bin --extensions .ts -d bin --source-maps true"
35
+ "debug-source-map": "babel bin --extensions .ts -d bin --source-maps true",
36
+ "test": "vitest run --project unit",
37
+ "test:watch": "vitest --project unit",
38
+ "test:integration": "vitest run --project integration",
39
+ "test:all": "vitest run"
36
40
  },
37
41
  "bin": {
38
42
  "datagrok-upload": "./bin/_deprecated/upload.js",
@@ -69,13 +73,14 @@
69
73
  "@types/ignore-walk": "^4.0.3",
70
74
  "@types/inquirer": "^8.2.10",
71
75
  "@types/js-yaml": "^4.0.9",
72
- "@types/node": "^16.18.70",
76
+ "@types/node": "^18.0.0",
73
77
  "@types/papaparse": "^5.3.15",
74
78
  "@typescript-eslint/eslint-plugin": "^5.62.0",
75
79
  "@typescript-eslint/parser": "^5.62.0",
76
80
  "eslint": "^8.56.0",
77
81
  "eslint-config-google": "^0.14.0",
78
82
  "typescript": "^5.3.3",
83
+ "vitest": "^3.2.4",
79
84
  "webpack": "^5.89.0",
80
85
  "webpack-cli": "^5.1.4"
81
86
  }
@@ -0,0 +1,25 @@
1
+ import {defineConfig} from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ projects: [
6
+ {
7
+ test: {
8
+ name: 'unit',
9
+ environment: 'node',
10
+ include: ['bin/**/*.test.ts'],
11
+ exclude: ['bin/**/*.integration.test.ts'],
12
+ },
13
+ },
14
+ {
15
+ test: {
16
+ name: 'integration',
17
+ environment: 'node',
18
+ include: ['bin/**/*.integration.test.ts'],
19
+ testTimeout: 30_000,
20
+ hookTimeout: 30_000,
21
+ },
22
+ },
23
+ ],
24
+ },
25
+ });