@devicecloud.dev/dcd 5.7.1 → 5.8.0-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/commands/cloud.d.ts +5 -0
- package/dist/commands/cloud.js +3 -0
- package/dist/config/flags/execution.flags.d.ts +5 -0
- package/dist/config/flags/execution.flags.js +5 -0
- package/dist/constants.d.ts +5 -0
- package/dist/gateways/api-gateway.d.ts +30 -2
- package/dist/gateways/api-gateway.js +59 -6
- package/dist/mcp/tools/run-cloud-test.js +5 -0
- package/dist/services/results-polling.service.d.ts +1 -0
- package/dist/services/results-polling.service.js +14 -4
- package/dist/services/test-submission.service.d.ts +7 -0
- package/dist/services/test-submission.service.js +6 -1
- package/package.json +2 -2
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -129,6 +129,11 @@ export declare const cloudCommand: import("citty").CommandDef<{
|
|
|
129
129
|
readonly alias: ["maestroVersion"];
|
|
130
130
|
readonly description: "Maestro version to run your flow against. Use \"latest\" for the most recent version. See https://docs.devicecloud.dev/configuration/maestro-versions for supported versions.";
|
|
131
131
|
};
|
|
132
|
+
readonly priority: {
|
|
133
|
+
readonly type: "string";
|
|
134
|
+
readonly default: "normal";
|
|
135
|
+
readonly description: "Run priority within your organization (options: low, normal, high). Reorders only your own queued runs across your concurrency pool, for every flow in this upload; it never moves them ahead of other customers. Runs queued longer than 6 hours are promoted first, so low runs are never starved. Retries keep the same priority.";
|
|
136
|
+
};
|
|
132
137
|
readonly retry: {
|
|
133
138
|
readonly type: "string";
|
|
134
139
|
readonly description: "Automatically retry the run up to the number of times specified (same as pressing retry in the UI) - this is free of charge";
|
package/dist/commands/cloud.js
CHANGED
|
@@ -42,6 +42,7 @@ const DOWNLOAD_OPTIONS = ['ALL', 'FAILED'];
|
|
|
42
42
|
const REPORT_OPTIONS = ['allure', 'html', 'html-detailed', 'junit'];
|
|
43
43
|
const ORIENTATION_OPTIONS = ['0', '90'];
|
|
44
44
|
const RUNNER_TYPE_OPTIONS = ['default', 'm4', 'm1', 'gpu1', 'cpu1'];
|
|
45
|
+
const PRIORITY_OPTIONS = ['low', 'normal', 'high'];
|
|
45
46
|
const RENDER_ENGINE_OPTIONS = ['lavapipe', 'swiftshader'];
|
|
46
47
|
/**
|
|
47
48
|
* Primary CLI command for executing tests on DeviceCloud.
|
|
@@ -162,6 +163,7 @@ export const cloudCommand = defineCommand({
|
|
|
162
163
|
const moropoApiKey = args['moropo-v1-api-key'];
|
|
163
164
|
const name = args.name;
|
|
164
165
|
const orientation = validateEnum(args.orientation, ORIENTATION_OPTIONS, 'orientation');
|
|
166
|
+
const priority = validateEnum(args.priority, PRIORITY_OPTIONS, 'priority') ?? 'normal';
|
|
165
167
|
let quiet = Boolean(args.quiet);
|
|
166
168
|
const report = validateEnum(args.report, REPORT_OPTIONS, 'report');
|
|
167
169
|
let retry = parseIntFlag(args.retry, 'retry');
|
|
@@ -600,6 +602,7 @@ export const cloudCommand = defineCommand({
|
|
|
600
602
|
metadata: mergedMetadata,
|
|
601
603
|
name,
|
|
602
604
|
orientation,
|
|
605
|
+
priority,
|
|
603
606
|
raw: [],
|
|
604
607
|
report,
|
|
605
608
|
retry,
|
|
@@ -33,6 +33,11 @@ export declare const executionFlags: {
|
|
|
33
33
|
readonly alias: ["maestroVersion"];
|
|
34
34
|
readonly description: "Maestro version to run your flow against. Use \"latest\" for the most recent version. See https://docs.devicecloud.dev/configuration/maestro-versions for supported versions.";
|
|
35
35
|
};
|
|
36
|
+
readonly priority: {
|
|
37
|
+
readonly type: "string";
|
|
38
|
+
readonly default: "normal";
|
|
39
|
+
readonly description: "Run priority within your organization (options: low, normal, high). Reorders only your own queued runs across your concurrency pool, for every flow in this upload; it never moves them ahead of other customers. Runs queued longer than 6 hours are promoted first, so low runs are never starved. Retries keep the same priority.";
|
|
40
|
+
};
|
|
36
41
|
readonly retry: {
|
|
37
42
|
readonly type: "string";
|
|
38
43
|
readonly description: "Automatically retry the run up to the number of times specified (same as pressing retry in the UI) - this is free of charge";
|
|
@@ -33,6 +33,11 @@ export const executionFlags = {
|
|
|
33
33
|
alias: ['maestroVersion'],
|
|
34
34
|
description: 'Maestro version to run your flow against. Use "latest" for the most recent version. See https://docs.devicecloud.dev/configuration/maestro-versions for supported versions.',
|
|
35
35
|
},
|
|
36
|
+
priority: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
default: 'normal',
|
|
39
|
+
description: 'Run priority within your organization (options: low, normal, high). Reorders only your own queued runs across your concurrency pool, for every flow in this upload; it never moves them ahead of other customers. Runs queued longer than 6 hours are promoted first, so low runs are never starved. Retries keep the same priority.',
|
|
40
|
+
},
|
|
36
41
|
retry: {
|
|
37
42
|
type: 'string',
|
|
38
43
|
description: 'Automatically retry the run up to the number of times specified (same as pressing retry in the UI) - this is free of charge',
|
package/dist/constants.d.ts
CHANGED
|
@@ -115,6 +115,11 @@ export declare const flags: {
|
|
|
115
115
|
readonly alias: ["maestroVersion"];
|
|
116
116
|
readonly description: "Maestro version to run your flow against. Use \"latest\" for the most recent version. See https://docs.devicecloud.dev/configuration/maestro-versions for supported versions.";
|
|
117
117
|
};
|
|
118
|
+
readonly priority: {
|
|
119
|
+
readonly type: "string";
|
|
120
|
+
readonly default: "normal";
|
|
121
|
+
readonly description: "Run priority within your organization (options: low, normal, high). Reorders only your own queued runs across your concurrency pool, for every flow in this upload; it never moves them ahead of other customers. Runs queued longer than 6 hours are promoted first, so low runs are never starved. Retries keep the same priority.";
|
|
122
|
+
};
|
|
118
123
|
readonly retry: {
|
|
119
124
|
readonly type: "string";
|
|
120
125
|
readonly description: "Automatically retry the run up to the number of times specified (same as pressing retry in the UI) - this is free of charge";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TAppMetadata } from '../types.js';
|
|
2
2
|
import type { AuthContext } from '../types/domain/auth.types.js';
|
|
3
3
|
import type { LiveCommandStatus, LiveExecResult, LiveSession, LiveSessionSummary } from '../types/domain/live.types.js';
|
|
4
|
-
import { components } from '../types/generated/schema.types.js';
|
|
4
|
+
import { components, paths } from '../types/generated/schema.types.js';
|
|
5
5
|
/**
|
|
6
6
|
* Error thrown for non-OK API responses, carrying the HTTP status so callers
|
|
7
7
|
* can branch on auth failures etc. without string matching.
|
|
@@ -10,6 +10,17 @@ export declare class ApiError extends Error {
|
|
|
10
10
|
status: number;
|
|
11
11
|
constructor(message: string, status: number);
|
|
12
12
|
}
|
|
13
|
+
type ResultsResponse = paths['/results/{uploadId}']['get']['responses']['200']['content']['application/json'];
|
|
14
|
+
/**
|
|
15
|
+
* The last results body a poll loop received and the ETag it came with, so
|
|
16
|
+
* the next poll can revalidate with `If-None-Match` and get an empty 304 back
|
|
17
|
+
* when nothing changed. Create one per poll loop; `getResultsForUpload` fills
|
|
18
|
+
* and clears it.
|
|
19
|
+
*/
|
|
20
|
+
export interface ResultsPollCache {
|
|
21
|
+
body?: ResultsResponse;
|
|
22
|
+
etag?: string;
|
|
23
|
+
}
|
|
13
24
|
export declare const ApiGateway: {
|
|
14
25
|
/**
|
|
15
26
|
* Enhances generic "fetch failed" errors with more specific diagnostic information
|
|
@@ -98,7 +109,23 @@ export declare const ApiGateway: {
|
|
|
98
109
|
token?: string;
|
|
99
110
|
}>;
|
|
100
111
|
finishLargeFile(baseUrl: string, auth: AuthContext, fileId: string, partSha1Array: string[]): Promise<unknown>;
|
|
101
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Fetch the result rows for an upload, for the `dcd cloud` poll loop.
|
|
114
|
+
*
|
|
115
|
+
* Asks for `?view=summary`, which leaves out `result_files` and the rest of
|
|
116
|
+
* the columns the CLI never reads. An API that predates the view ignores
|
|
117
|
+
* the parameter and returns the full rows, a superset, so this works
|
|
118
|
+
* against either.
|
|
119
|
+
*
|
|
120
|
+
* With a `cache`, a repeat poll revalidates with `If-None-Match` and a 304
|
|
121
|
+
* hands back the cached body.
|
|
122
|
+
* @param baseUrl API base URL
|
|
123
|
+
* @param auth AuthContext carrying request headers
|
|
124
|
+
* @param uploadId Upload ID to fetch results for
|
|
125
|
+
* @param cache Optional per-poll-loop cache, filled and cleared here
|
|
126
|
+
* @returns The results response body
|
|
127
|
+
*/
|
|
128
|
+
getResultsForUpload(baseUrl: string, auth: AuthContext, uploadId: string, cache?: ResultsPollCache): Promise<{
|
|
102
129
|
statusCode?: number;
|
|
103
130
|
results?: components["schemas"]["TResultResponse"][];
|
|
104
131
|
}>;
|
|
@@ -209,3 +236,4 @@ export declare const ApiGateway: {
|
|
|
209
236
|
stopLiveSession(baseUrl: string, auth: AuthContext, sessionName: string): Promise<void>;
|
|
210
237
|
getLiveSession(baseUrl: string, auth: AuthContext, sessionName: string): Promise<LiveSession>;
|
|
211
238
|
};
|
|
239
|
+
export {};
|
|
@@ -347,20 +347,73 @@ export const ApiGateway = {
|
|
|
347
347
|
throw error;
|
|
348
348
|
}
|
|
349
349
|
},
|
|
350
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Fetch the result rows for an upload, for the `dcd cloud` poll loop.
|
|
352
|
+
*
|
|
353
|
+
* Asks for `?view=summary`, which leaves out `result_files` and the rest of
|
|
354
|
+
* the columns the CLI never reads. An API that predates the view ignores
|
|
355
|
+
* the parameter and returns the full rows, a superset, so this works
|
|
356
|
+
* against either.
|
|
357
|
+
*
|
|
358
|
+
* With a `cache`, a repeat poll revalidates with `If-None-Match` and a 304
|
|
359
|
+
* hands back the cached body.
|
|
360
|
+
* @param baseUrl API base URL
|
|
361
|
+
* @param auth AuthContext carrying request headers
|
|
362
|
+
* @param uploadId Upload ID to fetch results for
|
|
363
|
+
* @param cache Optional per-poll-loop cache, filled and cleared here
|
|
364
|
+
* @returns The results response body
|
|
365
|
+
*/
|
|
366
|
+
async getResultsForUpload(baseUrl, auth, uploadId, cache) {
|
|
351
367
|
// TODO: merge with getUploadStatus
|
|
368
|
+
const url = `${baseUrl}/results/${uploadId}?view=summary`;
|
|
352
369
|
try {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
370
|
+
const headers = { ...auth.headers };
|
|
371
|
+
if (cache?.body && cache.etag) {
|
|
372
|
+
headers['if-none-match'] = cache.etag;
|
|
373
|
+
// Per the Fetch spec, undici appends `Cache-Control: no-cache` to any
|
|
374
|
+
// request that carries a conditional header and no Cache-Control of
|
|
375
|
+
// its own, and Express's `fresh` never answers 304 to a no-cache
|
|
376
|
+
// request. So every poll would get a full 200. max-age=0 still asks
|
|
377
|
+
// for revalidation, and keeps undici from adding no-cache.
|
|
378
|
+
headers['cache-control'] = 'max-age=0';
|
|
379
|
+
}
|
|
380
|
+
const res = await fetch(url, { headers });
|
|
381
|
+
// Before the `!res.ok` check, since a 304 is not `ok`.
|
|
382
|
+
if (res.status === 304) {
|
|
383
|
+
if (cache?.body) {
|
|
384
|
+
return cache.body;
|
|
385
|
+
}
|
|
386
|
+
// We sent no If-None-Match, so there is nothing to reuse. Start over
|
|
387
|
+
// with an unconditional request on the next poll.
|
|
388
|
+
if (cache) {
|
|
389
|
+
delete cache.body;
|
|
390
|
+
delete cache.etag;
|
|
391
|
+
}
|
|
392
|
+
throw new Error('Failed to get results: API returned 304 Not Modified to an unconditional request');
|
|
393
|
+
}
|
|
356
394
|
if (!res.ok) {
|
|
357
395
|
await this.handleApiError(res, 'Failed to get results');
|
|
358
396
|
}
|
|
359
|
-
|
|
397
|
+
const body = await parseJsonResponse(res, 'Failed to get results');
|
|
398
|
+
if (cache) {
|
|
399
|
+
// Only pin a real result set. The API reports some errors as a 200
|
|
400
|
+
// with a `{ statusCode, message }` envelope, and an empty set is
|
|
401
|
+
// rejected by the poll loop; caching either would replay it on 304.
|
|
402
|
+
const etag = res.headers.get('etag');
|
|
403
|
+
if (etag && Array.isArray(body.results) && body.results.length > 0) {
|
|
404
|
+
cache.body = body;
|
|
405
|
+
cache.etag = etag;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
delete cache.body;
|
|
409
|
+
delete cache.etag;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return body;
|
|
360
413
|
}
|
|
361
414
|
catch (error) {
|
|
362
415
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
363
|
-
throw this.enhanceFetchError(error,
|
|
416
|
+
throw this.enhanceFetchError(error, url);
|
|
364
417
|
}
|
|
365
418
|
throw error;
|
|
366
419
|
}
|
|
@@ -70,6 +70,10 @@ export function registerRunCloudTest(server) {
|
|
|
70
70
|
.enum(['default', 'm4', 'm1', 'gpu1', 'cpu1'])
|
|
71
71
|
.optional()
|
|
72
72
|
.describe('Runner type (default "default"). gpu1 is deprecated and will be removed on 9 November 2026; prefer cpu1.'),
|
|
73
|
+
priority: z
|
|
74
|
+
.enum(['low', 'normal', 'high'])
|
|
75
|
+
.optional()
|
|
76
|
+
.describe('Run priority within the organization (default "normal"). Reorders only the queued runs of this organization, for every flow in this upload; it never moves them ahead of other customers. Runs queued over 6 hours are promoted first, so low runs are never starved.'),
|
|
73
77
|
configFile: z.string().optional().describe('Path to a workspace config.yaml'),
|
|
74
78
|
ignoreShaCheck: z
|
|
75
79
|
.boolean()
|
|
@@ -208,6 +212,7 @@ export function registerRunCloudTest(server) {
|
|
|
208
212
|
iOSDevice: args.iosDevice,
|
|
209
213
|
iOSVersion: args.iosVersion,
|
|
210
214
|
name: args.name,
|
|
215
|
+
priority: args.priority,
|
|
211
216
|
runnerType: args.runnerType ?? 'default',
|
|
212
217
|
maestroVersion: resolvedMaestroVersion,
|
|
213
218
|
retry: args.retry,
|
|
@@ -118,6 +118,7 @@ export declare class ResultsPollingService {
|
|
|
118
118
|
* @param apiUrl API base URL
|
|
119
119
|
* @param auth AuthContext carrying request headers
|
|
120
120
|
* @param uploadId Upload ID to fetch results for
|
|
121
|
+
* @param cache The poll loop's results cache, for ETag revalidation
|
|
121
122
|
* @param debug Whether debug logging is enabled
|
|
122
123
|
* @param logger Optional logger function
|
|
123
124
|
* @returns Promise resolving to test results
|
|
@@ -133,6 +133,9 @@ export class ResultsPollingService {
|
|
|
133
133
|
let { auth } = options;
|
|
134
134
|
this.initializePollingDisplay(json, logger);
|
|
135
135
|
let sequentialPollFailures = 0;
|
|
136
|
+
// The last result set and its ETag, so a poll that finds nothing changed
|
|
137
|
+
// gets an empty 304 instead of the whole set again.
|
|
138
|
+
const resultsCache = {};
|
|
136
139
|
const pollIntervalMs = auth.mode === 'bearer'
|
|
137
140
|
? this.BEARER_POLL_INTERVAL_MS
|
|
138
141
|
: this.APIKEY_POLL_INTERVAL_MS;
|
|
@@ -229,7 +232,7 @@ export class ResultsPollingService {
|
|
|
229
232
|
nextPollAt = null;
|
|
230
233
|
renderStatus();
|
|
231
234
|
auth = await this.refreshPollingAuth(auth, uploadId, subscription, debug, logger);
|
|
232
|
-
const updatedResults = await this.fetchAndLogResults(apiUrl, auth, uploadId, debug, logger);
|
|
235
|
+
const updatedResults = await this.fetchAndLogResults(apiUrl, auth, uploadId, resultsCache, debug, logger);
|
|
233
236
|
const { summary } = this.calculateStatusSummary(updatedResults);
|
|
234
237
|
if (!json) {
|
|
235
238
|
statusBody = this.buildStatusBody(updatedResults, quiet, summary);
|
|
@@ -395,21 +398,28 @@ export class ResultsPollingService {
|
|
|
395
398
|
* @param apiUrl API base URL
|
|
396
399
|
* @param auth AuthContext carrying request headers
|
|
397
400
|
* @param uploadId Upload ID to fetch results for
|
|
401
|
+
* @param cache The poll loop's results cache, for ETag revalidation
|
|
398
402
|
* @param debug Whether debug logging is enabled
|
|
399
403
|
* @param logger Optional logger function
|
|
400
404
|
* @returns Promise resolving to test results
|
|
401
405
|
*/
|
|
402
|
-
async fetchAndLogResults(apiUrl, auth, uploadId, debug, logger) {
|
|
406
|
+
async fetchAndLogResults(apiUrl, auth, uploadId, cache, debug, logger) {
|
|
403
407
|
if (debug && logger) {
|
|
404
408
|
logger(`[DEBUG] Polling for results: ${uploadId}`);
|
|
405
409
|
}
|
|
406
|
-
|
|
410
|
+
// A 304 hands back the cached body itself; a 200 is always a new object.
|
|
411
|
+
const cachedBody = cache.body;
|
|
412
|
+
const response = await ApiGateway.getResultsForUpload(apiUrl, auth, uploadId, cache);
|
|
413
|
+
const notModified = cachedBody !== undefined && response === cachedBody;
|
|
414
|
+
const { results: updatedResults } = response;
|
|
407
415
|
// An empty array would otherwise read as "all complete, all passed".
|
|
408
416
|
if (!updatedResults || updatedResults.length === 0) {
|
|
409
417
|
throw new Error('no results');
|
|
410
418
|
}
|
|
411
419
|
if (debug && logger) {
|
|
412
|
-
logger(
|
|
420
|
+
logger(notModified
|
|
421
|
+
? `[DEBUG] Poll not modified (304), reusing ${updatedResults.length} cached results`
|
|
422
|
+
: `[DEBUG] Poll received ${updatedResults.length} results`);
|
|
413
423
|
for (const result of updatedResults) {
|
|
414
424
|
logger(`[DEBUG] Result status: ${result.test_file_name} - ${result.status}`);
|
|
415
425
|
}
|
|
@@ -50,6 +50,13 @@ export interface TestSubmissionConfig {
|
|
|
50
50
|
metadata?: string[];
|
|
51
51
|
name?: string;
|
|
52
52
|
orientation?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Where this upload sits among the org's own queued runs. Like
|
|
55
|
+
* `cancelPrevious`, sent as its own field rather than inside `config`: it is
|
|
56
|
+
* a scheduling directive for the whole upload, not run configuration. Only
|
|
57
|
+
* reorders the org's own queue; it never moves the org ahead of others.
|
|
58
|
+
*/
|
|
59
|
+
priority?: 'low' | 'normal' | 'high';
|
|
53
60
|
raw?: unknown;
|
|
54
61
|
/**
|
|
55
62
|
* Which emulator software renderer to boot with (Android only). Omitted
|
|
@@ -20,7 +20,7 @@ export class TestSubmissionService {
|
|
|
20
20
|
* @returns The flow zip buffer, its SHA-256, and the string-encoded fields
|
|
21
21
|
*/
|
|
22
22
|
async buildTestPayload(config) {
|
|
23
|
-
const { apiUrl, appBinaryId, appPlatform, encrypt, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], cancelPrevious = false, googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, retry, continueOnFailure = true, renderEngine, report, showCrosshairs, maestroChromeOnboarding, raw, disableAnimations, deviceMatrix, debug = false, logger, } = config;
|
|
23
|
+
const { apiUrl, appBinaryId, appPlatform, encrypt, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], cancelPrevious = false, googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, priority, retry, continueOnFailure = true, renderEngine, report, showCrosshairs, maestroChromeOnboarding, raw, disableAnimations, deviceMatrix, debug = false, logger, } = config;
|
|
24
24
|
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, includedFiles = [], referencedFiles, sequence, workspaceConfig, } = executionPlan;
|
|
25
25
|
const { flows: sequentialFlows = [] } = sequence ?? {};
|
|
26
26
|
const envObject = this.parseKeyValuePairs(env);
|
|
@@ -168,6 +168,11 @@ export class TestSubmissionService {
|
|
|
168
168
|
if (cancelPrevious) {
|
|
169
169
|
fields.cancelPrevious = 'true';
|
|
170
170
|
}
|
|
171
|
+
// Same reasoning: 'normal' is the default and is never sent, so the
|
|
172
|
+
// default path's wire shape is unchanged and an older API never sees it.
|
|
173
|
+
if (priority && priority !== 'normal') {
|
|
174
|
+
fields.priority = priority;
|
|
175
|
+
}
|
|
171
176
|
this.setOptionalFields(fields, {
|
|
172
177
|
androidApiLevel,
|
|
173
178
|
androidDevice,
|
package/package.json
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"chai": "^6.2.2",
|
|
31
31
|
"eslint": "^10.5.0",
|
|
32
32
|
"eslint-config-prettier": "^10.1.8",
|
|
33
|
-
"eslint-plugin-unicorn": "^
|
|
33
|
+
"eslint-plugin-unicorn": "^77.0.0",
|
|
34
34
|
"husky": "^9.1.7",
|
|
35
35
|
"mocha": "^12.0.0",
|
|
36
36
|
"prettier": "^3.8.4",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"test:unit": "node scripts/test-runner.mjs --unit",
|
|
69
69
|
"typecheck": "tsc --noEmit -p tsconfig.test.json"
|
|
70
70
|
},
|
|
71
|
-
"version": "5.
|
|
71
|
+
"version": "5.8.0-beta.2",
|
|
72
72
|
"bugs": {
|
|
73
73
|
"url": "https://discord.gg/gm3mJwcNw8"
|
|
74
74
|
},
|